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/261-landing-part-session-list.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The main view no longer jumps to the Main Bot when the roster drops a Bot whose chat is still open ([#261](https://github.com/ThinkFlowLab/FlyBot/issues/261)): `landingPart` asks the session list whether that conversation exists, so a revoked share or a removed Bot leaves the open chat (or its blank chat) in place, while a New Session the user opened and a session archived with its Bot still go to the Main Bot.
10 changes: 6 additions & 4 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2561,10 +2561,12 @@ function relativeShort(ms) {
var contentText = (content) => (content ?? []).filter((block) => block?.type === "text").map((block) => block.text).join("\n");
var HEADER = /^\[(Message from|Reply from|Group chat|Group post from|Bot team setup|Handoff · part|Secret card)[^\]]*\]\n?/;
var currentPart = (bot) => bot.sessionId ?? bot.id;
function landingPart(roster, last, mainId, { stay = false } = {}) {
function landingPart(roster, last, mainId, { stay = false, view = "", sessions } = {}) {
const bot = last ? roster.byId[last.botId] : void 0;
if (bot && currentPart(bot) !== last.sessionId) return currentPart(bot);
if (bot && stay) return last.sessionId;
const held = last !== null && last !== void 0 && (stay || view === last.sessionId) && sessions?.byId?.[last.sessionId] !== void 0;
if (held) return last.sessionId;
const main = roster.byId[mainId];
return main ? currentPart(main) : mainId;
}
Expand Down Expand Up @@ -8019,7 +8021,7 @@ function useLandOnMainBot(roster, useSessions, actions) {
return void 0;
}
const last = lastBot.current;
const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "" }), last ? 0 : 400);
const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400);
return () => clearTimeout(timer);
}, [mainView, roster]);
}
Expand Down Expand Up @@ -17458,11 +17460,11 @@ function apply(ctx) {
},
// `last` is the Bot conversation the view held before it cleared: when that part
// was archived for a fresh one, the view goes on in the fresh one.
landOn: async (id, last, { stay = false } = {}) => {
landOn: async (id, last, { stay = false, view = "" } = {}) => {
if (last) await pull().catch(() => {
});
setPlain(false);
const target = landingPart(roster.getSnapshot(), last, id, { stay });
const target = landingPart(roster.getSnapshot(), last, id, { stay, view, sessions: ctx.sessions.list.getSnapshot() });
noteNavigation("landOn", target, last?.sessionId);
expected = target;
ctx.uiWorkspace.openSession(target);
Expand Down
4 changes: 2 additions & 2 deletions core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,10 @@ export function apply(ctx) {
},
// `last` is the Bot conversation the view held before it cleared: when that part
// was archived for a fresh one, the view goes on in the fresh one.
landOn: async (id, last, { stay = false } = {}) => {
landOn: async (id, last, { stay = false, view = '' } = {}) => {
if (last) await pull().catch(() => { /* a stale team list is fixed by the next pull */ })
setPlain(false)
const target = landingPart(roster.getSnapshot(), last, id, { stay })
const target = landingPart(roster.getSnapshot(), last, id, { stay, view, sessions: ctx.sessions.list.getSnapshot() })
noteNavigation('landOn', target, last?.sessionId)
expected = target
ctx.uiWorkspace.openSession(target)
Expand Down
4 changes: 3 additions & 1 deletion core/src/client/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,9 @@ function useLandOnMainBot(roster, useSessions, actions) {
}
const last = lastBot.current
// An emptied view (id '') comes back to the chat it held; a blank New Session the user opened goes to the Main Bot.
const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === '' }), last ? 0 : 400)
// `view` goes along so a blank chat of the held session is told apart from a New Session the user
// opened, and the session list decides whether that chat still exists (#261).
const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === '', view: id }), last ? 0 : 400)
return () => clearTimeout(timer)
}, [mainView, roster])
}
Expand Down
9 changes: 8 additions & 1 deletion core/src/client/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,17 @@ export const currentPart = bot => bot.sessionId ?? bot.id
// one: the chat goes on in the fresh part. The view emptied while that part is still current (a session
// list reload, not the user leaving): the same chat again, never the Main Bot (#261). Anything else:
// the Main Bot.
export function landingPart(roster, last, mainId, { stay = false } = {}) {
// The session list, not the roster, says whether that chat still exists (#261): a Bot a friend revokes
// (or removes) drops out of `roster.byId` while its session stays open, and the view must stay on that
// conversation rather than jump to the Main Bot. `view` is the id the main view holds now, so a blank
// chat of that same session stays too, while a New Session the user opened is a different destination
// and still goes to the Main Bot.
export function landingPart(roster, last, mainId, { stay = false, view = '', sessions } = {}) {
const bot = last ? roster.byId[last.botId] : undefined
if (bot && currentPart(bot) !== last.sessionId) return currentPart(bot)
if (bot && stay) return last.sessionId
const held = last !== null && last !== undefined && (stay || view === last.sessionId) && sessions?.byId?.[last.sessionId] !== undefined
if (held) return last.sessionId
const main = roster.byId[mainId]
return main ? currentPart(main) : mainId
}
Expand Down
19 changes: 18 additions & 1 deletion core/test/land-on.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,28 @@ test('a part archived for a fresh one goes on in the fresh part', () => {

test('a blank New Session the user opened, a removed Bot, or no last chat goes to the Main Bot', () => {
assert.equal(landingPart(roster, last, 'chief'), 'chief-2', 'not an emptied view')
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: true }), 'chief-2', 'the Bot is gone')
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: true }), 'chief-2', 'the Bot is gone and its session with it')
assert.equal(landingPart(roster, null, 'chief', { stay: true }), 'chief-2')
assert.equal(landingPart({ byId: {} }, null, 'chief'), 'chief', 'no roster entry for the Main Bot: its id')
})

