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/452-tree-reorg-move-not-rename.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tree reorganization moves / reparents existing Bots; it never renames them to `归档-` and recreates. Archived Bots are hidden from the default Bot list. A one-shot repair restores leftover `归档-*` / `Archive-*` names.
5 changes: 3 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -6765,7 +6765,8 @@ function TeamSidebar(props) {
const [collapsed, setCollapsed] = (0, import_react10.useState)({});
const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] }));
const inTree = (entry) => entry.kind === "bot" && !entry.item.remote;
const tree = structure && wide && main ? botTree([main, ...order.filter(inTree).map((entry) => entry.item)], main.id, (id) => structure.parentOf(id)) : null;
const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false;
const tree = structure && wide && main ? botTree([main, ...order.filter(listed).map((entry) => entry.item)], main.id, (id) => structure.parentOf(id)) : null;
useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(","));
useLandOnMainBot(roster, useSessions, actions);
const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide };
Expand Down Expand Up @@ -6796,7 +6797,7 @@ function TeamSidebar(props) {
"div",
{ ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") },
tree ? (0, import_react10.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react10.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react10.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react10.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
order.filter((entry) => !(tree && inTree(entry))).map((entry) => entry.kind === "bot" ? (0, import_react10.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react10.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react10.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react10.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react10.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react10.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
wide ? (0, import_react10.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react10.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
)
);
Expand Down
3 changes: 2 additions & 1 deletion core/src/client/flybot-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// ds-bot's own components render whatever is registered:
//
// flybot.sidebar.section { id, order, component({ roster, actions, call }) } below the Bot list
// flybot.sidebar.structure { id, order, parentOf(botId), menuItems?, nodeActions? } the Bot list becomes one tree under the Main Bot;
// flybot.sidebar.structure { id, order, parentOf(botId), include?(botId), menuItems?, nodeActions? } the Bot list becomes one tree under the Main Bot;
// include(botId) === false hides that Bot from the default list (e.g. tree-archived);
// menuItems({ bot }) → [{ id, label, visible, run({ bot }) }] adds items to a Bot's right-click
// menu (visible false hides one; a string from run shows as a toast; a throw as the error toast);
// nodeActions({ bot }) is drawn after each tree row
Expand Down
6 changes: 4 additions & 2 deletions core/src/client/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,10 @@ export function TeamSidebar(props) {
const [collapsed, setCollapsed] = useState({})
const toggle = id => setCollapsed(value => ({ ...value, [id]: !value[id] }))
// Only own Bots belong to the tree; a friend's Bot stays in the ordered list below it.
// structure.include(id) === false hides a Bot from the default list (e.g. tree-archived, #452).
const inTree = entry => entry.kind === 'bot' && !entry.item.remote
const tree = structure && wide && main ? botTree([main, ...order.filter(inTree).map(entry => entry.item)], main.id, id => structure.parentOf(id)) : null
const listed = entry => inTree(entry) && structure?.include?.(entry.item.id) !== false
const tree = structure && wide && main ? botTree([main, ...order.filter(listed).map(entry => entry.item)], main.id, id => structure.parentOf(id)) : null
useListGlide(listRef, wide, order.map(entry => entry.item.id).join(','))
useLandOnMainBot(roster, useSessions, actions)
const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }
Expand All @@ -383,7 +385,7 @@ export function TeamSidebar(props) {
h('div', { ref: listRef, className: 'bt-list', role: 'region', 'aria-label': t('Bot list') },
tree ? h('div', { className: 'bt-tree', role: 'group', 'aria-label': t('Bot tree') }, tree.map(node => h(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions })))
: main ? h(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : h('div', { className: 'bt-hint' }, t('Setting up your Main Bot…')),
order.filter(entry => !(tree && inTree(entry))).map(entry => (entry.kind === 'bot'
order.filter(entry => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map(entry => (entry.kind === 'bot'
? h(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true })
: entry.kind === 'room'
? h(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true })
Expand Down
14 changes: 14 additions & 0 deletions core/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export function install(rt) {
systemPrompt: bot.systemPrompt ?? '', fields: clone(bot.fields ?? {}),
...(typeof bot.color === 'string' ? { color: bot.color } : {}),
...(bot.avatar && typeof bot.avatar === 'object' ? { avatar: clone(bot.avatar) } : {}),
...(bot.hidden === true ? { hidden: true } : {}),
})
const persist = () => rt.save().catch(error => warn('flybot: save failed: %s', error))

Expand Down Expand Up @@ -605,6 +606,18 @@ export function install(rt) {
})
}

/** Rename and/or hide a Bot in the sidebar. Used by plugins (e.g. tree archive, #452). */
async function updateBotFields(id, patch = {}) {
const bot = rt.botOf(id)
if (bot === undefined) throw new Error(`flybot: unknown Bot ${id}`)
const name = patch.name !== undefined ? String(patch.name).trim() : undefined
const wantName = name !== undefined && name !== bot.name
const wantHidden = typeof patch.hidden === 'boolean' && Boolean(patch.hidden) !== Boolean(bot.hidden)
if (wantName) await rt.updateBot(id, { name })
if (wantHidden) await rt.setFlags(id, { hidden: Boolean(patch.hidden) })
return view(rt.botOf(id))
}

async function send({ from, to, text, role = 'request', hop = 0, via = SHARED, meta, attachments } = {}) {
await rt.load()
if (typeof text !== 'string' || text.trim() === '') throw new Error('flybot: send needs text')
Expand Down Expand Up @@ -938,6 +951,7 @@ export function install(rt) {
hasAnyGrant: id => rt.hasAnyGrant?.(id) === true,
of: sessionId => view(rt.botOf(rt.selfOf(sessionId))),
create: spec => create({ plugin, ...spec }),
update: (id, patch) => updateBotFields(id, patch),
setSystemPrompt,
// Same object as flybot.fields.
fields,
Expand Down
5 changes: 3 additions & 2 deletions core/test/new-chat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ test('the sidebar + menu wires plugin actions in after New group chat', () => {
assert.match(sidebar, /Number\(foreign\(a\)\) - Number\(foreign\(b\)\)/)
// With @flybot/tree on, a friend's Bot is not a tree node under Main; it stays in the flat list.
assert.match(sidebar, /const inTree = entry => entry\.kind === 'bot' && !entry\.item\.remote/)
assert.match(sidebar, /botTree\(\[main, \.\.\.order\.filter\(inTree\)/)
assert.match(sidebar, /order\.filter\(entry => !\(tree && inTree\(entry\)\)\)/)
assert.match(sidebar, /structure\?\.include\?\.\(entry\.item\.id\) !== false/)
assert.match(sidebar, /botTree\(\[main, \.\.\.order\.filter\(listed\)/)
assert.match(sidebar, /order\.filter\(entry => !\(tree && inTree\(entry\)\) && !\(inTree\(entry\) && structure\?\.include/)
assert.equal(DICTIONARY.zh['Copy my handle'], '复制我的 handle')
assert.equal(DICTIONARY.zh['Face-to-face'], '面对面')
assert.equal(DICTIONARY.zh['Face-to-face room'], '面对面房间')
Expand Down
4 changes: 4 additions & 0 deletions core/types/flybot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export interface BotView {
color?: string
/** Owner-chosen avatar ({ shape }, { image }, or { emoji }). Omitted when unset. */
avatar?: { shape?: string; image?: string; emoji?: string }
/** True when hidden from the default Bot list (Search still finds it). */
hidden?: boolean
}

/** 'change' payload. Delivered for the listener's own plugin unless it subscribed with { all: true }. */
Expand Down Expand Up @@ -334,6 +336,8 @@ export interface FlyBot {
of(sessionId: string): BotView | undefined
/** Returns the new Bot's id. */
create(spec: CreateBotSpec): Promise<BotId>
/** Rename and/or set sidebar flags. Plugins use this to hide archived Bots (#452). */
update(id: BotId, patch: { name?: string; hidden?: boolean }): Promise<BotView>
setSystemPrompt(id: BotId, text: string): void
/** Same object as FlyBot.fields. */
fields: BotFields
Expand Down
4 changes: 2 additions & 2 deletions docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second

### One sidebar list

- `TeamSidebar` lists local Bots, listed remote chats (`isListedRemoteChat`), live face-to-face rooms, and shared rooms in one `bt-list`. Own chats sort before foreign ones (a remote Bot, a face room, or `sharedFrom`); pinned and recency still win. A friend's Bot keeps the globe `RemoteMark`; a live face room gets a small people `FaceMark` (`bt-row-remote bt-row-face`). No row wraps its title (#384): a friend's Bot row shows the Bot's name (`bt-row-title`), then its owner (`bt-row-owner`, muted, shrinks first) and the globe (at `--bt-ink-2`, readable) on one line; its second line is the preview like an own row, or *Friend's Bot* before the first message, and the full `remoteBotTitle` ("Helper (Bob)") is its hover text and label. The row wrap's gear is absolutely positioned so the hover/selected band stays full width (#384). A friend's Bot fallback look (`lookAvoidingLocals`) steps its seeded shape (then color) when it would match one of your own Bots exactly; a named ink and its published hex count as one color. `indexRoster` registers the own Bots (`setLocalBots`) and `BotMark` applies the avoidance to every remote Bot, so the sidebar, header, palette, details drawer and transcript agree; a published image/emoji/shape is left alone. A face-room row stays on one line with an ellipsis, and every room row has its full title as hover text (#304). Past/ended face rooms stay out of this list (`isPastFaceRoom`) so the social section can fold them under **Past rooms**. With `@flybot/tree` on, only own Bots are tree nodes under Main; a friend's Bot stays in the flat ordered list (not hidden when Main is collapsed).
- `TeamSidebar` lists local Bots, listed remote chats (`isListedRemoteChat`), live face-to-face rooms, and shared rooms in one `bt-list`. Own chats sort before foreign ones (a remote Bot, a face room, or `sharedFrom`); pinned and recency still win. A friend's Bot keeps the globe `RemoteMark`; a live face room gets a small people `FaceMark` (`bt-row-remote bt-row-face`). No row wraps its title (#384): a friend's Bot row shows the Bot's name (`bt-row-title`), then its owner (`bt-row-owner`, muted, shrinks first) and the globe (at `--bt-ink-2`, readable) on one line; its second line is the preview like an own row, or *Friend's Bot* before the first message, and the full `remoteBotTitle` ("Helper (Bob)") is its hover text and label. The row wrap's gear is absolutely positioned so the hover/selected band stays full width (#384). A friend's Bot fallback look (`lookAvoidingLocals`) steps its seeded shape (then color) when it would match one of your own Bots exactly; a named ink and its published hex count as one color. `indexRoster` registers the own Bots (`setLocalBots`) and `BotMark` applies the avoidance to every remote Bot, so the sidebar, header, palette, details drawer and transcript agree; a published image/emoji/shape is left alone. A face-room row stays on one line with an ellipsis, and every room row has its full title as hover text (#304). Past/ended face rooms stay out of this list (`isPastFaceRoom`) so the social section can fold them under **Past rooms**. With `@flybot/tree` on, only own Bots are tree nodes under Main; a friend's Bot stays in the flat ordered list (not hidden when Main is collapsed). The structure entry may set `include(botId) === false` to omit a Bot from the default list (tree-archived and leftover `归档-*` / `Archive-*` names, #452); those Bots stay findable in Search when not also `bot.hidden`.

### Approval wait badge on chat rows

- A Bot row uses the same gold wait dot as a room row when `bot.pendingCount > 0` (Bot Social holds on that friend's chat) or a local hold is open. Shared-room overview pending lines stay one line with ellipsis (`bt-shared-wait`).

### Bot tree chevron

- The sidebar Bot-list tree (`sidebar.js` `TreeNode`, from slot `flybot.sidebar.structure`) draws collapse/expand as `.bt-tree-toggle`: a 28×28 hit target with a 16px `.bt-tree-chev` glyph, hover background, and a `:focus-visible` accent ring. The glyph rotates when `aria-expanded` is false; the button stays upright so hover and focus stay square. Expand/Collapse `{name}` labels and `aria-expanded` are unchanged. Leaf rows use the same 28px width as a spacer (`.bt-tree-leaf`) so names stay aligned; indent is still `depth * 16px` and the Bot row keeps its height.
- The sidebar Bot-list tree (`sidebar.js` `TreeNode`, from slot `flybot.sidebar.structure`; optional `include(botId)` filters the default list, #452) draws collapse/expand as `.bt-tree-toggle`: a 28×28 hit target with a 16px `.bt-tree-chev` glyph, hover background, and a `:focus-visible` accent ring. The glyph rotates when `aria-expanded` is false; the button stays upright so hover and focus stay square. Expand/Collapse `{name}` labels and `aria-expanded` are unchanged. Leaf rows use the same 28px width as a spacer (`.bt-tree-leaf`) so names stay aligned; indent is still `depth * 16px` and the Bot row keeps its height.

### New-chat menu

Expand Down
6 changes: 3 additions & 3 deletions docs/subsystems/plugin-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ FlyBot's plugin API surface on top of the patched core: host hooks, client slots

| File | Role |
| --- | --- |
| `core/src/host/hooks.js` | Host hook points: send middleware, transports, grants, the owner-bound held-send approvals (#52), and task hold/decide/delete seams (#54). |
| `core/src/host/hooks.js` | Host hook points: send middleware, transports, grants, the owner-bound held-send approvals (#52), task hold/decide/delete seams (#54), and `bot.update({ name?, hidden? })` for plugin rename/hide (#452). |
| `core/src/host/flybot-feed.js` | Activity feed: folds a Bot Session's `session/event`s into `flybot.on('turn.start' \| 'turn.end' \| 'tool.start' \| 'tool.end' \| 'exchange.delivered')` (#241). |
| `core/src/client/flybot-slots.js` | Client slots, including the approval card queue (`flybot.approvals`, `flybot/decide`). |
| `core/src/client/flybot-slots.js` | Client slots, including the approval card queue (`flybot.approvals`, `flybot/decide`) and `flybot.sidebar.structure` (`parentOf`, optional `include`, `menuItems`, `nodeActions`). |
| `core/src/client/flybot-registry.js` | Registry. |
| `core/types/flybot.d.ts` | Types. |
| `core/types/flybot.d.ts` | Types (`BotView.hidden`, `bot.update`, …). |

## Entry points

Expand Down
Loading
Loading