| subsystem | core-client | ||
|---|---|---|---|
| owned_paths |
|
The browser side of the patched ds-bot core (core/): sources in core/src/client/, built into the committed bundle core/client.js. Split out of ds-bot-core so a host-only change does not have to edit this page. client.js is generated: never hand-edit it, run npm run build in core/ and commit both.
- Browser client: sidebar, chat, panels, themes, characters, Bot browser panel (
core/client.jsgenerated fromcore/src/client/). The sidebar + new-chat picker (core/src/client/new-chat.js) takes extra rows from slotflybot.new-chat.action(after New group chat;label/hintgo through the samet()map) and lists onlylistedChatBots(gone, hidden, and home-level remotes stay out; owner labels match the sidebar). Covered bycore/test/new-chat.test.jsin patch0003-flybot-api-client.
- The server side,
core/index.js,core/src/host/,core/src/shared/→ core-host. - Plugin slots and the browser
flybotservice (flybot-slots.js,flybot-registry.js) → plugin-runtime. - Pin, patches,
core/dev/→ ds-bot-core.
| File | Role |
|---|---|
core/src/client/ |
Client sources → build to core/client.js. |
core/src/client/new-chat.js |
Sidebar + picker. Plugin rows via flybot.new-chat.action (newChatActionRows in flybot-registry.js); social adds Face-to-face room and Add friend (copy actions live inside Add friend, #181). The DSH Agent row reads Add coding agent (hint "A plain DSH session, not a Bot") so it is not mistaken for Create new Bot (#181). The short + menu does not list individual Bots. A group the host refuses because one of my Bots is not shared with the friend whose Bot is in it (error.details.reason === 'ungranted-local', put on the error by client/api.js) is drawn as a card, not a red line: it names the Bot(s) and friend(s), and offers Copy invite link for {Bot} (__flybot__.social.createInvite; shown only when social is loaded and can invite that Bot, nothing is granted by the click; it says copied only when copiedToClipboard in shared/remote-label.js reports the write took, else it shows the link in a read-only, select-on-focus field to copy by hand) and Leave {Bot} out (ungrantedLocalFix, shared/remote-label.js). |
core/src/client/stats-dock.js |
The shell's run-stats footer under the composer (conversation.composer.dock) re-registered one priority lower with the shell's own component, inject and chat locale, so the English counts read 1 turn / 1 step instead of 1 turns 1 steps (pluralStats rewrites only the exact English template, #419). The shell ids differ by version — one stats footer in 0.2.0-rc.2, the counts split into an activity pill next to usage in 0.2.1-alpha.1 — so statsDocks registers one wrapper per id and the wrapper whose id the shell does not publish draws nothing (#659); usage stays hidden. Still hidden in a friend's Bot chat by CSS (#419). Tests: core/test/stats-dock.test.js. |
core/src/client/room-invite-preview.js |
The one room-invite preview card (#876). invitePreviewOf says only what a server reply can honestly say (a name cleaned at fixed point, the lead, n / m in the room, the deadline), inviteCountdown words the deadline, RoomInvitePreview draws the card from a caller-supplied h; kind renames the data-flybot hooks, so the face-room paste flow (face-link-preview) and the member-invite dialog (room-invite-preview) render the same tree instead of two copies of the markup. Registered on __flybot__ in flybot-slots.js, because a classic-script plugin cannot import it. Tests: core/test/room-invite-preview.test.js. |
core/src/client/face-at-filter.js |
A face room's @ menu without DSH's Sessions rows (#305): planFaceMenu (rows to hide, where the highlight goes, when to dismiss), faceMenuCss, watchFaceMenu. Tests: core/test/face-at-filter.test.js. |
core/test/new-chat-menu.test.js |
+ menu rows (#283): an action row's description is its own line under the title, an inline Bot hint is capped, so hovering never hides a title. |
core/test/new-chat.test.js |
Picker filters and the + short action menu (data-menu; New chat, New Bot, New group chat, plugin actions, then Add coding agent; #385 marks, verbs and hints). ungrantedLocalFix, copiedToClipboard (no clipboard or a refused write is a failure) and the refusal card (invite and leave-out actions, the copy-by-hand link field, no grant written by the client). |
core/src/client/pane-width.js |
The side panel's width policy (#705, #462): DEFAULT_WIDTH / DETAILS_WIDTH / DETAILS_MAX / MIN_WIDTH for the Bot's browser, MIN_CHAT for what the conversation keeps beside the panel, and panelWidthRange(view, viewport) — a narrow window shrinks the Details panel down to MIN_DETAILS (240) before it starves the conversation, so the row always fits and no pane is pushed past the window edge. Pure numbers, so core/test/narrow-pane.test.js needs no React. |
core/src/client/panes.js |
PANE_MIN_WIDTH_CSS (#705): every pane the multi-pane row flexes gets min-width:0, the conversation column is matched by its DSH class fragment, and the panel's long values (About text, 4.1M tok · Cache hit …) wrap with overflow-wrap:anywhere, so a value that cannot wrap never widens the panel past the space reserved for it. |
core/src/shared/approval-waiting.js |
The one rule for "this hold still waits for the owner's decision" (#734, #754): isApprovalWaiting(status) is true for pending and pending_approval and false for every settled spelling (approved, auto, failed, rejected, stale, missing). The host writes pending; Bot Social's own outbound hold arrives as pending_approval. Host and client both import this one file — the held-reply card, the approvals overlay and the chat bubble on this side, hooks.js (decide(), heldOf(), the cap, the in-flight filter, the cleanup) and channel.js (a row's wait badge) on the other — and the overlay asks the host for [...WAITING_STATUSES, 'failed'], so the two spellings cannot drift apart again on one side only. Tests: core/test/approval-waiting.test.js, core/test/host-approval-status.test.js. |
core/src/client/schedules.js |
Settings → Scheduled tasks, a Bot's card and the details line. Each row has Edit (pencil; the row turns into the same form that creates a task, filled in, with Save changes; an edited task keeps its time zone, a cron rule it cannot show is kept) and Pause / Resume; a paused row reads Paused (#354). A row with a recorded run adds one line under it: "Running since …", "Last run …", "Last run … · Stopped" or, in red, "Last run … · Failed: "; a failed run of a task that is not paused gets Retry, which runs the task once now (actions.scheduleRetry, #473). With Automation tasks off, the hint reads "Switch on Automation tasks on the Plugins page." and the button Open Plugins closes the Settings dialog and opens DSH's Plugins panel (actions.selectPanel('plugins')); the Harness settings dialog has no such switch (#378). Tests: core/test/schedules-client.test.js. |
core/src/client/inbox.js |
Agent inbox, step 1 (#349): the Inbox row at the top of the sidebar (InboxRow, count badge = cards in Needs you) and the inbox dialog (InboxDialog, ui.inbox, actions.openInbox / closeInbox) with the groups Needs you, In progress and Done (inboxModel); failed turns, a Latest line and a per-card N tool calls count (#471); a group's open question card (#552). Tests: core/test/inbox.test.js. |
core/src/client/standing-approvals.js |
Standing tool approvals (#540): StandingApprovalsCard, the Always allowed card in Bot settings (the Bot's grants) and Group settings (the grants this person gave in the group), one row per kind with Remove (actions.removeStandingApproval). The approval cards (member-status.js ApprovalCard, the inbox card) add Always allow for this Bot and, for an ask from a group, Always allow for this group (alwaysChoices; never on a friend's Bot). Host side in core-host. Tests: core/test/member-status.test.js, core/test/standing-approvals.test.js. |
core/src/client/mobile.js |
Phone-width layout, step 1 (#362): MOBILE_CSS under @media (max-width:640px) for held-reply and floating approval buttons, the chat pointers and Agent inbox cards. Tests: core/test/mobile-layout.test.js. |
Add a new invariant under the heading it belongs to (or a new heading), not at the end of the page: parallel PRs then do not edit the same lines.
client.jsmatchescore/src/client/;npm run build+--check(also invariant 3 of ds-bot-core).- The bundle carries the slot wrapper's empty rule:
.bt-flybot-section:empty { margin-top: 0; border-top: 0; padding-top: 0; }sits beside the base.bt-flybot-sectionrule inFLYBOT_CSS(flybot-slots.js), so aflybot.sidebar.sectionentry whose component rendersnullleaves no separator behind while a section with rows keeps the base rule (#818). Behavior is specified on plugin-runtime.
- The committed bundle includes
flybot-slots.jsregistry(), which exposesBotAvatar/BotMark/lookOfonglobalThis.__flybot__for classic-script plugins. Behavior is specified on plugin-runtime.
- 新建群聊 (
new-chat.js) picks a local Bot as admin; a friend's remote chat is split intoparticipants: [{ kind: 'bot', remote: true, handle }](splitRoomPicks). The host also converts remote chat ids passed as members, so the friend's owner gets the inbound-share projection instead of a silent local-only member. If the host refuses the group because one of my Bots is not shared with that friend, the screen keeps the picks and shows the refusal card with the invite / leave-out actions (see thenew-chat.jsrow).
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.
- Settings → Secrets: each key's row shows All my Bots, the chosen Bots' faces, or No Bot; opening it shows
ScopeEditor(secret-card.js) with three radios, All my Bots / Chosen Bots / No Bot, and Save access sendssecret-updatewith'all', the picked ids or[]. A new key and a connector key start as All my Bots (host rule in core-host).
A flybot.session.dock component also gets the host translator t, so a plugin's card can read in the interface language (the social hold card's Held for your approval: contains … line, #371; its words are in i18n.js).
The overlay (flybot-slots.js FlyBotApprovals) is the fallback, not a second copy (#260): an approval whose policy a flybot.session.dock entry declares (policy, e.g. social) is drawn by that dock in its chat, which reports the ids it shows through claim(ids); the overlay skips claimed ids. A docked approval no open chat shows becomes a small pointer (Sender → Recipient, Waiting for approval, Review opens the chat via actions.openSession) after 1.2 s, so moving between chats never flashes it; the chat that is open never gets a pointer for its own approvals (each FlyBotSessionDock reports its chat through chatOpened, #271). An approval's chat is its room, else the chat whose turn a held reply answers (meta.replyIn, #418), else the friend's chat it is addressed to, else the Bot that wrote it; the pointer's Review opens that one (#278). The claims live in approval-claims.js so a chat cell can claim without the slot components. Any other policy's approval is a card in the overlay, with the same two lines as the dock's card (Waiting for approval / Not sent, then Sender → Recipient; the policy's long reason is the tooltip) and Approve / Reject (Retry for a failed send). Approve / Reject is drawn only for a status isApprovalWaiting() calls waiting (pending, pending_approval), the same rule the held-reply card asks (#734); a settled status gets no button the host would refuse, and a failed send keeps Retry. The recipient is the person who gets it (#418, #498): the host's recipientName, else the owner of the friend's chat (friendBotOwner), else as the host named it, so a card reads Chief → SelfB1233, never two Bot names. senderName / targetName are resolved server-side and a delegated marker follows a relayed send (hop > 0). The decision itself is owner-bound in the host (#52); see core-host.
- A local group's Members · N card (
group-settings.js) ends with the plugin rows of slotflybot.room.members(FlyBotRoomMembersinflybot-slots.js), after the note on who leads the group (#892) and on the card's own row padding (.bt-flybot-room-members), never in a face or shared (inbound-share) room; the rows gethost, the core endpoint call (also__flybot__.host), forroom-invite/room-invite-mint/room-invite-revoke(#876, the social plugin's Invite by link). Covered bycore/test/room-members-slot.test.js. Plugin copy that core's dictionary carries is translated through__flybot__.t(#892: Invite by link, the Join-or-create dialog), so a zh UI reads it in Chinese. A room a link opened is titled by its own name on the joining home (roomTitleinremote-label.js: sidebar, header, drawer, brand); "Shared room from …" stays as the sidebar row's hover and the Shared tag's title (#892). In a room projected from another home the Members rows carry what that side could confirm (#904): a Bot learned from the host's roster reads Unconfirmed until this home holds its own fact about it, one with no Bot Social identity reads Not connected, and the remove × is not offered there — the host's membership is changed on the host's side. - The face room's details (
FaceMembersindetails-drawer.js) open withFaceLead(group-settings.js, #476): the current lead asBot (person), Default until the creator picked one. Only whenroom.face.lead.canPick(this home made the room and Bot Social stores a lead) does it show Change, a Make lead button per Bot of every home and Use the default lead; each callssocial/face-lead. Everyone else sees it read-only.FacePersonRowmarks the lead's Bot with a Lead tag (isLeadSeat, by handle). Never a chat command.
RoomAvatar(src/client/mark.jsroomAvatarPeople) seats current localroom.membersand live remoteparticipants(handle-seeded vialookBotForRemote). A face-to-face room with only remotes no longer falls back to the grey placeholder in the sidebar row or chat header.
- Group
RemoteMessage(cells.js) showsremoteMessageText(reply.text), the same unwrap the 1:1 friend chat already uses. Stored room lines stay fenced for the model; the bubble shows the inner words, not the<untrusted-message>wrapper (#98). - A Bot line that Bot Social holds for approval in a Face-to-face room is drawn once, in its approval card (#306):
GroupMessage(cells.js) asksisHeldLine(room, text, bot), which matches the line to aface_room_messageentry ofroom.pendingApprovalssent by that same Bot (the card'sfromequals the bubble's Bot name, ignoring case and spacing; a card with nofrommatches by text alone, and another Bot's identical words stay a full bubble) (whitespace-collapsed, without aTo the room:lead, allowing for the 800-character cap), and then draws nothing in the room. #456 makes the room's own record the first source:heldLineState(room, { seq, botId, text })(cells.js) readsroom.heldLines(by the line'sseq, whichGroupRepliespasses from the round reply; else by Bot and exact text) —sending/pendingdraws nothing (the card is the line's only place, greyed),rejecteddraws one grey event line<Bot> · Not sent(.bt-event.bt-not-sent,data-flybot=held-not-sent,{name} · 未发出) with the Bot's mark, also after a reload and in Past rooms; a line still waiting when the room ended reads Not sent too; asendingrecord older than a minute (a restart mid-send) falls back to the bubble. Approve removes the record, so the line becomes a normal bubble. Other room kinds, lines the owner typed (approved at once, never listed) and every other Bot line are unchanged.
- A friend's remote Bot is stored and delivered by its handle (
@<bot>.<home>— what the @ menu inserts andsendUserMentionsmatches). Where the user reads it — mention chips (src/client/user-message.js) and sidebar previews (nameMentionsinsrc/client/text.js) — the client rewrites the handle to the display name taken from the local roster (roster.bots[].remote,roster.remoteBots), never from message text, so a remote-controlled name reaches the UI only through the sanitized roster records (#71; display names, #9). - In a face-to-face room the bubble matches a mention by every alias the room host routes on (
faceAliasesinshared/remote-label.js, the same listface-mentions.jsuses), so@SelfA's Chieftyped on B is A's own Chief on A. The chip shows@and the seat label (faceSeatLabel,SelfA · Chief) on both homes, a bare name means the writer's own Bot (my line tries my seats first, a friend's line the friend's, whatever order the room lists its seats in, asfaceCandidatesdoes), a friend's Bot outside the room is not matched, and one space typed after a picked@Name(the menu inserts its own) is dropped from the drawing (#305). - A remote Bot's generated look is seeded by its handle (
remoteSeedinsrc/client/characters.js), never by its session id. When Bot Social sendsavatar/color/avatar_updated_at,flybot.remote.publishstores them on the remote-chat record (cached byavatar_updated_at).lookBotForRemoteoverlays a person payload's look (face-roommembers[]is the source for room avatars). A storedcolor/avatarstill wins over the seed; missing look stays handle-seeded (#137). - A friend's 1:1 chat (#443):
AssistantCellgives the reply bubbles abt-leadBotMarkfrom the chat record (friendLead; none on aNot sent:note). The activity row (activity-row.js) stays up whileawaitingReply(awaiting.js,roster.remoteAwaiting, 10 min cap) holds. Between the send turn and the echo turn it sits in the last turn's tail, with a 1.5 s grace anddata-carryso there is no second entrance, which keeps one row from send to reply. The socialPeerThinkingline is for face rooms only. A friend chat shows the row for a run that started from my submission and, between runs, whileawaitingReplyholds. The friend's reply run (the echo, which has no submission) hides it at once, and only the end of my send run gets the grace. The turn-tail hiding rules instyles.jsspare.bt-typingand its tail item, or the row between runs would be drawn withdisplay:none. - A friend's Bot the owner hid, a remote chat marked
gone/hidden, and a home-level handle (@friend.home, not@bot.friend.home) must not reappear in the sidebar, the 发给 / 和谁聊… picker,@mentions, orlist_botsfriends lines (isListedRemoteChat).flybot.remote.publishskips those handles so remote-chat sync marks the persisted session gone/hidden instead of opening it again; history is kept. Handle comparison is case-insensitive (normRemoteHandle). - The "@" reader is shared with the host (
core/src/shared/mentions.js, bundled intocore/client.js). #287 addedunmatchedMentionsthere (the@words that name nobody, used by the host for the face-room hint);mentionSpansand what the bubbles draw are unchanged. - A friend's Bot settings surface is not the local editor (#160): no rename, avatar editor, model, folder, instructions, Main Bot, Hide, Duplicate, or Delete. The details gear and context menu say Bot settings. The menu is Mark as Unread, Bot settings, and Copy conversation ID. Per-friend Auto-send and Remove this Bot stay on that page (
flybot.bot.details); Remove closes the overlay so a stale local editor cannot appear. The Bots settings list is local Bots only. - A friend's Bot details overview is that compact panel: handle plus
FlyBotDetails(Auto-send and a muted Remove link), no Browser tab, no Usage / model / folder. Opening the gear usesremote-bot-settings(RemoteBotSettingsSheet): a bottom sheet on narrow viewports and a centered card from 520px up, with the scrim behind the dialog (z-indexstacking likebt-settings/bt-mem) so the sheet stays clickable through enter/leave animations. The surrounding DSH settings-shell chrome (back/title) is not redesigned (#166). The sheet takes focus when it opens (tabIndex -1,preventScroll), closes on Escape from its ownonKeyDownas well asuseEscape, and gives focus back without scrolling; in the tree a friend's.bt-row-wrap(row plus gear) shrinks like a plain row (.bt-tree-item>.bt-row-wrap), so the gear never overflows the sidebar and focusing it cannot scroll it sideways (#274). On every wide row (own Bots too) the gear's focus and click runkeepSidebarStill, which puts back any sideways scroll a focus left on the row's ancestors or the page, now and after the settings dialog opened (#421). The Settings nav's last entry reads Harness settings ↗ with a tooltip saying it opens DeepSeek Harness's own settings in a separate window; FlyBot does not embed or retitle that dialog (#421). - An own Bot's settings (
BotSettingsForm) draw theflybot.bot.detailsentries once, as a titled Sharing card right after Instructions (FlyBotDetailswithwrap→Card), so sharing is not at the end of the page; with no entry there is no card. Bot settings and group settings both end with a More card (Duplicate / Delete). The composer card's:focus-withinborder is 55% ink, hover stays 30% (#406). Keyboard Tab order, checked in headless Chrome on a mock bench (#406): sidebar (Search, New…, the Bots, Connect apps, Account) → chat header pill → message box → Add, Dictate, voice → the turn stats; theshell.overlaylayer (approval cards) comes after the main column in the DOM, and no stop focuses an invisible element. The message box comes before + because DSH's DOM puts the input first and FlyBot's CSS only reorders it visually. - Every listed Bot row in wide sidebar mode is a
bt-row-wrap: the sessionbt-rowand a sibling gear (bt-row-settings, hover/focus) that opens Edit Bot for an own Bot or Bot settings for a friend's (#166, #276). - The Session header is the shell's own: FlyBot registers the Bot pill in
conversation.session.header.utilitiesrather than claiming the singleconversation.session.headerslot, so the seats that header renders — the lineage title andconversation.session.header.actions, where@deepseek-ai/ui-subagentputs the subagent catalog — keep rendering (#803). The floating header style FlyBot used to draw over the Session header is gone. - What the pill does to the page is not in the pill and not in a seat of its own (#803). The shell draws
.utilities— and the pill with it — only while its chrome shows, which a Session without a first message hides, so a side effect parked in the pill stops there: the friend's empty-chat hint,body[data-bt-friend-chat], theMessage {name}composer label, the visit ledger,body[data-bt-room]and the face-room@filter.installHeaderEffects(src/client/header-effects.js) is installed once fromapplyand keeps them on the Session in the main view. It carries no React, and each effect has its own dependency key: the sessions list answers before the roster, so a single "did the Session change" guard would drop a friend's flags on the first pass, whilebeginVisit/endVisitkeyed on more than the Session would open a second visit for a chat that was only just opened.test/header-effects.test.jsdrives those transitions on jsdom;test/header-slot.test.jspins that no such effect runs inHeaderPilland that FlyBot claims no other header seat. conversation.session.header.corneris not available as that seat: it issingleandui-sidebar-rightalready registers its right-sidebar button there with nopriority(0), andSlotCore.registerthrows on a second registration in the same cell (single slot "…" already has a registration at priority 0), which would fail FlyBot's whole client plugin fromapply. Only--bt-first-sepstill rides the pill, because it needs a turn and so always has a header to be drawn in.- The localized Message {name} label on the composer is one of those effects, not something the pill holds:
installHeaderEffectsstartsholdComposerLabel(src/client/composer-label.js) for the Session in the main view, keyedsessionId|name, so the label survives a Session whose header is not drawn. It sets the placeholder CSS var and writes the label on every[data-composer-input]'saria-labelanddata-placeholder; cleanup hands each input the shell's latest own value back when the Session changes or FlyBot stops (#166, #803). The shell picks its copy by phase before it knows whose chat it is (the new-session hero copy on a first open, its coding copy after a send), so aMutationObserver(those two attributes and child changes) rewrites them in its own callback, before the next paint, while the chat is open (#419, #499);test/composer-label.test.jsdrives it on a jsdom composer. The same installer setsbody[data-bt-friend-chat]in a friend's Bot chat, and CSS hides the shell's run-stats footer ([data-composer-stats]) there: the friend's Bot runs on its owner's home, so local turns/steps/tok/s are relay turns and always 0 (#419).ContextCellhidesTool added/Tool removedrows for the roster'spluginTools(set inindexRosterviasetPluginTools,text.js) as it does for connector tools (#419). The poll re-readspluginToolsfrom everystateanswer (a plugin reload makes no revision), andContextCell(injected withface) selects them from the roster so a row already drawn re-renders when they arrive.
- A face-to-face own message uses
data-heldon.bt-umsgwhile that room'spendingCount > 0: dashed, mixed user-surface,--bt-ink(not the finished sent user bubble). Incoming lines are unchanged. Face-to-faceGroupOverviewhides the Usage row.
- User-visible chrome says FlyBot (header mark + name, About, settings title, tab title, favicon, zh copy). The mark is the existing FlyBot logo (
docs/images/logo.svg), inlined asFlyBotMarkfromsrc/client/brand.js. Update popups say FlyBot core {version}. The npm package remainsds-bot; plugin id, localStorage keys, andconsole.warn('[ds-bot]')stay upstream. - The tab title follows the open chat (
sessionTabLabelinbrand.js: a friend's Bot readsName (owner), #384, a room uses its display name; an ended face-to-face room, closed or left, readsFace-to-face · closedunless the user named it, #250).installTabTitlere-reads it on session and roster changes, so a face room the joiner opens before the roster names it, or a room renamed later, still gets its own title (#173). Sidebar row gear controls are hover/focus-only, and Bot settings call the duplicate/delete card More. An empty chat with a friend's Bot shows a one-line hint (--bt-remote-empty, set byinstallHeaderEffects) that the friend approves unless Auto-send is on (#173). The agent entry on + reads Add Bot. - In
apply, anyctx.effectthat readsrostermust come afterconst roster = createSource(...): effects run immediately, and a read before the declaration throws and fails the whole client plugin load (core/test/client-apply-order.test.js).
TeamSidebarlists local Bots, listed remote chats (isListedRemoteChat), live face-to-face rooms, and shared rooms in onebt-list. Own chats sort before foreign ones (a remote Bot, a face room, orsharedFrom); pinned and recency still win. A friend's Bot keeps the globeRemoteMark; a live face room gets a small peopleFaceMark(bt-row-remote bt-row-face). No row wraps its title (#384): a friend's Bot row shows its one name,friendBotTitlefromshared/remote-label.js("Helper (Bob)", the same string as its header, tab, composer placeholder and @ menu, #498), inbt-row-title, then 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 name 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.indexRosterregisters the own Bots (setLocalBots) andBotMarkapplies 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). The face-room row leads with its number (faceRowTitleintext.js:Face-to-face · A, B · 482915reads482915 · A, B), so the cut never hides it; the hover text and label keep the full title (#458). A group row whose group holds an open question card (#550) says it waits for me:Waiting for your answerin its label and the waiting dot (bt-unread-wait), even when no approval waits there; the asking Bot's own row stays out of it because the card lives in the group (#552). A Bot row's group-work line (<group> · Thinking…) skips face rooms (awayGroupsintext.js): a face room has its own row, so its turns never take over the Bot's preview (#458). Past/ended face rooms stay out of this list (isPastFaceRoom) so the social section can fold them under Past rooms. With@flybot/treeon, 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 setinclude(botId) === falseto omit a Bot from the default list (tree-archived and leftover归档-*/Archive-*names, #452); those Bots stay findable in Search when not alsobot.hidden.
- One Inbox row sits at the top of the sidebar, above the Bot list (wide and rail), with a count badge of the Needs you cards; it opens the inbox dialog over the conversation (owner decision 2026-10-20, option a). No new + menu entry, no new sidebar section.
- Groups, in this order and with these words (the decision's "Need you" is written Needs you, as in the triage): Needs you, In progress, Done; each has a one-line empty message. Only data the client already has, no new host state:
work.approvals(tool approvals),pendingCounton chats (held Bot messages),work.bots/work.rooms(running turns), the task records ofsrc/host/tasks.js(actions.tasks→ the existingtasksendpoint, polled every 30 s) and the Session list'supdatedAtand last line for finished Bot chats. - Only my own Bots (not a friend's Bot, not another member's): a friend's chat appears only as a held message of mine waiting in it. Every card carries
assigner,ownerandexecutingBot(the first two are me in step 1) so team views can filter later. - Needs you: tool approvals, held messages, tasks that are blocked, have a result ready or failed (with the reason), and failed turns (#471,
kind: 'turn'): fromwork.ended, the same ended-turn entries the system notifications read (#353), a place's (a Bot's own chat or one group) latest ended turn when it failed, in the last 7 days, while that Bot is not running or waiting for an approval there; its title is the chat's last line (The last turn did not finish without the Session list), its reason the failure. A later finished turn there clears it, and that Bot chat is then not also in Done. Open only. In progress: running turns (with elapsed time; a turn waiting for an approval shows only in Needs you) and open/claimed/working tasks. Done: accepted or cancelled tasks and Bot chats whose last turn finished. Done and failed tasks age out after 7 days; Done shows the newest 20. - A group's open question card is a Needs you card too (#552):
roster.questions[<room id>]— a card a Bot asked about work handed out from that group (#550) — has the status Needs your answer (input, as a blocked task's question does), the question as its title, its detail as the reason, and in as its place; Open opens that group, where the card sits in the composer dock and takes the answer, so the inbox adds no second answer path. A card in a Bot's own chat, one for a removed group, and one from another member's Bot add none. - Each card has exactly one status, as a colored pill with an icon and a word (never color alone), the Bot, where and when, the title on one line with the full text on hover, and Open (closes the inbox, opens the chat). An approval the group chat's card can answer (
answerable) also has Reject / Allow once, which callactions.answerApproval, the same answer as that card (no second approval path); the card leaves and a status line says what was done; an error stays on the card. No native confirm dialog. A held Bot message, an approval DSH's card answers in the Bot's own chat, and every task have Open only: Bot-sent messages are approved in their chat. Every action is a button, so the dialog is keyboard reachable; the close button takes focus and Escape closes. - Latest activity (#471): every card may have one more line, Latest: … (
card.activity,bt-inbox-activity, full text on hover): a task's newest note, else what its Bot runs there now (only for running turns and tasks), else the chat's last line (previewOf); left out when it only repeats the title or the reason. - Tool-call count (#471): a card whose place used at least one tool shows one muted line,
N tool calls(card.calls,bt-inbox-calls): the count of the turn running in that place now, else of the newest ended turn there, from the work snapshot (work.bots[id].calls,work.rooms[roomId][].calls,work.ended[].calls, core-host). One place only, never added across places; a count of 0 or unknown draws no line. Nothing new is polled and the card stays read-only. i18n key{count} tool calls. - Out of scope so far (#471): the three-column board with a List / Board toggle (v2), a first-run guide, team views (#51/#149/#55/#57).
- Below 640px (
MOBILE_MAX_WIDTH,core/src/client/mobile.js,MOBILE_CSS, installed last in the shell group so it wins at equal specificity) a ~400px phone browser can skim a chat, press Approve / Reject on a held reply and work the Agent inbox. Wider screens are unchanged: every rule sits in that one media query. - Held-reply and floating approval cards: Reject / Approve share the card width as 40px buttons, Approve filled with the accent; the held card uses the message's full width. The floating approval stack and the chat pointers span the screen beside the shell's 56px icon rail (DSH collapses the sidebar below 1024px), so the top + and the account stay reachable.
- Inbox cards put their buttons on a row under the text (36px), and the title and reason wrap to two lines instead of one truncated line. The inbox dialog already fills the screen at this width (
.bt-mem). - Out of step 1 (owner decision 2026-10-20): PWA install and push (after #352), native apps. A notification will only ever open the card; approval stays on the in-app buttons.
- 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).
approval-notices.jsnoticeApprovalsraises one system notification per event while the window is in the background or shows another chat: an approval that starts waiting ({name} is waiting for your approval), and a turn of one of the user's own Bots that finished ({name} finished) or failed ({name} failed: <reason>), fromwork.ended(turns.jsendedView: the last 20, process-local; no friend's Bot, no turn a friend started, no hidden turn another Bot asked for, no face-to-face or inbound-share room, no turn the user stopped). Only when the platform already allows notifications (never asks), once per event, nothing from before the window opened; a click focuses the window and opens the chat or group, and a notification never approves, rejects or sends. Settings → General → Notifications (prefs.notifications, on unlessfalse) switches every kind off; events seen while off stay told.- Waiting too long (#469): an approval still waiting
WAITING_TOO_LONG(15 min) after it started, or after the window opened if it started earlier, gets one more notification ({name} has waited {n} min for your approval), from a timer set for the next due approval and cleared with the effect. - Per-kind switches (#469): under the main switch, Waiting for approval (covers the reminder), Finished and Failed (
prefs.notify.<approval|finished|failed>, on unlessfalse). - One per chat (#469): every notification of a chat or group shares the
tagds-bot-<chat or room id>, so a newer one replaces the older; its body counts the updates ({count} updates, the reminder not counted) since the chat was last on screen or its notification clicked. Quiet hours, mute and an in-app list are later slices. Covered bycore/test/approval-notices.test.js.
- The sidebar Bot-list tree (
sidebar.jsTreeNode, from slotflybot.sidebar.structure; optionalinclude(botId)filters the default list, #452) draws collapse/expand as.bt-tree-toggle: a 28×28 hit target with a 16px.bt-tree-chevglyph, hover background, and a:focus-visibleaccent ring. The glyph rotates whenaria-expandedis false; the button stays upright so hover and focus stay square. Expand/Collapse{name}labels andaria-expandedare unchanged. Leaf rows use the same 28px width as a spacer (.bt-tree-leaf) so names stay aligned; indent is stilldepth * 16pxand the Bot row keeps its height. An own Bot's row, in the tree and in the flat list, names the model the Bot runs on in its hover text (botRowTitleintext.js:Coder · DeepSeek V4 Pro, the model's served name frommodelOf, also intext.jsso the sidebar does not import the model menu, #366); a friend's Bot row keeps itsremoteBotTitle, since it runs on its owner's model.
- The + menu is a popover anchored next to the sidebar + (#195, #173): its
.bt-panecarriesdata-menuand stays see-through, the menu is a 280px card at the top left with no title row, and a press on the pane outside the card (or Escape) closes it. New chat, New group chat, New Bot and plugin rows open their full pane as before. - The sidebar footer button (
ConnectAppsinsidebar.js, label Connect apps / 连接应用) opens Settings > Connectors throughactions.openConnectors(), the same call and the same words as the account menu's Connect apps row. The shell's plugin panel stays reachable in the Bot surface through the account menu's Plugins row (actions.selectPanel('plugins'), 插件), without switching to classic Agent mode. - One name per destination in the account menu and command palette (#392, #393, #394): Settings opens the Settings dialog (General), since Bot settings / Edit Bot open one Bot; Scheduled tasks opens Settings → Scheduled tasks, the name Settings and a Bot's details use (DSH's own bundle stays "Automation tasks" in the Plugins hints); and Switch to classic Agent mode is the only way to classic Agent mode, so Settings → General is Appearance only.
- Create Bot starts on the Main Bot's model (#230):
startModel()(bot-draft.js) picks the Main Bot's chosen model, else the one it last ran on, when it is served; thenroster.defaultModel, then the first served model. Only served models are picked, so a provider without a credential is never preselected. - A missing model credential has its fix under the error (#405):
TurnErrorCell(model-menu.js) shadows DSH'sturn-errorchat node (locale: 'chat', so title and the AUTH / QUOTA / account copy are DSH's own strings). When the failure says the route has no API key (missingModelKey, e.g.no API key for provider route "deepseek-official"), the row says the Bot's model has no API key yet (the raw provider text is not drawn, not even on hover, since a provider may echo part of a key) and, for adeepseek*route, offers the same chips as a Bot with no model (setupChoices: Sign in to DeepSeek where the shell can, Enter a DeepSeek API key, Open settings to set up a custom model); any other or unnamed route gets Manage models. Under the DeepSeek chips (here and in the no-model dock) a How to set a DeepSeek API key link (KeyHelpLink, new tab) opens docs/deepseek-api-key.md on GitHub: DSH's own key dialog keeps only a typed or pasted key (#451), so the guide also gives the credentials file andDEEPSEEK_API_KEYways. Every other failure reads as DSH writes it, with no buttons. - Models and providers…, Manage models and Open settings to set up a custom model open DSH's Settings on its Models page (#407), not General: the
settings.launcherseat'sopenSettings()takes no section, soopenHarnessModels()(sidebar.js) calls the seat'sopenOnboarding('flybot-models'). Thatsettings.onboardingstep (ModelsSettingsStep, order 1000, after DSH's own steps) is handedopenSection: opened on purpose (explicit) it callsopenSection('models'), thencomplete(); in first-run onboarding it only completes. With no seat it falls back toopenHarnessSettings(). FlyBot's own Harness settings entry still opens DSH settings as a whole. - Settings say each thing once (#230): the Model and Main Bot cards show no row label that repeats the card title (
SwitchRowshowLabel: falsekeeps the switch's accessible name), the Create Bot dialog has its Back / Close in the header only (the footer Back / Cancel is for the inline form), and the account menu is as wide as its longest item (width:max-content, capped to the viewport) instead of cuttingSwitch to classic Agent mode. - Slot
flybot.settings.page(0003-flybot-api-client, types inflybot.d.ts, #337) adds a page to the Settings list right after Connectors:{ id, order, label, icon?, component }.SettingsDialog(settings.js) listsuseSettingsPages()entries under the keyplugin:<id>and draws the component throughSettingsPageSlot(a failing component draws nothing, it does not close the dialog); a page whose plugin is gone opens General.labelis an English key for the hostt(); the component gets{ roster, actions, call, run, t, Section, Row }(the host translator, the Settings section card and row, so the page matches the others and reads zh fromi18n.js). Social registers Bot Social (the server, see social). Test:core/test/settings-page-slot.test.js. - Slot
flybot.account.header(0003-flybot-api-client) is a compact block at the top of the account menu (FlyBotAccountHeader); social uses it for the owner's own handle (#181). - Slot
flybot.new-chat.action(0003-flybot-api-client, types inflybot.d.ts) is a row on the sidebar + new-chat menu, after New group chat. Plugins register{ id, order, label, hint?, visible?, run }; the picker lookslabel/hintup witht()like the neighbouring rows. A string returned fromrunis shown as a toast (actions.notify). Social registers Face-to-face room and Add friend; see plugin-runtime and social. - Sidebar + (
mode === 'new') opens a short action menu (data-menu, no To:/composer) until New chat or New group chat. That menu does not list individual Bots (Chief and the rest stay behind New chat). The Agent row in that FlyBot menu is Add coding agent; every row has its own mark (ChatIcon,BotIcon,PeopleIcon, a plugin row'sicon, drawn throughSlotIconbehind its own error boundary with the people mark as fallback; a function or memo/forwardRef component,TerminalIcon), one verb style (New … / Add …) and one short hint with no closing period (#385: Message one of your Bots, Set up a Bot with its own role, Chat with several of your Bots, Meet people nearby with a 6-digit number, Paste an invite link or code, A coding session without a Bot). A Bot's right-click menu has Edit Bot and no separate Rename Bot (the name is edited there); Add DSH Agent stays the DSH product string on Agent rows elsewhere (#126, #181). - Group bubbles, notices, and failed-reply lines never show a raw
<untrusted-message>wrap or a developer error (llm-deepseek: no API key).remoteMessageTextunwraps stored fences;failureLabelmaps missing-key / unknown model failures to a short friendly line. Details stay in Settings → Feedback. GroupRepliesdraws auserreply withother: true(another member's face-room line, #180) asPersonMessage(cells.js): the person's name, the person mark (PersonMarkinmark.js) and a received--bt-bubblebubble, not a centredbt-eventnotice and not the sent--bt-userstyle; own lines keep the right-hand bubble (#229).othercomes only from the host's person-line post (hooks.jspostHuman), never from text. Another home member's line in a shared group (UserMessageCell, author stamped by the host) gets the same person mark and the received surface (.bt-umsg-other .bt-ubody).PersonMark({ name })draws the person's initial on a color picked from the name (personColor, the core default-color hash ofperson:<name>), so one person looks the same on every home and two people are told apart; without a name it is the plain silhouette (#182).- A person's line keeps its line breaks (#455):
.bt-person-msg .bt-bubble :is(p,li)iswhite-space: pre-line, as the sender's ownpre-wrapbubble, since Markdown would join single newlines. A trigger whose source hasshown: true(a line a running round already drew, host #455) draws nothing. Covered bycore/test/face-people.test.js. - A face-to-face room is person-first (#229, #457, owner decision 2026-10-20).
face-seats.js(re-exported bygroup-settings.js) builds it from participant fields and the roster, never message text:faceSeatslists the live Bot seats ({ left: true }adds the ones that left, markedleft),faceSeatLabelnames each Bot<Bot> (<person>), mine included (Chief (SelfB),Chief (SelfA); a Bot is never(you)),faceBotLabelgives a local Bot that name in a face room, andfacePeoplegroups the seats by person (me once, first; a friend by home) withfacePersonLabelputting(you)on me only. The overview and group settings Members · N count people and draw one row per person (FacePersonRow: person mark, name, then that person's Bots; my Bots open their chat). - In a face room a bubble names its speaker the same way (#457): my Bots' lines
Chief (SelfB)(cells.jsGroupMessageviafaceBotLabel), a friend's BotResearcher (SelfA)(remoteSpeaker), name only, with the handle as the tooltip instead of a globe and handle line. My own typed line is mine, not my Bot's: it carries my person mark and name (data-face-mine) and is never drawn held, so it does not change colour while a Bot's reply waits (user-message.js). A@Chief (SelfA)chip stays bound to that Bot after it leaves:faceMentionablesalso matches the left seats (after the live ones), so the name never falls through to my own Chief. - One name for a friend's Bot (#384, #305, owner decision 2026-10-19).
remoteBotTitle(shared/remote-label.js) is the only format: the Bot's name, then its owner in parentheses (ownerNameOf: the human owner,ownerName/owner, before the home's labelhomeName),Helper (Bob), and a friend's Chief the same,Chief (Bob)(no<owner>'s Chiefspecial case). For a friend's chat in the roster,friendBotTitle(bot)is that one call (#498), on both homes: the sidebar row (one string, no muted owner part), chat header, tab title, composer placeholder, row tooltip and the@rows (a friend's Bot by that name with its handle as the description; the inserted text is unchanged; a friend's Bot is offered once: in a group by its seat, in a Bot's chat by its remote member row, which inserts the handle, not also by its chat row); in a face roomfaceSeatLabel/at-menu.jsfaceLabelgive the same format (Chief (Bob), my own BotChief (SelfB)with my name, #457). The social plugin's classic script reads the same functions fromglobalThis.__flybot__.names(friendBotTitle,friendBotOwner, set inflybot-slots.jsregistry()). Approval cards name the receiving person instead (Chief → SelfB1233, #418).faceAliaseskeeps the older labels (Bob · Chief,Chief · Bob's Bot,Bob's Chief) as aliases, so a line typed before the change still calls the same Bot. DSH's own Sessions rows in a face room's@menu are filtered in FlyBot (face-at-filter.js, below) until DSH has an API for it (#422). - Group and face-room Bot labels (#526, owner decision 2026-10-20, option a). The same person-first format:
Reviewer (flybot682b8a43), never Bot Social's member label plus the handle (Reviewer · FlyBot 682b8a43 (flybot682b8a43)).remoteBotNamedrops a trailing· <owner>from a Bot's name when it is that Bot's owner (the server'slabelreaching a name slot, e.g. a bubble header with no seat yet);ownerNameOfpasses over an owner name made up from an install id (isAutoOwnerName:FlyBot+ hex id) to the owner's username from the home handle, and keeps it only when no handle is known;seatOwnerNamereads my own seat's owner the same way (face-seats.js,at-menu.js). The Bot names inFacePersonRowand the remote rows of the group Members lists (group-settings.js,details-drawer.js) carry the handle as a tooltip, as bubble headers already do. Covered bycore/test/group-bot-label.test.js. - A face-to-face room's subtitle (
roomSubtitle→faceRoomCountingroup-settings.js) names what it counts,1 person · 1 Bot/2 people · 4 Bots: Bots are the live Bot seats, people the distinct owners among them (me once, a friend by owner name); human seats and Bots that left do not count (#306). - The composer
@in a face-to-face room (mentionRows/atSourcesinat-menu.js) offers the room's own Bots, each once (a repeated remote seat is folded by handle; Bots that left are out), labelled as the Members panel names them (Chief (SelfA),Chief (SelfB)), plus Everyone with the room's Bot count (2 Bots, #457). Published friends' Bots (remoteMentionRows) and plugin rows (the socialFriends' Botssection) are not offered there, since only the Bots in the room can be reached in it. Only the label changes: the inserted text is still the@Namethe room routes on (@SelfA's Chief), never a handle (#305). - A face room's
@menu shows no DSH Sessions rows (#305, owner decision 2026-10-19: filter in FlyBot for now; #422 asks DSH for an API). DSH's built-inreferencesource cannot be dropped for one Session, soinstallHeaderEffects(header-effects.js) startsactions.filterFaceAtMenufor a face room, which watches that Session's trigger controller (inputTriggers.sessionOf, the same one the composer uses) withwatchFaceMenu(face-at-filter.js): a<style data-bt-face-at>hides thereferencerows whose icon issession(Sessions and Subagents) and their section title by MenuView's option ids,controller.hovermoves the highlight past them in the direction of the key (so arrow keys skip them and Enter never picks one), and a menu with nothing else left isdismiss()ed, so Enter sends. File rows stay. Elsewhere, Everyone's count is the Bots it reaches (local members and seated friends' Bots),1 member/2 members.
- Creating a Bot or a group chat opens nothing in the chat by itself: the picker's group path sends no greeting flag and the card sends no
brief, so a new chat has no turn, no greeting bubble and no first-request cue. A line already typed in the composer is the user's own request: Create new Bot closes the pane andafterCreate(bot-draft.js) opens the new Bot, then sends that line to it as its first turn, so the Bot answers it andcreatePayloadcarries no brief for the host to keep as instructions. The Bot exists by then, so a failed send still opens its chat and reports the failure as a notice instead of leaving the card, whose Create can only answer "that name already exists" (#698). A Bot's first turn is otherwise a line the user writes in its chat or a message another Bot sends it. The cells no longer draw a creation turn:TriggerCellskipsrole === 'kickoff'(a setup prompt, not a line the parent sent) exactly ashistoryEntrydoes (#698); host side in core-host.
Every Bot at work in a group shows in one place, the member dock above the composer (member-status.js MemberDock), never as a second row in the transcript: GroupReplies (cells.js) draws only the round's lines, and splitBusy puts the round's current speaker first among the rows. Each row (WorkRow) is the Bot's mark, Name · <phrase>: <step> (workText; without a step <phrase>…) and the elapsed time from one minute on; the summary line for approvals here and members past the rows (SummaryLine) is a row of the same shape and column, with the first worker's time, not a centered .bt-event. The step is work.bots[id].step from the host, used for a group row only when that view is about the same group and tool (stepIn). A tool with no phrase reads Using <tool> (workPhrase), never only "On it". Rows come from the polled work view, so they leave with the turn. The approval card's action row wraps (.bt-mw-actions, flex-wrap) so all four choices, 拒绝 included, stay reachable in the panel's narrow width instead of overflowing off its left edge (#593). Tests: core/test/member-status.test.js.
A group or face room shows only its own work (#553, owner decision 2026-10-20 23:03): memberWork takes a member's entry in work.rooms[room], else its own-chat turn whose work.bots[id].fromRoomId is this room (work handed out from here with message_bot), and nothing else. A Bot busy in another room or its own chat is idle in this room's dock and member list, with no "busy elsewhere" hint and none of that room's steps, since its members can be other people. Approvals count when their roomId is this room, or when they wait in the handed own-chat turn. Handed work has no Stop in the member list (it runs in the Bot's own chat; stop-member-work stops the group turn). The Bot's own chat dock and the sidebar's Bot row still name the groups it works in.
- Bot settings → Model has a Test connection row under the model picker (
ModelTestRowinmodel-test.js, shown once the Bot has a chosen model). Its hint says the request may be billed. A click poststest-model({ id }) once; the button is disabled while it runs (Testing…), then the row showsWorks. {model} answered in {seconds} s.or a short reason per host code (testOutcome: NO_MODEL, NO_KEY, AUTH, QUOTA, RATE_LIMIT, TIMEOUT, NOT_SERVED, MODEL_NOT_FOUND, TRANSPORT, SERVER, INVALID_REQUEST, else UNKNOWN) with the code beside it. The host never sends provider text, so nothing a provider echoes (a key, a token) can be drawn. Picking another model clears the result. Tests:core/test/model-test.test.js.
Hidden DSH tool-removal notices
-
FlyBot
restrict()s DSH'sask_user_questionoff Bot sessions (hideBlockingQuestion). DSH then appends a developertool-removalcontext row, and@deepseek-ai/dsh-client-ui-chatrenders it as "Tool removed: {name}".ContextCellshadowsconversation.chat.nodecontextand returns null for those notices (HIDDEN_REMOVED_TOOLS). Other tool-change context (tool-added, unrelated removals) still shows;tool-callcells are unchanged. Empty shadowed nodes collapse via the existing:emptyflow-item rule. Covered bycore/test/tool-cells.test.js. -
A Bot's held reply to a friend is drawn on its own turn (#418):
AssistantCell(cells.js) finds it in the Bot'sheldReplies(text.jsheldReplyOf, by session and turn) and wraps the bubbles inHeldReply(bt-held-reply,data-statepending / approved / rejected / failed; pending and not-sent bubbles are dashed and muted). The turn's closing text carriesHeldReplyActions(held-reply.js): while it waits, the holding policy's inlineflybot.approvalsentry (inline: true, props{ held, decide, call, actions }) or plain Approve / Reject, and it claims the id so no overlay card or pointer repeats it; after the decision a quietSent to {to}/Not sentline (Retry for a failed send); a reply that went through Auto-send (auto, #417) is a plain bubble withAuto-sent to {to}. The recipient is the person who gets the reply (#418, #498): the host'stoOwner(from the hold'srecipientName, stored, so the same live and after a reload), else the owner of the friend's chat (friendBotOwner), so the card readsChief → SelfB1233. The sidebar preview (previewOfunsentTurns) shows the friend's line for a turn whose reply did not go. The overlay ignores inline entries. -
The held-send overlay (
FlyBotApprovals,flybot-slots.js) runs every hook before itsif (pending.length === 0) return null: a hook after that return made React throw on the first held message and no card ever drew (#254).test/overlay-approvals.test.jsrenders it with a stand-in that enforces React's hook-count rule.
Settings → Connectors (src/client/connectors.js) lists one row per connector from roster.connectors. The GitHub row takes a token. The Composio row takes an API key and an app choice: the picked apps as pills (click to remove) and a Search Composio apps box that asks connector-app-search (debounced; with the key being pasted before connecting, never saved by the search, and with the saved key after) and offers the results to add; without a key to search with, or when the search fails, it filters the built-in common apps instead (#359). It then shows Connected: <apps>, one line per picked app with Ready or Needs sign-in from connector-app-status (asked when the row is ready and again when the list or the picker changes; nothing is drawn until Composio answers), with Choose apps (the same picker, each pill also showing its status) (changes the list through connector-apps, no key) and Disconnect. The key goes from the masked input to connector-connect and is never read back. Copy is English with a zh entry for every string in i18n.js. Under a connected connector the row reads On for all N Bots, On for K of N Bots or Off for every Bot. Choose which Bots may use it., and Choose Bots opens one switch per local Bot (src/client/connector-bots.js, setBotConnector(botId, connector, enabled) → bot-connectors, #335, #369, #601): the Bot's name, on by default; only an explicit bot.connectors[id] === false reads as off. Sharing a Bot changes nothing there; the footer line says it is on by default for each of the owner's Bots on every turn, and that a friend's own Bot never gets it (zh: 默认对你的每个 Bot 开启…). The "also when others use it" switch is gone (#601). The same switch per connector is the Connectors card in each Bot's settings (BotConnectorsCard, shown only once a connector is connected). Connector tool notices from DSH (Tool added/removed: mcp__github__* / mcp__composio__*) are not drawn in the chat.
src/client/about.js shows the FlyBot mark, Version <core version> · <branch> <commit> (the part after the version only for a git install), the Star button (the repository from update.repoUrl, the live count only when it came back) and the star request line, then Updates (the FlyBot core row and the DeepSeek Harness (underlying) row, zh DeepSeek Harness(底层)) and Links (Source code → the repository, Release notes → its releases, Report a problem → Settings → Feedback). For a git install on a branch the FlyBot core row reads Installed from GitHub (main 082563a). Updates come from the maintainer: reinstall FlyBot to get them., and after Check for updates Up to date with main., 1 commit behind main. …, N commits behind main. … or Could not compare with main. … plus · Checked <time>; a row with no known commit keeps the first sentence. An install pinned to a commit (build.pinned) reads Pinned to commit 6d04db6. Updates come from the maintainer: reinstall FlyBot to get them. and the version line shows only the short commit (#379). Other installs keep their old lines (Updates are not offered for this install., Installed from a local folder; update it there., Update checks are off.). Every string has a zh entry.
A discarded .catch(() => {}) in the client sources either starts with void (fire and forget) or carries a one-line reason comment (an awaited wait); a .catch(() => undefined) whose value is used stays. check-empty-catch enforces it (see docs/code-standards.md).
npm test in core/ (it also fails when client.js is stale).
- A friend's line that Bot Social attests the friend typed themselves (#227) is drawn as the person:
AssistantCelllooks the turn up inroster.remoteTypedTurns(typedByinsrc/client/text.js) and rendersPersonMessage— the person's name and a person mark (.bt-person-mark) — instead of the Bot'sBubbles. The name comes from the roster (the authenticated Bot Socialfrom), never from message text. - The same attested line sent to one of my own Bots outside that friend's chat (a friend's Chief typing to my Helper) arrives as a remote message whose source has
remote.typedBy;RemoteMessage(cells.js) then draws it as the person (name, person mark,via <Bot>) instead of the friend's Bot avatar and handle. WithouttypedByit keeps the Bot rendering (#233 follow-up). In a face-to-face roomremoteSpeaker(source, roster, sessionId)draws the Bot with its room seat's look (the participant'scolor/avatarfrom Bot Socialmembers[]), so the bubble matches the Members panel, the room avatar and the waiting tiles; a chat record for that handle keeps its id and wears the seat's look (#182). Thevianame is the carrying Bot's plain name when this home knows that Bot (its chat orroster.remoteBots), and is left out when it does not; the model-facing sender label (X (typed via @…, Bot Social)) and the handle-derived· owner's Botnever reach that header (#275). Settings → Bots offers only the user's own Bots as a Main Bot, in the first-Main-Bot and Add a Main Bot selects (#276). - A friend's line in my Bot's own chat (#420):
friend-lines.jsfriendLineNewscompares each Bot's host stampfriendLineAtwith the last stamp this browser has shown (localStorageds-bot:friend-lines; stamps against stamps, never this browser's clock) and, when a newer one arrives while that chat is not visited,index.jsmarks the Bot unread the way Mark as Unread does, so the row shows the blue dot until the chat is opened. A browser with no record yet takes what is there as shown; another member's shared Bot and a friend's Bot chat are skipped. The Main Bot tile shows the same dot (.bt-tile-unread; a wait still shows on its avatar). A line whose source hasremote.firstContactopens with one mutedFirstContactline (cells.js):<person or friend's Bot> is talking to <Bot> through Bot Social. Covered bycore/test/friend-lines.test.jsandcore/test/inbound-ui.test.js. - The roster poll (
index.jspull) sendswatching: !document.hiddenwith eachstatecall (#497), so the host knows a window is on screen and the social plugin reads friends' inboxes at its live cadence: a friend's first line, its held reply's card, the Inbox count and the unread badge arrive in that window within seconds, with no reload. - The NEW divider (
cells.jsTurnTailCell/TurnProcessCell/ the unread opening) compares turn times with the visit'sseen(when the chat was last left) andentered(when this visit opened). Once the divider has been on screen (NewMessagePillsinheader.js,view.divider === 'in-view') and a shown turn begins after the visit opened (movedOnSince: a friend's next line, the user's own message; hidden exchange turns do not count),actions.settleVisitsetsseentoentered(settledVisit) and every divider of the visit goes, as on leaving and coming back. A divider still scrolled out of view, or a chat where nothing new happened, keeps it until the visit ends. Same rule in every chat, including a friend's typed line in the Bot's own chat (#275). Covered bycore/test/turn-tail.test.js.