Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2541-inbox-back-to-queue.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Closing the Workflow graphs page that an Inbox card opened now brings the Inbox back, instead of leaving you in the chat under it; a Settings page opened anywhere else still closes to the view beneath it ([#2541](https://github.com/ThinkFlowLab/FlyBot/issues/2541)).
16 changes: 11 additions & 5 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9327,11 +9327,16 @@ function useNow2(ms) {
function openCard(actions, card) {
actions.closeInbox();
if (card?.kind === "workflow") {
actions.openSettings("workflows");
actions.openSettings("workflows", null, "inbox");
return;
}
actions.openSession(card.chat);
}
function closedSettings(state) {
if (!state.settings && !state.remoteBotSettings) return state;
const back = state.settings?.back === "inbox";
return { ...state, settings: null, remoteBotSettings: null, ...back ? { inbox: true } : {} };
}
function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) {
const work = useWork((value) => value);
const sessions = useSessions((value) => value);
Expand Down Expand Up @@ -23023,17 +23028,18 @@ function apply(ctx) {
renameBot: (id) => {
ui2.set((state) => ({ ...state, details: id, view: "details", renaming: true, tab: null }));
},
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
openSettings: (page = "general", id = null) => {
// `id` opens one Bot (page 'bots') or one group chat (page 'groups'). `back` is where
// closing the page returns to; a page the inbox opened sends the queue back (#2541).
openSettings: (page = "general", id = null, back = null) => {
const remote = id && roster.getSnapshot().byId[id]?.remote;
if (page === "bots" && remote) {
ui2.set((state) => ({ ...state, remoteBotSettings: id, settings: null, menu: null, palette: null, newChat: null }));
return;
}
ui2.set((state) => ({ ...state, settings: { page, id }, remoteBotSettings: null, menu: null, palette: null, newChat: null }));
ui2.set((state) => ({ ...state, settings: back ? { page, id, back } : { page, id }, remoteBotSettings: null, menu: null, palette: null, newChat: null }));
},
closeSettings: () => {
ui2.set((state) => state.settings || state.remoteBotSettings ? { ...state, settings: null, remoteBotSettings: null } : state);
ui2.set(closedSettings);
},
openMemory: (botId) => {
ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null }));
Expand Down
14 changes: 13 additions & 1 deletion core/src/client/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,15 +603,27 @@ function useNow(ms) {
// Open on a card. A failed workflow run has no chat to land in, so it opens the
// Workflow graphs page in settings instead (#1706); every other card keeps the chat.
// The page key is `workflows` — Settings falls back to General for any key it does not list.
// The page also carries the inbox as where closing it returns, so one card read does not cost
// an Inbox reopened by hand (#2541).
export function openCard(actions, card) {
actions.closeInbox()
if (card?.kind === 'workflow') {
actions.openSettings('workflows')
actions.openSettings('workflows', null, 'inbox')
return
}
actions.openSession(card.chat)
}

// Closing Settings goes back to where the page was opened from. A page a card opened says so
// (`back: 'inbox'`), and the inbox comes back over the conversation instead of the chat under
// it; any other page closes to the view beneath it (#2541). The shell runs exactly this
// transition, so what a test asserts here is what the close does.
export function closedSettings(state) {
if (!state.settings && !state.remoteBotSettings) return state
const back = state.settings?.back === 'inbox'
return { ...state, settings: null, remoteBotSettings: null, ...(back ? { inbox: true } : {}) }
}

export function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) {
const work = useWork(value => value)
const sessions = useSessions(value => value)
Expand Down
11 changes: 6 additions & 5 deletions core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { GROUP_SETTINGS_CSS } from './group-settings.js'
import { FOLDER_CSS } from './folder-menu.js'
import { USAGE_CSS } from './usage-page.js'
import { SCHEDULE_CSS } from './schedules.js'
import { INBOX_CSS } from './inbox.js'
import { INBOX_CSS, closedSettings } from './inbox.js'
import { readWorkflows } from './workflows.js'
import { KANBAN_CSS } from './kanban.js'
import { MOBILE_CSS } from './mobile.js'
Expand Down Expand Up @@ -506,16 +506,17 @@ export function apply(ctx) {
showDetails: (id, tab = null) => { ui.set(state => ({ ...state, details: id, view: 'details', renaming: false, tab })) },
// Opening the conversation would move focus to its composer and blur the name field.
renameBot: (id) => { ui.set(state => ({ ...state, details: id, view: 'details', renaming: true, tab: null })) },
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
openSettings: (page = 'general', id = null) => {
// `id` opens one Bot (page 'bots') or one group chat (page 'groups'). `back` is where
// closing the page returns to; a page the inbox opened sends the queue back (#2541).
openSettings: (page = 'general', id = null, back = null) => {
const remote = id && roster.getSnapshot().byId[id]?.remote
if (page === 'bots' && remote) {
ui.set(state => ({ ...state, remoteBotSettings: id, settings: null, menu: null, palette: null, newChat: null }))
return
}
ui.set(state => ({ ...state, settings: { page, id }, remoteBotSettings: null, menu: null, palette: null, newChat: null }))
ui.set(state => ({ ...state, settings: back ? { page, id, back } : { page, id }, remoteBotSettings: null, menu: null, palette: null, newChat: null }))
},
closeSettings: () => { ui.set(state => (state.settings || state.remoteBotSettings ? { ...state, settings: null, remoteBotSettings: null } : state)) },
closeSettings: () => { ui.set(closedSettings) },
openMemory: (botId) => { ui.set(state => ({ ...state, memory: botId, menu: null, palette: null })) },
closeMemory: () => { ui.set(state => (state.memory ? { ...state, memory: null } : state)) },
// The Agent inbox (#349) opens over the conversation; Open on a card closes it first.
Expand Down
66 changes: 66 additions & 0 deletions core/test/inbox-settings-back.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// A failed workflow run has no chat, so Open on its inbox card opens the Workflow graphs page
// in Settings (#1706, inbox.js `openCard`). Closing that page did not come back to the queue:
// `closeSettings` only nulled the panel, so the person landed on whatever conversation lay under
// the inbox they had opened. The page a card opens now carries where closing it returns to, and
// the settings close reads that marker. `closedSettings` is the one transition the UI store runs
// for a close, so the shell and this test take the same path.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { runInNewContext } from 'node:vm'
import { build } from 'esbuild'

// inbox.js is a browser module: React and the icon set are the shell's, so it is bundled once with
// those left external and the platform modules stubbed, the way inbox.test.js loads it.
const bundle = await build({
entryPoints: [fileURLToPath(new URL('../src/client/inbox.js', import.meta.url))],
bundle: true, format: 'cjs', platform: 'node', write: false,
external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'],
})
const module = { exports: {} }
runInNewContext(bundle.outputFiles[0].text, {
module, exports: module.exports, setTimeout: () => 0, clearTimeout: () => {}, setInterval: () => 0, clearInterval: () => {}, Date, Promise,
require: (name) => {
const platform = {
react: {
createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}],
useEffect: () => {}, useLayoutEffect: () => {}, useRef: current => ({ current }), useCallback: fn => fn, useMemo: fn => fn(), Fragment: 'fragment',
},
'react-dom': {},
'@deepseek-ai/dsh-client-ui-primitives': {},
}
if (!(name in platform)) throw new Error(`no platform module ${name}`)
return platform[name]
},
})
const { openCard } = module.exports
const closedSettings = state => module.exports.closedSettings(state)

const calls = []
const actions = {
closeInbox: () => calls.push(['close']),
openSettings: (...args) => calls.push(['settings', ...args]),
openSession: id => calls.push(['session', id]),
}

test('#2541: a workflow card asks Settings to return to the inbox when it closes', () => {
calls.length = 0
openCard(actions, { kind: 'workflow', id: 'wf-1' })
assert.deepEqual(calls, [['close'], ['settings', 'workflows', null, 'inbox']], 'the page names the inbox as where closing returns')
})

test('#2541: closing that page brings the inbox back instead of the chat under it', () => {
const opened = { inbox: null, settings: { page: 'workflows', id: null, back: 'inbox' }, remoteBotSettings: null }
const closed = closedSettings(opened)
assert.equal(closed.inbox, true, 'the inbox is open again')
assert.equal(closed.settings, null)
})

test('#2541: a Settings page opened any other way still closes to the view under it', () => {
const closed = closedSettings({ inbox: null, settings: { page: 'workflows', id: null }, remoteBotSettings: null })
assert.equal(closed.inbox, null, 'no inbox comes back that was not open before')
assert.equal(closed.settings, null)
const untouched = { inbox: true, settings: null, remoteBotSettings: null }
assert.equal(closedSettings(untouched), untouched, 'a close with no panel open leaves the state alone')
})
Loading
Loading