// #261: the session list, not the roster, says whether that conversation still exists. The roster
// drops a Bot when a friend revokes the share or removes it, while the chat itself stays open.
const sessions = { byId: { 'friend-1': { id: 'friend-1' }, 'chief-2': { id: 'chief-2' } } }

test('#261: a roster that lost the Bot keeps the held chat whose session still exists', () => {
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: true, sessions }), 'friend-1', 'the view emptied, the chat is still there')
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: false, view: 'friend-1', sessions }), 'friend-1', 'a blank chat of that same session stays')
})

test('#261: a session that is gone, or a New Session the user opened, still goes to the Main Bot', () => {
const gone = { byId: { 'chief-2': { id: 'chief-2' } } }
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: true, sessions: gone }), 'chief-2', 'the session went with the Bot')
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: false, view: 'new-1', sessions }), 'chief-2', 'a New Session is a different destination')
assert.equal(landingPart({ byId: { chief } }, last, 'chief', { stay: false, sessions }), 'chief-2', 'no view: not a held chat')
assert.equal(landingPart(roster, last, 'chief', { stay: true, sessions }), 'friend-1', 'with the roster intact nothing moves')
})

test('a navigation with no user activation behind it is noted, with where it went and from where; a click is not', () => {
const saved = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
const warn = console.warn
Expand Down
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Add a new invariant under the heading it belongs to (or a new heading), not at t

### Main view landing and navigation trace (#261)

`useLandOnMainBot` (`sidebar.js`) puts an emptied main view back on a Bot. `landingPart` (`text.js`) picks where: a part archived for a fresh one goes on in the fresh part; a view that emptied while its chat is still the Bot's current part (a session list reload, not the user leaving) returns to that same chat; a blank New Session the user opened, or a removed Bot, goes to the Main Bot. Every `openSession` and `landOn` with no user activation behind it (`navigator.userActivation.isActive === false`) is written to the client diagnostics and the console (`navigation: landOn to … from … with no click (caller stack)`), so a jump a user cannot explain names its caller.
`useLandOnMainBot` (`sidebar.js`) puts an emptied main view back on a Bot. `landingPart` (`text.js`) picks where: a part archived for a fresh one goes on in the fresh part; a view that emptied while its chat is still the Bot's current part (a session list reload, not the user leaving) returns to that same chat; a chat whose Bot the roster no longer lists (a revoked share, a removal) stays put while its session is still in the session list, including a blank chat of that same session, because the session list — not the roster — says whether the conversation exists (#261); a New Session the user opened, or a session archived together with its Bot, goes to the Main Bot. Every `openSession` and `landOn` with no user activation behind it (`navigator.userActivation.isActive === false`) is written to the client diagnostics and the console (`navigation: landOn to … from … with no click (caller stack)`), so a jump a user cannot explain names its caller.

### Secrets page scope control (#545)

Expand Down
25 changes: 15 additions & 10 deletions patches/ds-bot/0001-core-fixes.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
FlyBot patch 0001-core-fixes on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/).

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
Expand Down Expand Up @@ -1787,25 +1785,32 @@ diff --git a/src/client/text.js b/src/client/text.js
function clock(ms) {
return new Date(ms).toLocaleTimeString(dateLocale(), { hour: 'numeric', minute: '2-digit' })
}
@@ -48,6 +48,17 @@ export function relativeShort(ms) {
@@ -48,6 +48,24 @@ export function relativeShort(ms) {
export const contentText = content => (content ?? []).filter(block => block?.type === 'text').map(block => block.text).join('\n')
export const HEADER = /^\[(Message from|Reply from|Group chat|Group post from|Bot team setup|Handoff · part|Secret card)[^\]]*\]\n?/
export const currentPart = bot => bot.sessionId ?? bot.id
+// Where an emptied main view goes. `last` is the Bot conversation it held. A part archived for a fresh
+// one: the chat goes on in the fresh part. The view emptied while that part is still current (a session
+// list reload, not the user leaving): the same chat again, never the Main Bot (#261). Anything else:
+// the Main Bot.
+export function landingPart(roster, last, mainId, { stay = false } = {}) {
+// The session list, not the roster, says whether that chat still exists (#261): a Bot a friend revokes
+// (or removes) drops out of `roster.byId` while its session stays open, and the view must stay on that
+// conversation rather than jump to the Main Bot. `view` is the id the main view holds now, so a blank
+// chat of that same session stays too, while a New Session the user opened is a different destination
+// and still goes to the Main Bot.
+export function landingPart(roster, last, mainId, { stay = false, view = '', sessions } = {}) {
+ const bot = last ? roster.byId[last.botId] : undefined
+ if (bot && currentPart(bot) !== last.sessionId) return currentPart(bot)
+ if (bot && stay) return last.sessionId
+ const held = last !== null && last !== undefined && (stay || view === last.sessionId) && sessions?.byId?.[last.sessionId] !== undefined
+ if (held) return last.sessionId
+ const main = roster.byId[mainId]
+ return main ? currentPart(main) : mainId
+}
// The prompts in a due scheduled task's message. DSH frames it for the model: one
// task as `reminder_prompt_json: "…"`, several as `reminders_json: [{reminder_prompt}]`.
export function reminderPrompts(text) {
@@ -62,6 +62,15 @@ export function reminderPrompts(text) {
@@ -62,6 +69,15 @@ export function reminderPrompts(text) {
}
}
export const stripHeader = text => text.replace(HEADER, '')
Expand All @@ -1821,7 +1826,7 @@ diff --git a/src/client/text.js b/src/client/text.js

// Of the Bots in a conversation, the one whose memory changed last, by the host's
// clock: changes reaching one window in the same poll share a `seen` time.
@@ -95,6 +95,13 @@ export function summaryBlocks(text) {
@@ -95,6 +102,13 @@ export function summaryBlocks(text) {
return blocks
}

Expand All @@ -1835,7 +1840,7 @@ diff --git a/src/client/text.js b/src/client/text.js
export function isHiddenTurn(roster, sessionId, turn) {
if (sessionId === undefined || turn === undefined) return false
return (roster.exchangeTurns[sessionId] ?? []).includes(turn)
@@ -102,6 +102,13 @@ export function isHiddenTurn(roster, sessionId, turn) {
@@ -102,6 +109,13 @@ export function isHiddenTurn(roster, sessionId, turn) {

export const toolName = root => root.name || root.call?.name

Expand All @@ -1849,7 +1854,7 @@ diff --git a/src/client/text.js b/src/client/text.js
// While a Session's history loads, a card asked in it may yet be drawn at its call,
// so the docks hold it back until then.
export const historyLoading = snapshot => snapshot?.openState === 'cold' || snapshot?.openState === 'loading'
@@ -125,7 +125,7 @@ export function toolArgs(root) {
@@ -125,7 +132,7 @@ export function toolArgs(root) {
}

// A relay context is a Bot-to-Bot request, reply or stop notice injected into the
Expand All @@ -1858,7 +1863,7 @@ diff --git a/src/client/text.js b/src/client/text.js
// parts) are not events in the chat, nor is a request handed back after its line was
// already shown: deferred, resumed after a restart, or retried after a tool call
// written as text.
@@ -200,14 +200,26 @@ export function commRunOf(order, peers, turn, hiddenTurns = []) {
@@ -200,14 +207,26 @@ export function commRunOf(order, peers, turn, hiddenTurns = []) {
}

// Latest visible line for a sidebar row; Bot-to-Bot exchange turns stay out of it.
Expand Down Expand Up @@ -1888,7 +1893,7 @@ diff --git a/src/client/text.js b/src/client/text.js
}
return ''
}
@@ -230,3 +230,35 @@ export function usageOf(list, ids) {
@@ -230,3 +237,35 @@ export function usageOf(list, ids) {
const hit = input > 0 ? Math.round((cacheRead / input) * 100) : 0
return t('{tokens} tok · Cache hit {hit}%', { tokens: formatTokens(input + output), hit })
}
Expand Down
18 changes: 9 additions & 9 deletions patches/ds-bot/0003-flybot-api-client.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
FlyBot patch 0003-flybot-api-client on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/).

diff --git a/src/client/activity-row.js b/src/client/activity-row.js
--- a/src/client/activity-row.js
+++ b/src/client/activity-row.js
Expand Down Expand Up @@ -1306,11 +1304,11 @@ diff --git a/src/client/index.js b/src/client/index.js
- landOn: async (id, last) => {
- if (last) await pull().catch(() => {})
- const bot = last ? roster.getSnapshot().byId[last.botId] : undefined
+ landOn: async (id, last, { stay = false } = {}) => {
+ landOn: async (id, last, { stay = false, view = '' } = {}) => {
+ if (last) await pull().catch(() => { /* a stale team list is fixed by the next pull */ })
setPlain(false)
- const target = bot && currentPart(bot) !== last.sessionId ? currentPart(bot) : partFor(id)
+ const target = landingPart(roster.getSnapshot(), last, id, { stay })
+ const target = landingPart(roster.getSnapshot(), last, id, { stay, view, sessions: ctx.sessions.list.getSnapshot() })
+ noteNavigation('landOn', target, last?.sessionId)
expected = target
ctx.uiWorkspace.openSession(target)
Expand Down Expand Up @@ -2300,17 +2298,19 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
groups.map((group, index) => h(Fragment, { key: index },
index > 0 ? h('div', { className: 'bt-menu-sep', role: 'separator' }) : null,
group.map(([label, Icon, run, hint]) => h('button', {
@@ -556,7 +556,8 @@ function useLandOnMainBot(roster, useSessions, actions) {
@@ -556,7 +556,10 @@ function useLandOnMainBot(roster, useSessions, actions) {
return undefined
}
const last = lastBot.current
- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400)
+ // An emptied view (id '') comes back to the chat it held; a blank New Session the user opened goes to the Main Bot.
+ const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === '' }), last ? 0 : 400)
+ // `view` goes along so a blank chat of the held session is told apart from a New Session the user
+ // opened, and the session list decides whether that chat still exists (#261).
+ const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === '', view: id }), last ? 0 : 400)
return () => clearTimeout(timer)
}, [mainView, roster])
}
@@ -572,9 +572,9 @@ export function FooterAccount(props) {
@@ -572,9 +574,9 @@ export function FooterAccount(props) {
export function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface(value => value.mode)
if (mode !== 'agent') return null
Expand All @@ -2323,7 +2323,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
}

// Agent mode has none of the shell-group styles, so the return button carries its own.
@@ -582,22 +582,22 @@ export const RETURN_CSS = `
@@ -582,22 +584,22 @@ export const RETURN_CSS = `
.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease}
.bt-return:hover{background:rgba(127,127,127,.14)}
.bt-return:active{transform:scale(.96)}
Expand Down Expand Up @@ -2351,7 +2351,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
// shell's footer classes are CSS-module names, so they match on their stems.
export const SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
@@ -658,4 +658,14 @@ export const SIDEBAR_CSS = `
@@ -658,4 +660,14 @@ export const SIDEBAR_CSS = `
.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)}
@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}}
@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}}
Expand Down
Loading
Loading