From 7c00e55ccb2d1d89c49412862e71eadee5faa950 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sun, 25 Oct 2026 18:44:01 +0800 Subject: [PATCH] feat(social): offer joining and starting a face-to-face room as two entries (#1021) One label named a create path and a join path at once, so a person who wanted to join read past a create path to find it. The new-chat menu now carries Join by number or link and Start a face-to-face room as separate rows; both open the same screen with the same handlers, titled after the row that was tapped, and the start path drops the link and scan fields because those are ways into a group that already exists. --- ...-join-and-start-are-two-entries.changed.md | 1 + core/client.js | 8 +- core/src/client/i18n.js | 8 +- docs/subsystems/core-client.md | 2 +- docs/subsystems/plugin-runtime.md | 2 +- docs/subsystems/social.md | 2 +- patches/ds-bot/0001-core-fixes.patch | 76 +- patches/ds-bot/0008-client-bundle.patch | 716 +++++++++--------- plugins/social/client.js | 68 +- plugins/social/test/social.test.js | 106 ++- 10 files changed, 549 insertions(+), 440 deletions(-) create mode 100644 changelog.d/1021-join-and-start-are-two-entries.changed.md diff --git a/changelog.d/1021-join-and-start-are-two-entries.changed.md b/changelog.d/1021-join-and-start-are-two-entries.changed.md new file mode 100644 index 000000000..c52dff145 --- /dev/null +++ b/changelog.d/1021-join-and-start-are-two-entries.changed.md @@ -0,0 +1 @@ +The new-chat menu offers joining and starting as two rows, so one label no longer names both: **Join by number or link** carries the number and the invite link, and **Start a face-to-face room** opens the same screen under its own name without the link and scan fields, because those are ways into a group that already exists ([#1021](https://github.com/ThinkFlowLab/FlyBot/issues/1021)). diff --git a/core/client.js b/core/client.js index 8d17e47b9..1fd075baf 100644 --- a/core/client.js +++ b/core/client.js @@ -55,14 +55,18 @@ var ZH = { "Chat with several of your Bots": "和你的几个 Bot 一起聊", "Face-to-face room": "面对面房间", "Meet people nearby with a 6-digit number": "用同一组 6 位数字和身边的人见面", - // #892: one entry for both ways in, a number entered together or a group's invite link, and its dialog. - "Join or create by number or link": "面对面建群或用链接加入", + // #892: the entries and the dialog named both ways in. #1021: the picker offers them as two rows, + // so the label that named a create path and a join path at once is gone. + "Join by number or link": "用号码或链接加入", + "Start a face-to-face room": "面对面建群", // #1053: a fresh 6-digit code offers to start the room. The hint still covers a number whose room already opened (#999). "Start a new room with {code}": "用 {code} 开一个新房间", "No one is waiting under this number. If someone already opened the room, the number no longer works — ask them for an invite link.": "这个号码下还没有人在等。如果已经有人开过这个房间,号码就不再有效——向他们要一条邀请链接。", "Start a new room": "开一个新房间", "Enter a number, or paste a group invite link": "输入数字面对面建群,或粘贴邀请链接加入", "Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.": "选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。", + "Pick the number everyone nearby types": "定一个大家都输入的号码", + "Pick your Bots and choose a 6-digit number. Everyone nearby types the same number to build the room together.": "选好你的 Bot,再定一组 6 位数字。身边的人输入同一个号码,就能一起把这个群建起来。", "Or paste a group invite link (https://…/r/…)": "或粘贴群邀请链接(https://…/r/…)", // #1044: a turn that died before writing anything still says so where the chat is. "Could not run: {error}": "这一轮没能跑起来:{error}", diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js index 32104873a..0233eccd5 100644 --- a/core/src/client/i18n.js +++ b/core/src/client/i18n.js @@ -18,14 +18,18 @@ const ZH = { 'Chat with several of your Bots': '和你的几个 Bot 一起聊', 'Face-to-face room': '面对面房间', 'Meet people nearby with a 6-digit number': '用同一组 6 位数字和身边的人见面', - // #892: one entry for both ways in, a number entered together or a group's invite link, and its dialog. - 'Join or create by number or link': '面对面建群或用链接加入', + // #892: the entries and the dialog named both ways in. #1021: the picker offers them as two rows, + // so the label that named a create path and a join path at once is gone. + 'Join by number or link': '用号码或链接加入', + 'Start a face-to-face room': '面对面建群', // #1053: a fresh 6-digit code offers to start the room. The hint still covers a number whose room already opened (#999). 'Start a new room with {code}': '用 {code} 开一个新房间', 'No one is waiting under this number. If someone already opened the room, the number no longer works — ask them for an invite link.': '这个号码下还没有人在等。如果已经有人开过这个房间,号码就不再有效——向他们要一条邀请链接。', 'Start a new room': '开一个新房间', 'Enter a number, or paste a group invite link': '输入数字面对面建群,或粘贴邀请链接加入', 'Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.': '选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。', + 'Pick the number everyone nearby types': '定一个大家都输入的号码', + 'Pick your Bots and choose a 6-digit number. Everyone nearby types the same number to build the room together.': '选好你的 Bot,再定一组 6 位数字。身边的人输入同一个号码,就能一起把这个群建起来。', 'Or paste a group invite link (https://…/r/…)': '或粘贴群邀请链接(https://…/r/…)', // #1044: a turn that died before writing anything still says so where the chat is. 'Could not run: {error}': '这一轮没能跑起来:{error}', diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index f8dc757fd..de8291acb 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -70,7 +70,7 @@ Add a new invariant under the heading it belongs to (or a new heading), not at t ### Held-send approval card -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 face-room create prompt (*Start a new room with {code}*, and the hint under it, #1053) is in that same map, so a zh UI reads it in Chinese. +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 face-room create prompt (*Start a new room with {code}*, and the hint under it, #1053) is in that same map, so a zh UI reads it in Chinese. So are the social picker's two face-room rows and the screen they open (#1021): **Join by number or link** (用号码或链接加入) and **Start a face-to-face room** (面对面建群), the start row's hint, and its own about line, all four used through `t()` — so both rows and both dialog titles read Chinese, and the single key #892 wrote for one combined row (*Join or create by number or link*) is gone. 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 `canRetry()` allows, #968). 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 only while `canRetry()` allows it (#968). 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](core-host.md). diff --git a/docs/subsystems/plugin-runtime.md b/docs/subsystems/plugin-runtime.md index c4294789e..ad9e80a79 100644 --- a/docs/subsystems/plugin-runtime.md +++ b/docs/subsystems/plugin-runtime.md @@ -75,7 +75,7 @@ FlyBot's plugin API surface on top of the patched core: host hooks, client slots - Social registers a second `flybot.session.dock` entry, `social-room-end` (`FaceRoomStatus`, order 40): the plain ended-room line for a left/closed face room (#191). - Social registers `flybot.settings.page` (`SocialServerSettings`, label **Bot Social**): the server address, its status and the switch form (#337). The host side is `social/server` (a read) and `social/server-check` / `social/server-set` (writes: blocked on a read-only host and, like every plugin write, while the team has more than one member). - Social also registers `flybot.account.header` (`MyHandle`): my own Bot Social handle plus Copy at the top of the account menu (#181), hidden while not connected. -- Social registers two `flybot.new-chat.action` rows (Join or create by number or link, Add friend). Each `run` closes the picker. Copying an invite and seeing your own handle happen inside the Add friend panel (#181). Still not team mode. +- Social registers three `flybot.new-chat.action` rows (Join by number or link, Start a face-to-face room, Add friend; the two face rows pass which one opened the screen, #1021). Each `run` closes the picker. Copying an invite and seeing your own handle happen inside the Add friend panel (#181). Still not team mode. - A friend's Bot opens **Bot settings** through core `remote-bot-settings` (`openSettings('bots', id)`): a compact sheet (`data-flybot=remote-bot-sheet`) with `FlyBotDetails` from slot `flybot.bot.details`, not the local Bot editor. The sheet stacks above its scrim like the host settings overlay (#166). - In an own Bot's settings the host frames `flybot.bot.details` in its titled **Sharing** card (`FlyBotDetails({ wrap })`), so an entry draws no title of its own; the friend's-Bot sheet and the details drawer draw the entries without a frame (#406). diff --git a/docs/subsystems/social.md b/docs/subsystems/social.md index 2eb56a813..fc0162fe0 100644 --- a/docs/subsystems/social.md +++ b/docs/subsystems/social.md @@ -10,7 +10,7 @@ The social plugin talks to a [Bot Social](https://github.com/Birfy/bot-social) s Bot Social plugin for chatting with friends' Bots (send middleware `social` with per-home approval) lives entirely under `plugins/social/`. Inbound friend messages arrive immediately; outbound drafts the server holds (`pending_approval`) are listed on `social/status` and approved in the open chat (`flybot.session.dock`). The social sidebar strip shows a badge and a one-line jump list. The same queue is also attached to a native shared room via `flybot.roomApprovals.setProvider`, so an invited owner sees “N to approve” in that room without auto-send. -Face-to-face rooms (#83, #92) also live here: the primary entry is the sidebar **+** menu (slot `flybot.new-chat.action` on the core new-chat picker, after New group chat; `label` / `hint` use the same `t()` map). **Join or create by number or link** (zh 面对面建群或用链接加入, #892; it covers both ways in: a six-digit number entered together, or a group invite link pasted or scanned) and **Add friend** are those rows (copying an invite and your own handle live inside Add friend); the social strip has no **+ Add** button. That entry opens the same screen, titled the same, whose copy names both ways and whose link field reads *Or paste a group invite link* (pick one or more local Bots, six-digit number, live waiting room that shows the code, **Enter chat**). The pick list and waiting roster draw each Bot with the host `BotAvatar` (team roster look for yours; a friend's Bot uses its published look or a handle-seeded look, plus any avatar face-room `members[]` carries). Waiting-room and picker Bots are a wrapping tile grid (avatar on top, name centered below; yours a tiny accent dot; waiting tiles use the server owner label, e.g. `Chief · BoxB`); **Leave** and **Enter chat** share a fixed footer while a long grid scrolls under the countdown. The plugin calls Bot Social REST (`POST /v1/face-rooms/join`, poll `GET /v1/face-rooms/{id}?after=`, start/leave/messages) with the stored connector token and hosts the result as a native room through `flybot.rooms` (`origin: 'face-to-face'`). Those face-room routes accept a connector token; owner settings do not (`GET`/`PATCH /v1/me` returns 403 `connector_token_rest_forbidden`). Peer text is fenced; `pending_approval` drafts of kind `face_room_message` use the in-chat Approve / Reject path. **Auto-send** for face rooms is one compact checkbox (tooltip) on the open room's overview, via MCP `whoami` (read) and `set_auto_approve { auto_approve_face_rooms }` (write; the owner's click requests and uses `confirm_code` in the same step); default off. A friend's Bot in the room card, waiting grid, **main sidebar row**, and core group overview uses the host `BotAvatar` (published look, optional member `avatar`, else a handle-seeded look). **Sharing → Copy invite link** creates a per-Bot grant when the friend accepts; **Share with a friend** (#500) lists current friends who do not have this Bot yet and grants it in one click (Bot Social `grant_bot_to_friend`, companion Birfy/bot-social#34) without an invite link — one row per **Bot**, not per person, labelled `Bot (Owner)` and carrying that Bot's handle and `peer_bot_id`, so a friend who runs several Bots gets a row each and the click grants to the row's own Bot while the receipt names it (#973) — from the row label the click carries, not from the home key, so a server answer without names never prints `selftestb2` (#994), and a caller that sends no label gets `with your friend` instead; the filter asks per **Bot** too, so a friend whose Bot already has it still gets a row for the others and only a grant that names no Bot falls back to hiding that whole friend (#981); the friend is named by `friend_bot_id`, the peer id `list_friends` returns — the platform never lists a friend's owner id — and `friend_owner_id` still works for a server that only knows it (#592); a server without that tool tries the REST route `POST /v1/bots/:id/grants` and, when it 404s too, says *Bot Social needs an update to share with a friend. Invite links still work.* instead of the raw MCP error (the production `MCP error -32602: Tool … not found` wording counts as an unknown tool, #563); a server that has the tool but refuses the arguments says *Bot Social did not accept that share request. Its server may need an update; invite links still work.*, never the raw `-32602` text (#592); A failed **Share** and a failed friend-list read keep separate notes (#987) — two slots, not one (#992): the share outcome (`share-note`) and the list error (`share-list-error`) are drawn side by side, so a share click cannot erase the list error and its **Try again**, and the 5 s poll cannot erase the share's message; a successful read settles the list error only, a successful share settles the share line only, and "No friends left to share with" is suppressed by a list error only — a share error never speaks for the list. **People with access** lists one row per **Bot** that has this Bot — named `Bot (Owner)` like the share list, carrying that Bot's handle, the groups it is used in, and its own **Revoke** (#995). **Revoke** asks once before it runs (#1073): the button opens `Revoke ? They lose this Bot.` (and `in N rooms` when that row already names rooms) with **Revoke** and **Keep**, and never the browser confirm box. The handle, or the home when the row names no Bot, and the usage hint share one line with a middle dot between them (`@handle · In use in N rooms`), so the two never run together (#1073). One **Auto-send** switch per person. The hint stays one readable row however long the share lives (#955): it names at most three rooms and then `+N more`, keeps every room one per line in its tooltip, and a room a grant row already named is not printed again under **This room only**. There is no published toggle. A line that Bot sends 1:1 to a seat in that room is judged under the room (#948): the plugin resolves it before the member check (`roomFor` → `roomContextFor`), the seat itself is the target when the roster does not list it (`pairFor`), and the send names that room (`room_id`) so Bot Social checks the same consent the host already did; the `roomOnly` identity is neither dropped nor widened — with no such room the refusal is the same sentence as before, and `send_as_member` is never reached. Bringing my own Bot by number is an implicit room-scoped grant: if it has no identity yet, join registers it room-only (`add_home_member`) and revokes that identity (`remove_home_member`) when it leaves its last face room. Friends' Bots lists only Bots granted to me. Held replies show in the chat (owner Approve / Reject). **Remove this Bot** declines that grant on Bot Social (`decline_bot_grant`) so it leaves the owner's Manage access; an older server falls back to a local hide. The row drops immediately; room leave and a roster refresh continue in the background. A local hide is forgotten only after two consecutive successful `list_remote_bots` results without that handle, or an explicit re-share / `regranted` join. Hidden, gone, and home-level remotes stay out of `flybot.remote.publish` and the new-chat picker. Friends' Bots paints a cached roster immediately and refreshes in the background (keep-alive HTTP, parallel mount calls). +Face-to-face rooms (#83, #92) also live here: the primary entry is the sidebar **+** menu (slot `flybot.new-chat.action` on the core new-chat picker, after New group chat; `label` / `hint` use the same `t()` map). **Join by number or link** (zh 用号码或链接加入) and **Start a face-to-face room** (zh 面对面建群, #1021) are two rows, because one label naming a create path and a join path at once read as neither (#892 had one); **Add friend** is the third (copying an invite and your own handle live inside Add friend); the social strip has no **+ Add** button. Both face rows open the same screen with the same handlers, titled after the row that was tapped; the start path keeps the six-digit number (a room is built by typing one together) and drops the link and scan fields, which are ways into a group that already exists. The join path's copy names both ways in and its link field reads *Or paste a group invite link* (pick one or more local Bots, six-digit number, live waiting room that shows the code, **Enter chat**). The pick list and waiting roster draw each Bot with the host `BotAvatar` (team roster look for yours; a friend's Bot uses its published look or a handle-seeded look, plus any avatar face-room `members[]` carries). Waiting-room and picker Bots are a wrapping tile grid (avatar on top, name centered below; yours a tiny accent dot; waiting tiles use the server owner label, e.g. `Chief · BoxB`); **Leave** and **Enter chat** share a fixed footer while a long grid scrolls under the countdown. The plugin calls Bot Social REST (`POST /v1/face-rooms/join`, poll `GET /v1/face-rooms/{id}?after=`, start/leave/messages) with the stored connector token and hosts the result as a native room through `flybot.rooms` (`origin: 'face-to-face'`). Those face-room routes accept a connector token; owner settings do not (`GET`/`PATCH /v1/me` returns 403 `connector_token_rest_forbidden`). Peer text is fenced; `pending_approval` drafts of kind `face_room_message` use the in-chat Approve / Reject path. **Auto-send** for face rooms is one compact checkbox (tooltip) on the open room's overview, via MCP `whoami` (read) and `set_auto_approve { auto_approve_face_rooms }` (write; the owner's click requests and uses `confirm_code` in the same step); default off. A friend's Bot in the room card, waiting grid, **main sidebar row**, and core group overview uses the host `BotAvatar` (published look, optional member `avatar`, else a handle-seeded look). **Sharing → Copy invite link** creates a per-Bot grant when the friend accepts; **Share with a friend** (#500) lists current friends who do not have this Bot yet and grants it in one click (Bot Social `grant_bot_to_friend`, companion Birfy/bot-social#34) without an invite link — one row per **Bot**, not per person, labelled `Bot (Owner)` and carrying that Bot's handle and `peer_bot_id`, so a friend who runs several Bots gets a row each and the click grants to the row's own Bot while the receipt names it (#973) — from the row label the click carries, not from the home key, so a server answer without names never prints `selftestb2` (#994), and a caller that sends no label gets `with your friend` instead; the filter asks per **Bot** too, so a friend whose Bot already has it still gets a row for the others and only a grant that names no Bot falls back to hiding that whole friend (#981); the friend is named by `friend_bot_id`, the peer id `list_friends` returns — the platform never lists a friend's owner id — and `friend_owner_id` still works for a server that only knows it (#592); a server without that tool tries the REST route `POST /v1/bots/:id/grants` and, when it 404s too, says *Bot Social needs an update to share with a friend. Invite links still work.* instead of the raw MCP error (the production `MCP error -32602: Tool … not found` wording counts as an unknown tool, #563); a server that has the tool but refuses the arguments says *Bot Social did not accept that share request. Its server may need an update; invite links still work.*, never the raw `-32602` text (#592); A failed **Share** and a failed friend-list read keep separate notes (#987) — two slots, not one (#992): the share outcome (`share-note`) and the list error (`share-list-error`) are drawn side by side, so a share click cannot erase the list error and its **Try again**, and the 5 s poll cannot erase the share's message; a successful read settles the list error only, a successful share settles the share line only, and "No friends left to share with" is suppressed by a list error only — a share error never speaks for the list. **People with access** lists one row per **Bot** that has this Bot — named `Bot (Owner)` like the share list, carrying that Bot's handle, the groups it is used in, and its own **Revoke** (#995). **Revoke** asks once before it runs (#1073): the button opens `Revoke ? They lose this Bot.` (and `in N rooms` when that row already names rooms) with **Revoke** and **Keep**, and never the browser confirm box. The handle, or the home when the row names no Bot, and the usage hint share one line with a middle dot between them (`@handle · In use in N rooms`), so the two never run together (#1073). One **Auto-send** switch per person. The hint stays one readable row however long the share lives (#955): it names at most three rooms and then `+N more`, keeps every room one per line in its tooltip, and a room a grant row already named is not printed again under **This room only**. There is no published toggle. A line that Bot sends 1:1 to a seat in that room is judged under the room (#948): the plugin resolves it before the member check (`roomFor` → `roomContextFor`), the seat itself is the target when the roster does not list it (`pairFor`), and the send names that room (`room_id`) so Bot Social checks the same consent the host already did; the `roomOnly` identity is neither dropped nor widened — with no such room the refusal is the same sentence as before, and `send_as_member` is never reached. Bringing my own Bot by number is an implicit room-scoped grant: if it has no identity yet, join registers it room-only (`add_home_member`) and revokes that identity (`remove_home_member`) when it leaves its last face room. Friends' Bots lists only Bots granted to me. Held replies show in the chat (owner Approve / Reject). **Remove this Bot** declines that grant on Bot Social (`decline_bot_grant`) so it leaves the owner's Manage access; an older server falls back to a local hide. The row drops immediately; room leave and a roster refresh continue in the background. A local hide is forgotten only after two consecutive successful `list_remote_bots` results without that handle, or an explicit re-share / `regranted` join. Hidden, gone, and home-level remotes stay out of `flybot.remote.publish` and the new-chat picker. Friends' Bots paints a cached roster immediately and refreshes in the background (keep-alive HTTP, parallel mount calls). ## Owns diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index 73f08cb57..59ba66bf6 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -464,7 +464,7 @@ diff --git a/src/client/feedback.js b/src/client/feedback.js diff --git a/src/client/i18n.js b/src/client/i18n.js --- a/src/client/i18n.js +++ b/src/client/i18n.js -@@ -12,6 +12,63 @@ +@@ -12,6 +12,67 @@ 'Unread activity': '有未读', Search: '搜索', 'New chat': '新对话', @@ -474,14 +474,18 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js + 'Chat with several of your Bots': '和你的几个 Bot 一起聊', + 'Face-to-face room': '面对面房间', + 'Meet people nearby with a 6-digit number': '用同一组 6 位数字和身边的人见面', -+ // #892: one entry for both ways in, a number entered together or a group's invite link, and its dialog. -+ 'Join or create by number or link': '面对面建群或用链接加入', ++ // #892: the entries and the dialog named both ways in. #1021: the picker offers them as two rows, ++ // so the label that named a create path and a join path at once is gone. ++ 'Join by number or link': '用号码或链接加入', ++ 'Start a face-to-face room': '面对面建群', + // #1053: a fresh 6-digit code offers to start the room. The hint still covers a number whose room already opened (#999). + 'Start a new room with {code}': '用 {code} 开一个新房间', + 'No one is waiting under this number. If someone already opened the room, the number no longer works — ask them for an invite link.': '这个号码下还没有人在等。如果已经有人开过这个房间,号码就不再有效——向他们要一条邀请链接。', + 'Start a new room': '开一个新房间', + 'Enter a number, or paste a group invite link': '输入数字面对面建群,或粘贴邀请链接加入', + 'Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.': '选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。', ++ 'Pick the number everyone nearby types': '定一个大家都输入的号码', ++ 'Pick your Bots and choose a 6-digit number. Everyone nearby types the same number to build the room together.': '选好你的 Bot,再定一组 6 位数字。身边的人输入同一个号码,就能一起把这个群建起来。', + 'Or paste a group invite link (https://…/r/…)': '或粘贴群邀请链接(https://…/r/…)', + // #1044: a turn that died before writing anything still says so where the chat is. + 'Could not run: {error}': '这一轮没能跑起来:{error}', @@ -528,7 +532,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'New…': '新建', 'Animation': '动画', 'Bot animation': 'Bot 动画', -@@ -26,6 +26,21 @@ +@@ -26,6 +87,21 @@ Duplicate: '复制一份', Hidden: '已隐藏', Admin: '管理员', @@ -550,7 +554,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Add: '添加', Close: '关闭', Copy: '复制', -@@ -41,22 +41,69 @@ +@@ -41,22 +117,69 @@ ' and ': '、', and: '和', '{count} members': '{count} 位成员', @@ -623,7 +627,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Connectors: '连接器', 'Search connectors': '搜索连接器', 'No connectors': '暂无连接器', -@@ -93,6 +93,59 @@ +@@ -93,6 +216,59 @@ 'Connecting…': '正在连接…', 'Could not connect: {error}': '连接失败:{error}', 'Connected as {account}': '已连接:{account}', @@ -683,7 +687,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Connected: '已连接', Disconnect: '断开', Connect: '连接', -@@ -105,17 +105,44 @@ +@@ -105,17 +281,44 @@ 'GitHub addresses must start with https://': 'GitHub 地址必须以 https:// 开头', 'This DSH cannot load connectors': '这个 DSH 不能加载连接器', 'No such connector': '没有这个连接器', @@ -732,7 +736,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js // Header 'Message {name}': '给 {name} 发消息', -@@ -128,8 +128,6 @@ +@@ -128,8 +331,6 @@ New: '新', // New chat @@ -741,7 +745,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Add to group chat': '加入群聊', 'Message Bot': '给 Bot 发消息', 'To:': '发给:', -@@ -154,9 +154,20 @@ +@@ -154,9 +355,20 @@ Everyone: '所有人', 'Every member answers in turn, admin first.': '每位成员轮流回答,管理员先说。', 'Members · {count}': '成员 · {count}', @@ -762,7 +766,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Remove {name} from the group': '把 {name} 移出群聊', 'Remove from group': '移出群聊', 'Add member': '添加成员', -@@ -215,6 +215,25 @@ +@@ -215,6 +427,25 @@ 'This model is not available now. Pick another one.': '这个模型现在不可用,请换一个。', 'Every message of this Bot runs on it.': '这个 Bot 的每条消息都用它。', 'Applies from the next message.': '从下一条消息开始生效。', @@ -788,7 +792,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Profile: '资料', Label: '标签', 'Optional. Shown under the name.': '可选,显示在名字下方。', -@@ -238,6 +238,7 @@ +@@ -238,6 +469,7 @@ 'Kept above the other chats.': '排在其他对话上面。', 'Search still finds it.': '仍然可以搜索到。', Manage: '管理', @@ -796,7 +800,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js // Group chat settings 'Leave it empty to name the group after its members.': '留空就按成员命名。', -@@ -274,6 +274,8 @@ +@@ -274,6 +506,8 @@ 'Harness default': 'Harness 默认', Saving: '保存中', 'Manage models': '管理模型', @@ -805,7 +809,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js // Shapes Whale: '鲸鱼', Star: '星星', Moon: '月亮', Wave: '声波', Squircle: '圆角方', Bull: '牛角', Blossom: '花朵', -@@ -312,9 +312,10 @@ +@@ -312,9 +546,10 @@ 'No group chats yet.': '还没有群聊。', 'A group chat lets several Bots work on one thing with you.': '在群聊里,几个 Bot 和你一起做一件事。', 'Back to group chats': '返回群聊列表', @@ -818,7 +822,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Harness settings': 'Harness 设置', 'Back to Bots': '返回 Bot 列表', 'Close settings': '关闭设置', -@@ -324,7 +324,6 @@ +@@ -324,7 +559,6 @@ Pin: '置顶', Pinned: '已置顶', 'Mark as Unread': '标为未读', @@ -826,7 +830,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Group settings': '群设置', 'Rename group': '重命名群聊', 'Copy conversation ID': '复制对话 ID', -@@ -333,6 +333,8 @@ +@@ -333,6 +567,8 @@ // The DSH Agent's own row and Session list. "DSH Agent" is a product name and // stays in English. 'Add DSH Agent': '添加 DSH Agent', @@ -835,7 +839,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Remove DSH Agent': '移除 DSH Agent', 'New session': '新会话', Archive: '归档', -@@ -344,13 +344,13 @@ +@@ -344,13 +580,13 @@ '{n} d': '{n} 天', // Plugin updates @@ -855,7 +859,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'You have {version}.': '当前版本 {version}。', 'The update did not finish: {reason}': '更新没有完成:{reason}', "What's new": '更新说明', -@@ -360,9 +360,68 @@ +@@ -360,9 +596,68 @@ 'DSH update': 'DSH 更新', 'DSH {version}': 'DSH {version}', 'Restart DSH to start using DSH {version}.': '重启 DSH 后开始使用 DSH {version}。', @@ -925,7 +929,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js // Command palette Messages: '消息', Commands: '命令', -@@ -390,6 +390,7 @@ +@@ -390,6 +685,7 @@ Reply: '回复', 'More message actions': '更多操作', More: '更多', @@ -933,7 +937,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Reactions: '表情', 'Message actions': '消息操作', Download: '下载', -@@ -403,6 +403,9 @@ +@@ -403,6 +699,9 @@ 'Replies from': '群聊回复:', 'Reply from': '收到回复:', 'Message from': '收到消息:', @@ -943,7 +947,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Stopped by': '叫停者:', '{count} messages with': '{count} 条消息,来自', 'Routine started': '例行任务开始', -@@ -412,6 +412,8 @@ +@@ -412,6 +711,8 @@ 'Working out a plan': '理一下思路', 'Sorting out the details': '梳理细节', 'On it': '在做了', @@ -952,7 +956,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Busy with it': '正在处理', 'Moving ahead': '推进中', 'Taking care of it': '处理中', -@@ -439,7 +439,6 @@ +@@ -439,7 +740,6 @@ 'Looking back through the chat': '翻看聊天记录', '{name} looked up its own chat': '{name} 查看了自己的对话', '{name} looked up its own chat for “{text}”': '{name} 在自己的对话里查找「{text}」', @@ -960,7 +964,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js "{name} didn't answer in time": '{name} 没有及时回应', '{name} paused for your approval': '{name} 停下来等你批准', '{name} started on it': '{name} 开始做了', -@@ -450,12 +450,30 @@ +@@ -450,12 +750,30 @@ 'Asked to make the tool call it wrote as text': '工具调用被写成了文字,已让它重新调用', 'A Bot': '一个 Bot', '{name} is waiting for your approval': '{name} 等你批准', @@ -992,7 +996,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js "Couldn't message {name}": '没能给 {name} 发消息', Messaged: '已发消息给', "Couldn't stop {name}": '没能叫停 {name}', -@@ -478,6 +478,7 @@ +@@ -478,6 +796,7 @@ Selected: '已选', Skip: '跳过', Submit: '提交', @@ -1000,7 +1004,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Choose all that apply': '可多选', 'Dismiss question': '关闭问题', Dismiss: '关闭', -@@ -485,7 +485,7 @@ +@@ -485,7 +804,7 @@ 'Your answer': '你的回答', // Secrets @@ -1009,7 +1013,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'No Bot': '没有 Bot', 'Paste or type the key': '粘贴或输入密钥', 'Only {name}': '只给 {name}', -@@ -528,7 +528,7 @@ +@@ -528,7 +847,7 @@ 'NAME, e.g. GITHUB_TOKEN': '名字,例如 GITHUB_TOKEN', 'What it is for': '用途', Value: '值', @@ -1018,7 +1022,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Save key': '保存密钥', 'Keys your Bots asked for. Bots never see the values: they use them as ': '你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ', ' in their shell, and every value is replaced by ': ' 使用,而且发给模型之前,每个值都会被替换成 ', -@@ -697,11 +697,11 @@ +@@ -697,11 +1016,11 @@ 'Compared with the {count} days before': '与再往前 {count} 天相比', '{share}% of all usage from {span}': '占 {span} 全部用量的 {share}%', // About @@ -1033,7 +1037,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Updates: '更新', 'Check for updates': '检查更新', 'Checking…': '正在检查…', -@@ -711,6 +711,13 @@ +@@ -711,6 +1030,13 @@ 'Update checks are off.': '已关闭更新检查。', 'Installed from a local folder; update it there.': '从本地文件夹安装,请在那里更新。', 'Updates are not offered for this install.': '这种安装方式不提供更新。', @@ -1047,7 +1051,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js Links: '链接', 'Source code': '源代码', 'Release notes': '发布说明', -@@ -726,7 +726,7 @@ +@@ -726,7 +1052,7 @@ Show: '查看', Hide: '收起', 'GitHub issue': 'GitHub issue', @@ -1056,7 +1060,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Report on GitHub': '在 GitHub 上报告', 'Copied the diagnostics.': '已复制诊断信息。', 'Could not copy. Select the text below and copy it.': '无法复制。请选中下面的文字手动复制。', -@@ -738,7 +738,7 @@ +@@ -738,7 +1064,7 @@ 'Recent errors': '最近的错误', 'Recent errors · {count}': '最近的错误 · {count}', 'No errors recorded.': '没有错误记录。', @@ -1065,7 +1069,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Model call': '模型调用', 'This window': '这个窗口', 'Error log': '错误日志', -@@ -771,6 +771,14 @@ +@@ -771,6 +1097,14 @@ 'Add at least one more Bot': '再至少添加一个 Bot', Members: '成员', // Connectors and scheduled tasks @@ -1080,7 +1084,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Scheduled tasks': '定时任务', 'Every day': '每天', 'Every {count} days': '每 {count} 天', -@@ -784,7 +784,15 @@ +@@ -784,7 +1118,15 @@ 'Every weekend at {time}': '每个周末 {time}', 'On the schedule {expression}': '按计划 {expression}', 'Next: {time}': '下次 {time}', @@ -1096,7 +1100,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'in {name}': '在 {name}', 'Click again to delete': '再点一次删除', 'Delete task': '删除任务', -@@ -806,8 +806,8 @@ +@@ -806,8 +1148,8 @@ Unit: '单位', 'Create task': '创建任务', 'Turn on Automation tasks in DSH': '在 DSH 里打开「自动化任务」', @@ -1107,7 +1111,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'New task': '新建任务', 'No scheduled tasks yet.': '还没有定时任务。', 'No scheduled tasks yet. Add one': '还没有定时任务,去添加', -@@ -823,6 +823,12 @@ +@@ -823,6 +1165,12 @@ 'Repeat every 1 minute to 7 days': '重复间隔要在 1 分钟到 7 天之间', 'Pick when it runs': '选择什么时候运行', 'No such task': '没有这个任务', @@ -1120,7 +1124,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Working folder': '工作文件夹', 'Working folder: {folder}': '工作文件夹:{folder}', Folder: '文件夹', -@@ -855,6 +855,8 @@ +@@ -855,6 +1203,8 @@ [/^A key can have at most (\d+) characters$/, '密钥最多 $1 个字符'], [/^(.+) is not saved yet$/, '$1 还没有保存'], [/^The Bot team in (.+) is open in another dsh \((.+)\)\. This window shows the team but cannot change it; close the other one and reload\.$/, '$1 里的 Bot 团队已在另一个 dsh 中打开($2)。这个窗口只能查看,不能修改;关掉另一个后刷新。'], @@ -1129,7 +1133,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js [/^HTTP (\d+)$/, '请求失败(HTTP $1)'], [/^GitHub answered (\d+)$/, 'GitHub 返回了 $1'], [/^Keep the name under (\d+) characters$/, '名字不能超过 $1 个字'], -@@ -883,6 +883,20 @@ +@@ -883,6 +1233,20 @@ 'Unknown Bot': '找不到这个 Bot', 'Unknown conversation': '找不到这个对话', 'A group chat needs at least two Bots': '群聊至少要两个 Bot', @@ -1150,7 +1154,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'The admin must be a member of the group': '管理员必须是群成员', 'Unknown group chat': '找不到这个群聊', 'Hand the admin role to another member instead of leaving the group without one': '请把管理员交给其他成员,不要让群聊没有管理员', -@@ -905,6 +905,8 @@ +@@ -905,6 +1269,8 @@ 'Write the entry to save.': '请写下要保存的内容。', 'This DeepSeek Harness has no folder chooser': '这个 DeepSeek Harness 没有文件夹选择窗口', 'This DeepSeek Harness cannot list folders': '这个 DeepSeek Harness 不能列出文件夹', diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 884ecf58b..1d9ba501f 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -3,7 +3,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gener diff --git a/client.js b/client.js --- a/client.js +++ b/client.js -@@ -49,6 +49,63 @@ +@@ -49,6 +49,67 @@ "Unread activity": "有未读", Search: "搜索", "New chat": "新对话", @@ -13,14 +13,18 @@ diff --git a/client.js b/client.js + "Chat with several of your Bots": "和你的几个 Bot 一起聊", + "Face-to-face room": "面对面房间", + "Meet people nearby with a 6-digit number": "用同一组 6 位数字和身边的人见面", -+ // #892: one entry for both ways in, a number entered together or a group's invite link, and its dialog. -+ "Join or create by number or link": "面对面建群或用链接加入", ++ // #892: the entries and the dialog named both ways in. #1021: the picker offers them as two rows, ++ // so the label that named a create path and a join path at once is gone. ++ "Join by number or link": "用号码或链接加入", ++ "Start a face-to-face room": "面对面建群", + // #1053: a fresh 6-digit code offers to start the room. The hint still covers a number whose room already opened (#999). + "Start a new room with {code}": "用 {code} 开一个新房间", + "No one is waiting under this number. If someone already opened the room, the number no longer works — ask them for an invite link.": "这个号码下还没有人在等。如果已经有人开过这个房间,号码就不再有效——向他们要一条邀请链接。", + "Start a new room": "开一个新房间", + "Enter a number, or paste a group invite link": "输入数字面对面建群,或粘贴邀请链接加入", + "Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.": "选好你的 Bot。和身边的人输入同一组 6 位数字一起建群,或粘贴群邀请链接、扫描二维码加入那个群。", ++ "Pick the number everyone nearby types": "定一个大家都输入的号码", ++ "Pick your Bots and choose a 6-digit number. Everyone nearby types the same number to build the room together.": "选好你的 Bot,再定一组 6 位数字。身边的人输入同一个号码,就能一起把这个群建起来。", + "Or paste a group invite link (https://…/r/…)": "或粘贴群邀请链接(https://…/r/…)", + // #1044: a turn that died before writing anything still says so where the chat is. + "Could not run: {error}": "这一轮没能跑起来:{error}", @@ -67,7 +71,7 @@ diff --git a/client.js b/client.js "New…": "新建", "Animation": "动画", "Bot animation": "Bot 动画", -@@ -63,6 +63,21 @@ +@@ -63,6 +124,21 @@ Duplicate: "复制一份", Hidden: "已隐藏", Admin: "管理员", @@ -89,7 +93,7 @@ diff --git a/client.js b/client.js Add: "添加", Close: "关闭", Copy: "复制", -@@ -78,21 +78,68 @@ +@@ -78,21 +154,68 @@ " and ": "、", and: "和", "{count} members": "{count} 位成员", @@ -161,7 +165,7 @@ diff --git a/client.js b/client.js Connectors: "连接器", "Search connectors": "搜索连接器", "No connectors": "暂无连接器", -@@ -129,6 +129,59 @@ +@@ -129,6 +252,59 @@ "Connecting…": "正在连接…", "Could not connect: {error}": "连接失败:{error}", "Connected as {account}": "已连接:{account}", @@ -221,7 +225,7 @@ diff --git a/client.js b/client.js Connected: "已连接", Disconnect: "断开", Connect: "连接", -@@ -141,17 +141,44 @@ +@@ -141,17 +317,44 @@ "GitHub addresses must start with https://": "GitHub 地址必须以 https:// 开头", "This DSH cannot load connectors": "这个 DSH 不能加载连接器", "No such connector": "没有这个连接器", @@ -270,7 +274,7 @@ diff --git a/client.js b/client.js // Header "Message {name}": "给 {name} 发消息", Message: "发消息", -@@ -162,8 +162,6 @@ +@@ -162,8 +365,6 @@ "Dismiss new messages": "关闭新消息提示", New: "新", // New chat @@ -279,7 +283,7 @@ diff --git a/client.js b/client.js "Add to group chat": "加入群聊", "Message Bot": "给 Bot 发消息", "To:": "发给:", -@@ -187,9 +187,20 @@ +@@ -187,9 +388,20 @@ Everyone: "所有人", "Every member answers in turn, admin first.": "每位成员轮流回答,管理员先说。", "Members · {count}": "成员 · {count}", @@ -300,7 +304,7 @@ diff --git a/client.js b/client.js "Remove {name} from the group": "把 {name} 移出群聊", "Remove from group": "移出群聊", "Add member": "添加成员", -@@ -247,6 +247,25 @@ +@@ -247,6 +459,25 @@ "This model is not available now. Pick another one.": "这个模型现在不可用,请换一个。", "Every message of this Bot runs on it.": "这个 Bot 的每条消息都用它。", "Applies from the next message.": "从下一条消息开始生效。", @@ -326,7 +330,7 @@ diff --git a/client.js b/client.js Profile: "资料", Label: "标签", "Optional. Shown under the name.": "可选,显示在名字下方。", -@@ -270,6 +270,7 @@ +@@ -270,6 +501,7 @@ "Kept above the other chats.": "排在其他对话上面。", "Search still finds it.": "仍然可以搜索到。", Manage: "管理", @@ -334,7 +338,7 @@ diff --git a/client.js b/client.js // Group chat settings "Leave it empty to name the group after its members.": "留空就按成员命名。", "Every member reads it before speaking in this group.": "每位成员在这个群里发言前都会读到。", -@@ -303,6 +303,8 @@ +@@ -303,6 +535,8 @@ "Harness default": "Harness 默认", Saving: "保存中", "Manage models": "管理模型", @@ -343,7 +347,7 @@ diff --git a/client.js b/client.js // Shapes Whale: "鲸鱼", Star: "星星", -@@ -374,9 +374,10 @@ +@@ -374,9 +608,10 @@ "No group chats yet.": "还没有群聊。", "A group chat lets several Bots work on one thing with you.": "在群聊里,几个 Bot 和你一起做一件事。", "Back to group chats": "返回群聊列表", @@ -356,7 +360,7 @@ diff --git a/client.js b/client.js "Harness settings": "Harness 设置", "Back to Bots": "返回 Bot 列表", "Close settings": "关闭设置", -@@ -385,7 +385,6 @@ +@@ -385,7 +620,6 @@ Pin: "置顶", Pinned: "已置顶", "Mark as Unread": "标为未读", @@ -364,7 +368,7 @@ diff --git a/client.js b/client.js "Group settings": "群设置", "Rename group": "重命名群聊", "Copy conversation ID": "复制对话 ID", -@@ -394,6 +394,8 @@ +@@ -394,6 +628,8 @@ // The DSH Agent's own row and Session list. "DSH Agent" is a product name and // stays in English. "Add DSH Agent": "添加 DSH Agent", @@ -373,7 +377,7 @@ diff --git a/client.js b/client.js "Remove DSH Agent": "移除 DSH Agent", "New session": "新会话", Archive: "归档", -@@ -404,13 +404,13 @@ +@@ -404,13 +640,13 @@ "{n} h": "{n} 小时", "{n} d": "{n} 天", // Plugin updates @@ -393,7 +397,7 @@ diff --git a/client.js b/client.js "You have {version}.": "当前版本 {version}。", "The update did not finish: {reason}": "更新没有完成:{reason}", "What's new": "更新说明", -@@ -420,8 +420,67 @@ +@@ -420,8 +656,67 @@ "DSH update": "DSH 更新", "DSH {version}": "DSH {version}", "Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。", @@ -462,7 +466,7 @@ diff --git a/client.js b/client.js // Command palette Messages: "消息", Commands: "命令", -@@ -446,6 +446,7 @@ +@@ -446,6 +741,7 @@ Reply: "回复", "More message actions": "更多操作", More: "更多", @@ -470,7 +474,7 @@ diff --git a/client.js b/client.js Reactions: "表情", "Message actions": "消息操作", Download: "下载", -@@ -459,6 +459,9 @@ +@@ -459,6 +755,9 @@ "Replies from": "群聊回复:", "Reply from": "收到回复:", "Message from": "收到消息:", @@ -480,7 +484,7 @@ diff --git a/client.js b/client.js "Stopped by": "叫停者:", "{count} messages with": "{count} 条消息,来自", "Routine started": "例行任务开始", -@@ -468,6 +468,8 @@ +@@ -468,6 +767,8 @@ "Working out a plan": "理一下思路", "Sorting out the details": "梳理细节", "On it": "在做了", @@ -489,7 +493,7 @@ diff --git a/client.js b/client.js "Busy with it": "正在处理", "Moving ahead": "推进中", "Taking care of it": "处理中", -@@ -495,7 +495,6 @@ +@@ -495,7 +796,6 @@ "Looking back through the chat": "翻看聊天记录", "{name} looked up its own chat": "{name} 查看了自己的对话", "{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」", @@ -497,7 +501,7 @@ diff --git a/client.js b/client.js "{name} didn't answer in time": "{name} 没有及时回应", "{name} paused for your approval": "{name} 停下来等你批准", "{name} started on it": "{name} 开始做了", -@@ -506,12 +506,30 @@ +@@ -506,12 +806,30 @@ "Asked to make the tool call it wrote as text": "工具调用被写成了文字,已让它重新调用", "A Bot": "一个 Bot", "{name} is waiting for your approval": "{name} 等你批准", @@ -529,7 +533,7 @@ diff --git a/client.js b/client.js "Couldn't message {name}": "没能给 {name} 发消息", Messaged: "已发消息给", "Couldn't stop {name}": "没能叫停 {name}", -@@ -533,13 +533,14 @@ +@@ -533,13 +851,14 @@ Selected: "已选", Skip: "跳过", Submit: "提交", @@ -545,7 +549,7 @@ diff --git a/client.js b/client.js "No Bot": "没有 Bot", "Paste or type the key": "粘贴或输入密钥", "Only {name}": "只给 {name}", -@@ -582,7 +582,7 @@ +@@ -582,7 +901,7 @@ "NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN", "What it is for": "用途", Value: "值", @@ -554,7 +558,7 @@ diff --git a/client.js b/client.js "Save key": "保存密钥", "Keys your Bots asked for. Bots never see the values: they use them as ": "你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ", " in their shell, and every value is replaced by ": " 使用,而且发给模型之前,每个值都会被替换成 ", -@@ -747,11 +747,11 @@ +@@ -747,11 +1066,11 @@ "Compared with the {count} days before": "与再往前 {count} 天相比", "{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%", // About @@ -569,7 +573,7 @@ diff --git a/client.js b/client.js Updates: "更新", "Check for updates": "检查更新", "Checking…": "正在检查…", -@@ -761,6 +761,13 @@ +@@ -761,6 +1080,13 @@ "Update checks are off.": "已关闭更新检查。", "Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。", "Updates are not offered for this install.": "这种安装方式不提供更新。", @@ -583,7 +587,7 @@ diff --git a/client.js b/client.js Links: "链接", "Source code": "源代码", "Release notes": "发布说明", -@@ -776,7 +776,7 @@ +@@ -776,7 +1102,7 @@ Show: "查看", Hide: "收起", "GitHub issue": "GitHub issue", @@ -592,7 +596,7 @@ diff --git a/client.js b/client.js "Report on GitHub": "在 GitHub 上报告", "Copied the diagnostics.": "已复制诊断信息。", "Could not copy. Select the text below and copy it.": "无法复制。请选中下面的文字手动复制。", -@@ -788,7 +788,7 @@ +@@ -788,7 +1114,7 @@ "Recent errors": "最近的错误", "Recent errors · {count}": "最近的错误 · {count}", "No errors recorded.": "没有错误记录。", @@ -601,7 +605,7 @@ diff --git a/client.js b/client.js "Model call": "模型调用", "This window": "这个窗口", "Error log": "错误日志", -@@ -821,6 +821,14 @@ +@@ -821,6 +1147,14 @@ "Add at least one more Bot": "再至少添加一个 Bot", Members: "成员", // Connectors and scheduled tasks @@ -616,7 +620,7 @@ diff --git a/client.js b/client.js "Scheduled tasks": "定时任务", "Every day": "每天", "Every {count} days": "每 {count} 天", -@@ -834,7 +834,15 @@ +@@ -834,7 +1168,15 @@ "Every weekend at {time}": "每个周末 {time}", "On the schedule {expression}": "按计划 {expression}", "Next: {time}": "下次 {time}", @@ -632,7 +636,7 @@ diff --git a/client.js b/client.js "in {name}": "在 {name}", "Click again to delete": "再点一次删除", "Delete task": "删除任务", -@@ -856,8 +856,8 @@ +@@ -856,8 +1198,8 @@ Unit: "单位", "Create task": "创建任务", "Turn on Automation tasks in DSH": "在 DSH 里打开「自动化任务」", @@ -643,7 +647,7 @@ diff --git a/client.js b/client.js "New task": "新建任务", "No scheduled tasks yet.": "还没有定时任务。", "No scheduled tasks yet. Add one": "还没有定时任务,去添加", -@@ -873,6 +873,12 @@ +@@ -873,6 +1215,12 @@ "Repeat every 1 minute to 7 days": "重复间隔要在 1 分钟到 7 天之间", "Pick when it runs": "选择什么时候运行", "No such task": "没有这个任务", @@ -656,7 +660,7 @@ diff --git a/client.js b/client.js "Working folder": "工作文件夹", "Working folder: {folder}": "工作文件夹:{folder}", Folder: "文件夹", -@@ -902,6 +902,8 @@ +@@ -902,6 +1250,8 @@ [/^A key can have at most (\d+) characters$/, "密钥最多 $1 个字符"], [/^(.+) is not saved yet$/, "$1 还没有保存"], [/^The Bot team in (.+) is open in another dsh \((.+)\)\. This window shows the team but cannot change it; close the other one and reload\.$/, "$1 里的 Bot 团队已在另一个 dsh 中打开($2)。这个窗口只能查看,不能修改;关掉另一个后刷新。"], @@ -665,7 +669,7 @@ diff --git a/client.js b/client.js [/^HTTP (\d+)$/, "请求失败(HTTP $1)"], [/^GitHub answered (\d+)$/, "GitHub 返回了 $1"], [/^Keep the name under (\d+) characters$/, "名字不能超过 $1 个字"], -@@ -930,6 +930,20 @@ +@@ -930,6 +1280,20 @@ "Unknown Bot": "找不到这个 Bot", "Unknown conversation": "找不到这个对话", "A group chat needs at least two Bots": "群聊至少要两个 Bot", @@ -686,7 +690,7 @@ diff --git a/client.js b/client.js "The admin must be a member of the group": "管理员必须是群成员", "Unknown group chat": "找不到这个群聊", "Hand the admin role to another member instead of leaving the group without one": "请把管理员交给其他成员,不要让群聊没有管理员", -@@ -951,7 +951,9 @@ +@@ -951,7 +1315,9 @@ "The model wrote nothing": "模型没有输出内容", "Write the entry to save.": "请写下要保存的内容。", "This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口", @@ -697,7 +701,7 @@ diff --git a/client.js b/client.js }; Object.assign(ZH, HOST_ZH); var fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text; -@@ -1016,6 +1016,14 @@ +@@ -1016,6 +1382,14 @@ entries.push({ time: now, level: "error", text, ...code ? { code } : {} }); if (entries.length > MAX) entries.splice(0, entries.length - MAX); } @@ -712,7 +716,7 @@ diff --git a/client.js b/client.js var quietly = (where) => (error) => noteClientError(where, error); var clearClientErrors = () => { entries.length = 0; -@@ -1042,12 +1042,17 @@ +@@ -1042,12 +1416,17 @@ }; function failureLabel(code, message) { if (FAILURES[code]) return t(FAILURES[code]); @@ -733,7 +737,7 @@ diff --git a/client.js b/client.js function issueUrl(base, { what = "", diagnostics }) { const text = what.trim().slice(0, WHAT_MAX); const title = text.split("\n")[0].slice(0, 80); -@@ -1066,10 +1066,16 @@ +@@ -1066,10 +1445,16 @@ var call = async (endpoint, payload, signal) => { let response; try { @@ -751,7 +755,7 @@ diff --git a/client.js b/client.js body: JSON.stringify({ endpoint, payload: payload ?? null }), signal }); -@@ -1087,7 +1087,7 @@ +@@ -1087,7 +1472,7 @@ if (!result.ok) { const code = result.error?.code ?? "bot/error"; const message = hostText(result.error?.message ?? "Request failed"); @@ -760,7 +764,7 @@ diff --git a/client.js b/client.js if (!REFUSALS.has(code)) { noteClientError(endpoint, Object.assign(new Error(result.error?.message ?? "Request failed"), { code })); markNoted(error); -@@ -1097,6 +1097,388 @@ +@@ -1097,6 +1482,388 @@ return result.value; }; @@ -1149,7 +1153,7 @@ diff --git a/client.js b/client.js // src/client/inks.js var INKS = { deepseek: ["#4D6BFE", "#6A84FF"], -@@ -1177,15 +1177,92 @@ +@@ -1177,15 +1944,92 @@ value = Math.imul(value ^ value >>> 13, 3266489909); return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length]; } @@ -1244,7 +1248,7 @@ diff --git a/client.js b/client.js var SHAPES = {}; for (const [id, character] of Object.entries(CHARACTERS)) { const layers = [ -@@ -1206,8 +1206,8 @@ +@@ -1206,8 +2050,8 @@ return ""; } } @@ -1255,7 +1259,7 @@ diff --git a/client.js b/client.js var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`; var holes = (def, eyes) => `${eyes}`; var MASK_ID = "%MASK%"; -@@ -1242,163 +1242,17 @@ +@@ -1242,163 +2086,17 @@ } var DEFAULT_MASK = maskOf(FALLBACK_SHAPE); @@ -1428,7 +1432,7 @@ diff --git a/client.js b/client.js // src/shared/mentions.js var ALL_WORDS = ["all", "everyone", "所有人", "全体"]; var NAME_GOES_ON = /^(?:[a-z0-9_\-/\\]|\.[a-z0-9_-])/i; -@@ -1445,14 +1445,36 @@ +@@ -1445,14 +2143,36 @@ } return spans; } @@ -1466,7 +1470,7 @@ diff --git a/client.js b/client.js if (!span.bot || span.bot.id === selfId) continue; out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; cursor = span.end; -@@ -1463,6 +1463,20 @@ +@@ -1463,6 +2183,20 @@ const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null; return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null; } @@ -1487,7 +1491,7 @@ diff --git a/client.js b/client.js function clock(ms) { return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); } -@@ -1483,6 +1483,15 @@ +@@ -1483,6 +2217,15 @@ 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; @@ -1503,7 +1507,7 @@ diff --git a/client.js b/client.js function reminderPrompts(text) { const one = /^reminder_prompt_json: (.*)$/m.exec(text); const batch = /^reminders_json: (.*)$/m.exec(text); -@@ -1494,6 +1494,13 @@ +@@ -1494,6 +2237,13 @@ } } var stripHeader = (text) => text.replace(HEADER, ""); @@ -1517,7 +1521,7 @@ diff --git a/client.js b/client.js function latestMemoryChange(news, ids) { const at = (id) => news?.[id]?.seen ? news[id].at ?? 0 : 0; return ids.reduce((best, id) => at(id) > at(best) ? id : best, ids[0] ?? null); -@@ -1506,25 +1506,35 @@ +@@ -1506,25 +2256,35 @@ paragraph = []; }; for (const raw of String(text ?? "").split("\n")) { @@ -1557,7 +1561,7 @@ diff --git a/client.js b/client.js var historyLoading = (snapshot) => snapshot?.openState === "cold" || snapshot?.openState === "loading"; function toolArgs(root) { if (typeof root.args?.text === "function") { -@@ -1597,14 +1597,21 @@ +@@ -1597,14 +2357,21 @@ } return { count, peerIds }; } @@ -1582,7 +1586,7 @@ diff --git a/client.js b/client.js } return ""; } -@@ -1624,9 +1624,917 @@ +@@ -1624,9 +2391,917 @@ const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } @@ -2501,7 +2505,7 @@ diff --git a/client.js b/client.js var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; function springEasing(stiffness, damping) { const w0 = Math.sqrt(stiffness); -@@ -1659,12 +1659,12 @@ +@@ -1659,12 +3334,12 @@ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); last = now; const steps = Math.ceil(dt * 240); @@ -2517,7 +2521,7 @@ diff --git a/client.js b/client.js } if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { value.x = value.to; -@@ -1699,10 +1699,10 @@ +@@ -1699,10 +3374,10 @@ } var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { @@ -2531,7 +2535,7 @@ diff --git a/client.js b/client.js if (own.current === null) { own.current = { key: void 0, -@@ -1736,7 +1736,7 @@ +@@ -1736,7 +3411,7 @@ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); container.dataset.glide = ""; }; @@ -2540,7 +2544,7 @@ diff --git a/client.js b/client.js const state = own.current; const node = box.current; if (node === state.node && key === state.key) return; -@@ -1750,7 +1750,7 @@ +@@ -1750,7 +3425,7 @@ } place(fresh); }); @@ -2549,7 +2553,7 @@ diff --git a/client.js b/client.js own.current.observer?.disconnect(); own.current.springs.stop(); }, []); -@@ -1758,16 +1758,16 @@ +@@ -1758,16 +3433,16 @@ } function Segmented({ label, value, className, children }) { const glide = useGlide(value); @@ -2570,7 +2574,7 @@ diff --git a/client.js b/client.js const container = list.current; if (!container) return; const next = /* @__PURE__ */ new Map(); -@@ -1788,8 +1788,8 @@ +@@ -1788,8 +3463,8 @@ }, [key]); } function useMorph(box, key) { @@ -2581,7 +2585,7 @@ diff --git a/client.js b/client.js const node = box.current; const state = last.current; if (!node) { -@@ -1817,8 +1817,8 @@ +@@ -1817,8 +3492,8 @@ }); } function useLinger(value, ms) { @@ -2592,7 +2596,7 @@ diff --git a/client.js b/client.js if (value) { setKept(value); return void 0; -@@ -1838,6 +1838,7 @@ +@@ -1838,6 +3513,7 @@ .bt-mark{display:inline-block;flex:none;position:relative;line-height:0} .bt-mark svg{width:100%;height:100%;overflow:visible} .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} @@ -2600,7 +2604,7 @@ diff --git a/client.js b/client.js .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} .bt-mark .bt-b{transform-origin:50% 100%} .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} -@@ -1856,6 +1856,14 @@ +@@ -1856,6 +3532,14 @@ @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} @@ -2615,7 +2619,7 @@ diff --git a/client.js b/client.js @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -@@ -1933,15 +1933,23 @@ +@@ -1933,15 +3617,23 @@ .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} @@ -2641,7 +2645,7 @@ diff --git a/client.js b/client.js .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} -@@ -1950,27 +1950,32 @@ +@@ -1950,27 +3642,32 @@ .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -2678,7 +2682,7 @@ diff --git a/client.js b/client.js .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} -@@ -1992,6 +1992,7 @@ +@@ -1992,6 +3689,7 @@ .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} @@ -2686,7 +2690,7 @@ diff --git a/client.js b/client.js .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} .bt-bubble [class*="_markdown"]>*+*{margin-top:10px} -@@ -2085,6 +2085,7 @@ +@@ -2085,6 +3783,7 @@ @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} @@ -2694,7 +2698,7 @@ diff --git a/client.js b/client.js .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} .bt-to-label{color:var(--bt-ink-2)} .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} -@@ -2098,7 +2098,9 @@ +@@ -2098,7 +3797,9 @@ .bt-option[aria-selected=true]{background:var(--bt-active)} .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -2705,7 +2709,7 @@ diff --git a/client.js b/client.js .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} .bt-nc-spacer{flex:1} -@@ -2127,6 +2127,10 @@ +@@ -2127,6 +3828,10 @@ .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} .bt-send:disabled{opacity:.35;cursor:default} .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} @@ -2716,7 +2720,7 @@ diff --git a/client.js b/client.js .bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} .bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} -@@ -2163,6 +2163,8 @@ +@@ -2163,6 +3868,8 @@ .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -2725,7 +2729,7 @@ diff --git a/client.js b/client.js .bt-drawer-name:hover{background:var(--bt-hover)} .bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} .bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} -@@ -2255,6 +2255,7 @@ +@@ -2255,6 +3962,7 @@ .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} .bt-settings-shell{margin-top:auto} @@ -2733,7 +2737,7 @@ diff --git a/client.js b/client.js /* Usage while another page is open: out of the flow at Usage's own width, so it keeps its layout, and not rendered. */ .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} -@@ -2288,6 +2288,8 @@ +@@ -2288,6 +3996,8 @@ .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} @@ -2742,7 +2746,7 @@ diff --git a/client.js b/client.js .bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0} .bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))} .bt-set-control{flex:none;display:flex;align-items:center;gap:6px} -@@ -2308,8 +2308,17 @@ +@@ -2308,8 +4018,17 @@ .bt-connector-actions{flex:none;display:flex;gap:6px} .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-connector-form-row{display:flex;align-items:center;gap:6px} @@ -2760,7 +2764,7 @@ diff --git a/client.js b/client.js /* Shell chrome reshaped for the team view. Selectors use stable data attributes where the shell offers them and CSS-module name suffixes elsewhere. */ -@@ -2427,7 +2427,8 @@ +@@ -2427,7 +4146,8 @@ .bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} .bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} @media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} @@ -2770,7 +2774,7 @@ diff --git a/client.js b/client.js .bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} .bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} /* Popover keyframes move translate and scale, which compose with a transform the popover -@@ -2599,6 +2599,14 @@ +@@ -2599,6 +4319,14 @@ .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} .bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} .bt-think summary::-webkit-details-marker{display:none} @@ -2785,7 +2789,7 @@ diff --git a/client.js b/client.js .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} .bt-event>span{padding:0 2px} -@@ -2612,7 +2612,6 @@ +@@ -2612,7 +4340,6 @@ .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} [class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} @@ -2793,7 +2797,7 @@ diff --git a/client.js b/client.js .bt-group-replies{display:contents} /* A Bot chat is flat in every work-details mode, without changing the user's mode (it is a DSH-wide setting): step groups lose their header and stay open, and what a -@@ -2622,21 +2622,28 @@ +@@ -2622,21 +4349,28 @@ [data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} @@ -2830,7 +2834,7 @@ diff --git a/client.js b/client.js [data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} [data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} [data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} -@@ -2648,6 +2648,9 @@ +@@ -2648,6 +4382,9 @@ [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} .bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards} .bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards} @@ -2840,7 +2844,7 @@ diff --git a/client.js b/client.js .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} -@@ -2679,7 +2679,8 @@ +@@ -2679,7 +4416,8 @@ @keyframes bt-fade-in{from{opacity:0}to{opacity:1}} @keyframes bt-fade-out{from{opacity:1}to{opacity:0}} [data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} @@ -2850,7 +2854,7 @@ diff --git a/client.js b/client.js [data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} [class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} [class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} -@@ -2691,6 +2691,8 @@ +@@ -2691,6 +4429,8 @@ [data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} [data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} [data-composer-placeholder]{font-size:0!important;line-height:0!important} @@ -2859,7 +2863,7 @@ diff --git a/client.js b/client.js [data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} /* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ [data-composer-card] button[class*="_add"]{display:none!important} -@@ -2722,6 +2722,7 @@ +@@ -2722,6 +4462,7 @@ .bt-mic:active,.bt-voice:active{scale:.9} [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} body[data-bt-room] .bt-voice{display:none!important} @@ -2867,7 +2871,7 @@ diff --git a/client.js b/client.js body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} .bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} [data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} -@@ -2776,6 +2776,7 @@ +@@ -2776,6 +4517,7 @@ .bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} .bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} .bt-urow{display:flex;justify-content:flex-end} @@ -2875,7 +2879,7 @@ diff --git a/client.js b/client.js .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} .bt-ububble p{margin:0;line-height:22px} -@@ -2855,226 +2855,204 @@ +@@ -2855,226 +4597,204 @@ }; } @@ -3257,7 +3261,7 @@ diff --git a/client.js b/client.js ); } var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); -@@ -3093,14 +3093,14 @@ +@@ -3093,14 +4813,14 @@ var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); function PinMarkIcon() { @@ -3276,7 +3280,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -3112,21 +3112,48 @@ +@@ -3112,21 +4832,48 @@ var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); @@ -3328,7 +3332,7 @@ diff --git a/client.js b/client.js if (count === 0) return void 0; const onKey = (event) => { if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -@@ -3146,10 +3146,10 @@ +@@ -3146,10 +4893,10 @@ }, [count, ownRef]); } function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { @@ -3343,7 +3347,7 @@ diff --git a/client.js b/client.js const options = optionsOf(question); const multi = question.multiSelect === true; const custom = own.trim(); -@@ -3167,7 +3167,7 @@ +@@ -3167,7 +4914,7 @@ }; useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; @@ -3352,7 +3356,7 @@ diff --git a/client.js b/client.js ref: ownRef, rows: 1, value: own, -@@ -3186,7 +3186,7 @@ +@@ -3186,7 +4933,7 @@ }) : null; const rows = options.map((option, optionIndex) => { const selected = multi && picked.includes(option.label); @@ -3361,7 +3365,7 @@ diff --git a/client.js b/client.js "button", { key: option.label, -@@ -3196,36 +3196,36 @@ +@@ -3196,36 +4943,36 @@ "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), onClick: () => choose(option.label) }, @@ -3411,7 +3415,7 @@ diff --git a/client.js b/client.js "form", { className: raised ? "bt-q bt-q-raised" : "bt-q", -@@ -3235,24 +3235,24 @@ +@@ -3235,24 +4982,24 @@ submit(); } }, @@ -3446,7 +3450,7 @@ diff --git a/client.js b/client.js ) : null, foot ); -@@ -3262,29 +3262,29 @@ +@@ -3262,29 +5009,29 @@ const prompt = question?.question ?? ""; const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; if (chosen.length === 0) { @@ -3487,7 +3491,7 @@ diff --git a/client.js b/client.js ); })) ); -@@ -3292,8 +3292,8 @@ +@@ -3292,8 +5039,8 @@ function QuestionCard({ matched }) { const pending = matched; const questions = pending.questions ?? []; @@ -3498,7 +3502,7 @@ diff --git a/client.js b/client.js const question = questions[index]; if (question === void 0) return null; const onAnswer = (answer) => { -@@ -3305,10 +3305,10 @@ +@@ -3305,10 +5052,10 @@ } pending.answer({ answers: next }); }; @@ -3511,7 +3515,7 @@ diff --git a/client.js b/client.js if (answered === question.id) return null; const onAnswer = (answer) => { setAnswered(question.id); -@@ -3318,10 +3318,10 @@ +@@ -3318,10 +5065,10 @@ setAnswered(question.id); void actions.dismissQuestion(sessionId); }; @@ -3524,7 +3528,7 @@ diff --git a/client.js b/client.js ); } function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -@@ -3329,16 +3329,16 @@ +@@ -3329,16 +5076,16 @@ const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; const loading = useSession?.(historyLoading) === true; if (question === void 0 || question.callId && (drawn || loading)) return null; @@ -3544,7 +3548,7 @@ diff --git a/client.js b/client.js key: question.id, question, answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -@@ -3346,37 +3346,37 @@ +@@ -3346,37 +5093,37 @@ } // src/client/secret-card.js @@ -3594,7 +3598,7 @@ diff --git a/client.js b/client.js className: "bt-secret-input", type: MASK_BY_CSS ? "text" : "password", "data-masked": MASK_BY_CSS ? "" : void 0, -@@ -3401,12 +3401,12 @@ +@@ -3401,12 +5148,12 @@ }); } function ScopeSwitch({ value, onChange, bot }) { @@ -3611,7 +3615,7 @@ diff --git a/client.js b/client.js key: id, type: "button", role: "radio", -@@ -3417,26 +3417,26 @@ +@@ -3417,26 +5164,26 @@ ); } function SecretHead({ bot, title, detail, name, onClose }) { @@ -3650,7 +3654,7 @@ diff --git a/client.js b/client.js const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); const run = (promise) => { setBusy(true); -@@ -3452,73 +3452,73 @@ +@@ -3452,73 +5199,73 @@ if (!busy) void run(actions.secretAllow(request.id)); }; const filling = request.kind === "fill" || replacing; @@ -3747,7 +3751,7 @@ diff --git a/client.js b/client.js } return null; } -@@ -3531,44 +3531,46 @@ +@@ -3531,44 +5278,46 @@ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); if (waiting.length === 0) return null; @@ -3809,7 +3813,7 @@ diff --git a/client.js b/client.js const long = value.trim().length >= MIN_VALUE; const save = () => { if (!long) return; -@@ -3583,66 +3583,66 @@ +@@ -3583,66 +5332,66 @@ setValue(next); setReplaced(false); }; @@ -3902,7 +3906,7 @@ diff --git a/client.js b/client.js const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; const save = () => { if (!ready) return; -@@ -3653,43 +3653,43 @@ +@@ -3653,43 +5402,43 @@ } }); }; @@ -3964,7 +3968,7 @@ diff --git a/client.js b/client.js ]; } var SECRET_CSS = ` -@@ -3756,6 +3756,144 @@ +@@ -3756,6 +5505,144 @@ @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} `; @@ -4109,7 +4113,7 @@ diff --git a/client.js b/client.js // src/client/turn-reasoning.js function turnReasoning(steps, current) { const members = steps.length ? steps : [current]; -@@ -3767,7 +3767,7 @@ +@@ -3767,7 +5654,7 @@ var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); // src/client/cells.js @@ -4118,7 +4122,7 @@ diff --git a/client.js b/client.js var import_react_dom = require("react-dom"); var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); -@@ -3780,7 +3780,7 @@ +@@ -3780,7 +5667,7 @@ actions.openSession(target); }; const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; @@ -4127,7 +4131,7 @@ diff --git a/client.js b/client.js "div", { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, texts2.map((text, index) => { -@@ -3788,21 +3788,21 @@ +@@ -3788,21 +5675,21 @@ const picked = key ? reactions?.[index] ?? [] : []; const last = index === texts2.length - 1; const live = streaming && last; @@ -4155,7 +4159,7 @@ diff --git a/client.js b/client.js key: emoji, type: "button", className: "bt-react", -@@ -3813,9 +3813,22 @@ +@@ -3813,9 +5700,22 @@ }) ); } @@ -4179,7 +4183,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (refs.some((ref) => ref.current?.contains(event.target))) return; -@@ -3836,11 +3836,11 @@ +@@ -3836,11 +5736,11 @@ }, [open]); } function MessageTools({ text, name, sessionId, react, actions }) { @@ -4196,7 +4200,7 @@ diff --git a/client.js b/client.js const close = () => setPanel(null); useDismiss(panel !== null, close, [tools, pop]); const open = (kind, event) => { -@@ -3865,25 +3865,25 @@ +@@ -3865,25 +5765,25 @@ link.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); }; @@ -4231,7 +4235,7 @@ diff --git a/client.js b/client.js "div", { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, item(t("Copy"), CopyIcon, copy2), -@@ -3892,11 +3892,12 @@ +@@ -3892,11 +5792,12 @@ ); } function ThinkingRow({ text }) { @@ -4247,7 +4251,7 @@ diff --git a/client.js b/client.js ); } function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,19 +3904,33 @@ +@@ -3904,19 +5805,33 @@ const location2 = node.location; const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; const seq = node.data.finalNode?.seq; @@ -4283,7 +4287,7 @@ diff --git a/client.js b/client.js const running = data.status === "running"; useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); if (hidden) return null; -@@ -3924,13 +3924,14 @@ +@@ -3924,13 +5839,14 @@ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; if (texts2.length === 0 && !reasoning && relays.length === 0) return null; const selfId = roster.byId[sessionId]?.id ?? sessionId; @@ -4301,7 +4305,7 @@ diff --git a/client.js b/client.js key: relay.seq, prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), bots: [sender], -@@ -3940,61 +3940,230 @@ +@@ -3940,61 +5856,230 @@ whole: true }); }), @@ -4555,7 +4559,7 @@ diff --git a/client.js b/client.js ); } function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -@@ -4005,14 +4005,14 @@ +@@ -4005,14 +6090,14 @@ const target = other && other.id !== self?.id ? other : self; if (!target) return null; const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); @@ -4573,7 +4577,7 @@ diff --git a/client.js b/client.js } var scrollParent = (node) => { for (let at = node.parentElement; at; at = at.parentElement) { -@@ -4021,11 +4021,11 @@ +@@ -4021,11 +6106,11 @@ return null; }; function EarlierParts({ sessionId, roster, actions }) { @@ -4590,7 +4594,7 @@ diff --git a/client.js b/client.js const load = (cursor) => { if (busy.current) return; busy.current = true; -@@ -4041,19 +4041,19 @@ +@@ -4041,19 +6126,19 @@ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); }); }; @@ -4613,7 +4617,7 @@ diff --git a/client.js b/client.js const node = top.current; if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; const observer = new IntersectionObserver((entries2) => { -@@ -4069,35 +4069,35 @@ +@@ -4069,35 +6154,35 @@ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); } @@ -4660,7 +4664,7 @@ diff --git a/client.js b/client.js if (selfId && other.id) actions.openExchange(selfId, other.id); }, whole: true }); } -@@ -4105,7 +4105,7 @@ +@@ -4105,7 +6190,7 @@ return null; } }), @@ -4669,7 +4673,7 @@ diff --git a/client.js b/client.js ); } function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -@@ -4116,7 +4116,7 @@ +@@ -4116,7 +6201,7 @@ const deferred = source.kind === "bot" && source.deferred === true; const relayed = isRelayContext(data); const turn = node.location?.turn?.turn ?? data.turn; @@ -4678,7 +4682,7 @@ diff --git a/client.js b/client.js if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); }, [relayed, sessionId, turn, source.senderSessionId]); const order = useSessions((list) => { -@@ -4135,23 +4135,31 @@ +@@ -4135,23 +6220,31 @@ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); const body = triggerBody(); @@ -4717,7 +4721,7 @@ diff --git a/client.js b/client.js const run = relayed ? commRunOf( order.split(",").filter(Boolean).map(Number), Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -@@ -4165,7 +4165,7 @@ +@@ -4165,7 +6258,7 @@ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); } @@ -4726,7 +4730,7 @@ diff --git a/client.js b/client.js prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), bots, onOpen: (bot) => actions.openExchange(selfId, bot.id), -@@ -4174,24 +4174,24 @@ +@@ -4174,24 +6267,24 @@ } if (source.kind === "schedule") { const prompts = reminderPrompts(contentText(data.content)); @@ -4756,7 +4760,7 @@ diff --git a/client.js b/client.js actions.noteTurnTime(sessionId, turn, "first", start); }, [sessionId, turn, start]); const visit = useUi((state) => state.visits?.[sessionId]); -@@ -4199,11 +4199,63 @@ +@@ -4199,11 +6292,63 @@ const own = state[sessionId]?.[turn]; return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; }); @@ -4821,7 +4825,7 @@ diff --git a/client.js b/client.js var SEPARATOR_GAP_MS = 30 * 60 * 1e3; function clockLabel(time, now = Date.now()) { const date = new Date(time); -@@ -4233,10 +4233,19 @@ +@@ -4233,10 +6378,19 @@ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; } var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; @@ -4842,7 +4846,7 @@ diff --git a/client.js b/client.js actions.noteTurnTime(sessionId, turn, "end", time); }, [sessionId, turn, time]); const label = useTurnClock((state) => { -@@ -4250,15 +4250,15 @@ +@@ -4250,15 +6404,15 @@ return start !== void 0 && start > visit.seen && start <= visit.entered; }); if (!label && !fresh) return null; @@ -4863,7 +4867,7 @@ diff --git a/client.js b/client.js } var PHRASES = { thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -@@ -4320,19 +4320,22 @@ +@@ -4320,19 +6474,22 @@ } function workPhrase(tool) { if (!tool) return t("Thinking about it"); @@ -4891,7 +4895,7 @@ diff --git a/client.js b/client.js if (done) return void 0; let stop = false; let timer; -@@ -4341,10 +4341,10 @@ +@@ -4341,10 +6498,10 @@ const result = await actions.roomProgress(sessionId); if (stop) return; const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; @@ -4904,7 +4908,7 @@ diff --git a/client.js b/client.js } timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); }; -@@ -4362,71 +4362,89 @@ +@@ -4362,71 +6519,89 @@ replies = []; } } @@ -5025,7 +5029,7 @@ diff --git a/client.js b/client.js }) ); } -@@ -4435,52 +4435,52 @@ +@@ -4435,52 +6610,52 @@ function callTree(root) { const calls = []; const seen = /* @__PURE__ */ new Set(); @@ -5094,7 +5098,7 @@ diff --git a/client.js b/client.js case "create_group": case "update_group": case "delete_group": -@@ -4492,64 +4492,80 @@ +@@ -4492,64 +6667,80 @@ post_to_group: ["Posted in", "Couldn't post in the group"] }[tool]; if (!done) return null; @@ -5193,7 +5197,7 @@ diff --git a/client.js b/client.js var import_react_dom2 = require("react-dom"); var PHRASE_HOLD_MS = 800; var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); -@@ -4559,9 +4559,71 @@ +@@ -4559,9 +6750,71 @@ const hours = Math.floor(minutes / 60); return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; } @@ -5268,7 +5272,7 @@ diff --git a/client.js b/client.js if (!active) { setHost(null); return void 0; -@@ -4569,7 +4569,7 @@ +@@ -4569,7 +6822,7 @@ let frame = 0; const find = () => { frame = 0; @@ -5277,7 +5281,7 @@ diff --git a/client.js b/client.js setHost((prev) => prev === node ? prev : node); }; const observer = new MutationObserver(() => { -@@ -4581,13 +4581,13 @@ +@@ -4581,13 +6834,13 @@ cancelAnimationFrame(frame); observer.disconnect(); }; @@ -5294,7 +5298,7 @@ diff --git a/client.js b/client.js const seen = /* @__PURE__ */ new WeakSet(); const started = Date.now(); let column = null; -@@ -4638,21 +4638,31 @@ +@@ -4638,21 +6891,31 @@ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; @@ -5333,7 +5337,7 @@ diff --git a/client.js b/client.js if (wantKey === null || shown?.key === wantKey) return void 0; const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -@@ -4663,24 +4663,29 @@ +@@ -4663,24 +6926,29 @@ const timer = setTimeout(make, wait); return () => clearTimeout(timer); }, [wantKey, shown]); @@ -5374,7 +5378,7 @@ diff --git a/client.js b/client.js if (phase !== null) return; endedAt.current = Date.now(); setShown(null); -@@ -4691,31 +4691,37 @@ +@@ -4691,31 +6959,37 @@ const delta = before - scroller.scrollTop; if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); }, [phase]); @@ -5423,7 +5427,7 @@ diff --git a/client.js b/client.js ) ) ), host); -@@ -4731,49 +4731,1478 @@ +@@ -4731,49 +7005,1478 @@ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } @@ -6914,7 +6918,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4785,55 +4785,131 @@ +@@ -4785,55 +8488,131 @@ onClick: () => actions.openSession(id), onContextMenu }, @@ -7061,7 +7065,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ +@@ -4841,20 +8620,20 @@ actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -7090,7 +7094,7 @@ diff --git a/client.js b/client.js if (!renaming) return; input.current?.focus(); input.current?.select(); -@@ -4875,10 +4875,10 @@ +@@ -4875,10 +8654,10 @@ } })(); }; @@ -7103,7 +7107,7 @@ diff --git a/client.js b/client.js ref: input, defaultValue: session.title, "aria-label": t("Rename"), -@@ -4891,7 +4891,7 @@ +@@ -4891,7 +8670,7 @@ }) ); } @@ -7112,7 +7116,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4905,8 +4905,8 @@ +@@ -4905,8 +8684,8 @@ actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -7123,7 +7127,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ +@@ -4941,10 +8720,10 @@ const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -7136,7 +7140,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +4959,30 @@ +@@ -4959,30 +8738,30 @@ actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -7178,7 +7182,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +4996,8 @@ +@@ -4996,8 +8775,8 @@ } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -7189,7 +7193,7 @@ diff --git a/client.js b/client.js const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +5014,44 @@ +@@ -5014,9 +8793,44 @@ tops.current = { layout, map: next }; }, [layout, key]); } @@ -7235,7 +7239,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); -@@ -5025,17 +5025,26 @@ +@@ -5025,17 +8839,26 @@ for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); @@ -7266,7 +7270,7 @@ diff --git a/client.js b/client.js 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 }; -@@ -5045,39 +5045,42 @@ +@@ -5045,39 +8868,42 @@ event.preventDefault(); actions.openPalette(event.key); }; @@ -7329,7 +7333,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -@@ -5116,14 +5116,14 @@ +@@ -5116,14 +8942,14 @@ setDshError(""); void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); }; @@ -7349,7 +7353,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-round bt-update", -@@ -5133,21 +5133,21 @@ +@@ -5133,21 +8959,21 @@ "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle @@ -7382,7 +7386,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: phase === "installing", -@@ -5155,17 +5155,17 @@ +@@ -5155,17 +8981,17 @@ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, @@ -7407,7 +7411,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ +@@ -5176,61 +9002,25 @@ ), document.body) : null ); } @@ -7479,7 +7483,7 @@ diff --git a/client.js b/client.js ); } var openShellSettings = null; -@@ -5259,6 +5259,18 @@ +@@ -5259,6 +9049,18 @@ else openHarnessSettings(); }); } @@ -7498,7 +7502,7 @@ diff --git a/client.js b/client.js function openThroughMenu(trigger) { const before = new Set(document.querySelectorAll('[role="menu"]')); trigger.click(); -@@ -5288,32 +5288,32 @@ +@@ -5288,32 +9090,32 @@ })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { @@ -7539,7 +7543,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -@@ -5341,30 +5341,32 @@ +@@ -5341,30 +9143,32 @@ const groups = [ [ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -7584,7 +7588,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -5372,7 +5372,7 @@ +@@ -5372,7 +9176,7 @@ setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -7593,7 +7597,7 @@ diff --git a/client.js b/client.js )) ), document.body) : null ); -@@ -5382,8 +5382,8 @@ +@@ -5382,8 +9186,8 @@ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); @@ -7604,7 +7608,7 @@ diff --git a/client.js b/client.js if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +5397,40 @@ +@@ -5397,40 +9201,40 @@ return void 0; } const last = lastBot.current; @@ -7655,7 +7659,7 @@ diff --git a/client.js b/client.js } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5491,18 +5491,28 @@ +@@ -5491,18 +9295,28 @@ .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}} @@ -7688,7 +7692,7 @@ diff --git a/client.js b/client.js const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; -@@ -5512,16 +5512,16 @@ +@@ -5512,16 +9326,16 @@ return shown; } function MemoryNotice({ shown }) { @@ -7710,7 +7714,7 @@ diff --git a/client.js b/client.js ); } function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { -@@ -5537,7 +5537,7 @@ +@@ -5537,7 +9351,7 @@ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); @@ -7719,7 +7723,7 @@ diff --git a/client.js b/client.js if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); -@@ -5548,52 +5548,40 @@ +@@ -5548,52 +9362,40 @@ const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; const detailsOpen = useUi((state) => state.details === sessionId); @@ -7791,7 +7795,7 @@ diff --git a/client.js b/client.js let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +5642,7 @@ +@@ -5642,7 +9444,7 @@ }, []); return view; } @@ -7800,7 +7804,7 @@ diff --git a/client.js b/client.js const replies = useSessions((list) => { const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -@@ -5658,18 +5658,22 @@ +@@ -5658,18 +9460,22 @@ }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; @@ -7828,7 +7832,7 @@ diff --git a/client.js b/client.js const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +5677,16 @@ +@@ -5677,16 +9483,16 @@ }, [showDown]); if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); @@ -7849,7 +7853,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), -@@ -5694,7 +5694,7 @@ +@@ -5694,7 +9500,7 @@ event.stopPropagation(); onDismiss(); } @@ -7858,7 +7862,7 @@ diff --git a/client.js b/client.js ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5704,8 +5704,8 @@ +@@ -5704,8 +9510,8 @@ }; const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); return (0, import_react_dom4.createPortal)( @@ -7869,7 +7873,7 @@ diff --git a/client.js b/client.js null, showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -@@ -5714,19 +5714,117 @@ +@@ -5714,19 +9520,117 @@ ); } @@ -7994,7 +7998,7 @@ diff --git a/client.js b/client.js if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); -@@ -5774,19 +5774,38 @@ +@@ -5774,19 +9678,38 @@ const stop = () => recognition.current?.stop(); return { listening, note, start, stop }; } @@ -8036,7 +8040,7 @@ diff --git a/client.js b/client.js const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); if (box instanceof HTMLElement) { box.focus(); -@@ -5797,10 +5797,10 @@ +@@ -5797,10 +9720,10 @@ }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; @@ -8050,7 +8054,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +5808,16 @@ +@@ -5808,16 +9731,16 @@ title: dictation.listening ? t("Stop dictation") : t("Dictate"), onMouseDown: keepComposerFocus, onClick: () => dictation.listening ? dictation.stop() : dictation.start() @@ -8071,7 +8075,7 @@ diff --git a/client.js b/client.js ); } function menuIcons(roster) { -@@ -5825,17 +5825,17 @@ +@@ -5825,17 +9748,17 @@ return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; @@ -8094,7 +8098,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +5876,10 @@ +@@ -5876,10 +9799,10 @@ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); @@ -8108,7 +8112,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-plus", -@@ -5889,8 +5889,8 @@ +@@ -5889,8 +9812,8 @@ "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle @@ -8119,7 +8123,7 @@ diff --git a/client.js b/client.js "div", { ref: menu, -@@ -5900,9 +5900,9 @@ +@@ -5900,9 +9823,9 @@ style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, @@ -8132,7 +8136,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -5922,15 +5922,57 @@ +@@ -5922,15 +9845,57 @@ function mentionRows(roster, sessionId, query = "") { const here = conversationOf(roster, sessionId); if (here === null) return null; @@ -8194,7 +8198,7 @@ diff --git a/client.js b/client.js function chatRows(roster, list, sessionId) { const here = conversationOf(roster, sessionId); if (here === null) return []; -@@ -5959,14 +5959,31 @@ +@@ -5959,14 +9924,31 @@ order: -20, showGroupTitle: false, async candidates(session, { query }) { @@ -8230,7 +8234,7 @@ diff --git a/client.js b/client.js }, onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 }; -@@ -5995,6 +5995,83 @@ +@@ -5995,6 +9977,83 @@ return [bots, chats]; } @@ -8314,7 +8318,7 @@ diff --git a/client.js b/client.js // src/client/speech.js var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); -@@ -6121,232 +6121,8 @@ +@@ -6121,232 +10180,8 @@ }; } @@ -8548,7 +8552,7 @@ diff --git a/client.js b/client.js var import_react_dom6 = require("react-dom"); var MENU_EXIT_MS = 120; function groupModels(models) { -@@ -6367,17 +6367,10 @@ +@@ -6367,17 +10202,10 @@ }); } var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); @@ -8568,7 +8572,7 @@ diff --git a/client.js b/client.js setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; -@@ -6402,12 +6402,12 @@ +@@ -6402,12 +10230,12 @@ ]; } function EffortRows({ reasoning, current, onPick }) { @@ -8586,7 +8590,7 @@ diff --git a/client.js b/client.js "button", { key: row.effort ?? "default", -@@ -6416,17 +6416,17 @@ +@@ -6416,17 +10244,17 @@ "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, @@ -8610,7 +8614,7 @@ diff --git a/client.js b/client.js "button", { key: model.ref, -@@ -6436,20 +6436,20 @@ +@@ -6436,20 +10264,20 @@ title: model.ref, onClick: () => onPick(model.ref) }, @@ -8639,7 +8643,7 @@ diff --git a/client.js b/client.js if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +6484,7 @@ +@@ -6484,6 +10312,7 @@ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { @@ -8647,7 +8651,7 @@ diff --git a/client.js b/client.js const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); const shown = modelOf(bot, roster); const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6511,10 @@ +@@ -6511,10 +10340,10 @@ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; const models = roster.models ?? []; @@ -8661,7 +8665,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6527,37 +6527,38 @@ +@@ -6527,37 +10356,38 @@ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, onClick: toggle }, @@ -8714,7 +8718,7 @@ diff --git a/client.js b/client.js const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; -@@ -6570,12 +6570,12 @@ +@@ -6570,12 +10400,12 @@ }).finally(() => setBusy("")); }; const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); @@ -8731,7 +8735,7 @@ diff --git a/client.js b/client.js key: model.ref, type: "button", className: "bt-model-chip", -@@ -6584,50 +6584,115 @@ +@@ -6584,50 +10414,115 @@ "aria-label": `${model.name} · ${provider}`, title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) @@ -8863,7 +8867,7 @@ diff --git a/client.js b/client.js ...ops, ...hooks, t: actions.accountT(), -@@ -6639,6 +6639,16 @@ +@@ -6639,6 +10534,16 @@ }); } var MODEL_CSS = ` @@ -8880,7 +8884,7 @@ diff --git a/client.js b/client.js .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} -@@ -6675,18 +6675,88 @@ +@@ -6675,18 +10580,88 @@ @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} `; @@ -8976,7 +8980,7 @@ diff --git a/client.js b/client.js const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { -@@ -6719,35 +6719,35 @@ +@@ -6719,35 +10694,35 @@ }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -9028,7 +9032,7 @@ diff --git a/client.js b/client.js className: "bt-folder-input", autoFocus: true, value: draft, -@@ -6759,33 +6759,33 @@ +@@ -6759,33 +10734,33 @@ if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), @@ -9078,7 +9082,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6798,19 +6798,19 @@ +@@ -6798,19 +10773,19 @@ title: value, onClick: toggle }, @@ -9104,7 +9108,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -6830,8 +6830,8 @@ +@@ -6830,8 +10805,8 @@ `; // src/client/schedules.js @@ -9115,7 +9119,7 @@ diff --git a/client.js b/client.js var shared = null; var listeners = /* @__PURE__ */ new Set(); function publish(view) { -@@ -6839,8 +6839,8 @@ +@@ -6839,8 +10814,8 @@ for (const listener of listeners) listener(view); } function useSchedules(actions) { @@ -9126,7 +9130,7 @@ diff --git a/client.js b/client.js listeners.add(setView); let stopped = false; let timer; -@@ -6848,7 +6848,7 @@ +@@ -6848,7 +10823,7 @@ if (!stopped) publish(next); }).catch(() => { }).finally(() => { @@ -9135,7 +9139,7 @@ diff --git a/client.js b/client.js }); load(); return () => { -@@ -6896,11 +6896,25 @@ +@@ -6896,11 +10871,25 @@ return ""; } } @@ -9166,7 +9170,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -6913,20 +6913,48 @@ +@@ -6913,20 +10902,48 @@ setBusy(true); void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; @@ -9224,7 +9228,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, -@@ -6934,7 +6934,7 @@ +@@ -6934,7 +10951,7 @@ "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove @@ -9233,7 +9237,7 @@ diff --git a/client.js b/client.js ); } var pad = (number) => String(number).padStart(2, "0"); -@@ -6945,22 +6945,44 @@ +@@ -6945,22 +10962,44 @@ } var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]]; @@ -9291,7 +9295,7 @@ diff --git a/client.js b/client.js }; const submit = (event) => { event.preventDefault(); -@@ -6975,146 +6975,185 @@ +@@ -6975,146 +11014,185 @@ setBusy(false); return; } @@ -9539,7 +9543,7 @@ diff --git a/client.js b/client.js ))) ); } -@@ -7127,6 +7127,12 @@ +@@ -7127,6 +11205,12 @@ .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} .bt-task-remove svg{width:14px;height:14px} @@ -9552,7 +9556,7 @@ diff --git a/client.js b/client.js .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} .bt-task-head{display:flex;align-items:center;gap:6px} .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -@@ -7158,7 +7158,7 @@ +@@ -7158,7 +11242,7 @@ `; // src/client/capability-settings.js @@ -9561,7 +9565,7 @@ diff --git a/client.js b/client.js var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); var parseMap = (text, label) => { let value; -@@ -7172,11 +7172,11 @@ +@@ -7172,11 +11256,11 @@ }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { @@ -9576,7 +9580,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, -@@ -7188,7 +7188,7 @@ +@@ -7188,7 +11272,7 @@ ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { @@ -9585,7 +9589,7 @@ diff --git a/client.js b/client.js const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); -@@ -7199,7 +7199,7 @@ +@@ -7199,7 +11283,7 @@ if (draft.kind === "mcp" && draft.transport === "stdio") { payload.transport = "stdio"; payload.command = draft.command; @@ -9594,7 +9598,7 @@ diff --git a/client.js b/client.js payload.env = parseMap(draft.envText, t("Environment")); } else if (draft.kind === "mcp") { payload.transport = "streamable-http"; -@@ -7219,95 +7219,95 @@ +@@ -7219,95 +11303,95 @@ } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); @@ -9730,7 +9734,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); -@@ -7330,20 +7330,20 @@ +@@ -7330,20 +11414,20 @@ setConfirming(""); void run(actions.deleteCapability(item.name)); }; @@ -9758,7 +9762,7 @@ diff --git a/client.js b/client.js key: item.name, item, confirming: confirming === item.name, -@@ -7359,10 +7359,10 @@ +@@ -7359,10 +11443,10 @@ const attached = new Set(bot.capabilities ?? []); const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; @@ -9771,7 +9775,7 @@ diff --git a/client.js b/client.js key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7372,9 +7372,55 @@ +@@ -7372,9 +11456,55 @@ ); } @@ -9829,7 +9833,7 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,13 +7384,13 @@ +@@ -7384,13 +11514,13 @@ for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { @@ -9846,7 +9850,7 @@ diff --git a/client.js b/client.js if (!stop) noteCount(botId, view.entries.length); }).catch(() => { }); -@@ -7404,16 +7404,16 @@ +@@ -7404,16 +11534,16 @@ function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -9869,7 +9873,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7428,45 +7428,45 @@ +@@ -7428,45 +11558,45 @@ return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { @@ -9934,7 +9938,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7478,9 +7478,9 @@ +@@ -7478,9 +11608,9 @@ return groups; }, []); function EntryRow({ entry, remove }) { @@ -9947,7 +9951,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -7495,16 +7495,16 @@ +@@ -7495,16 +11625,16 @@ setBusy(true); if (!await remove(entry)) setBusy(false); }; @@ -9969,7 +9973,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, -@@ -7512,7 +7512,7 @@ +@@ -7512,7 +11642,7 @@ "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click @@ -9978,7 +9982,7 @@ diff --git a/client.js b/client.js ); } function Entries({ bot, view, remove }) { -@@ -7521,25 +7521,25 @@ +@@ -7521,25 +11651,25 @@ { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; @@ -10015,7 +10019,7 @@ diff --git a/client.js b/client.js )) ); } -@@ -7551,52 +7551,52 @@ +@@ -7551,52 +11681,52 @@ return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); } function Reply({ answer, dismiss }) { @@ -10087,7 +10091,7 @@ diff --git a/client.js b/client.js alive.current = true; void load({ summarize: true }); return () => { -@@ -7606,9 +7606,9 @@ +@@ -7606,9 +11736,9 @@ }, [botId]); return { view, error, load, show }; } @@ -10100,7 +10104,7 @@ diff --git a/client.js b/client.js const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); -@@ -7616,18 +7616,18 @@ +@@ -7616,18 +11746,18 @@ } function MemoryDialog({ bot, actions, leaving = false }) { const { view, error, load, show } = useMemoryView(bot.id, actions); @@ -10129,7 +10133,7 @@ diff --git a/client.js b/client.js if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7689,52 +7689,52 @@ +@@ -7689,52 +11819,52 @@ const when3 = view?.summary ? ago(view.summary.at, now) : null; const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; const busy = answer?.pending === true; @@ -10205,7 +10209,7 @@ diff --git a/client.js b/client.js ref: inputRef, className: "bt-mem-input", value: text, -@@ -7744,10 +7744,10 @@ +@@ -7744,10 +11874,10 @@ "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), onChange: (event) => setText(event.target.value) }), @@ -10218,7 +10222,7 @@ diff --git a/client.js b/client.js ) ) ) -@@ -7755,8 +7755,40 @@ +@@ -7755,8 +11885,40 @@ ); } @@ -10260,7 +10264,7 @@ diff --git a/client.js b/client.js var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +7780,8 @@ +@@ -7780,8 +11942,8 @@ throw new Error(t("That image is too large")); } function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { @@ -10271,7 +10275,7 @@ diff --git a/client.js b/client.js const commit = () => { const next = draft?.trim(); setDraft(null); -@@ -7792,7 +7792,7 @@ +@@ -7792,7 +11954,7 @@ if (next === void 0 || next === (value ?? "") || required && next === "") return; void onSave(next); }; @@ -10280,7 +10284,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, -@@ -7813,29 +7813,29 @@ +@@ -7813,29 +11975,29 @@ }); } function Row({ label, hint, stack = false, children }) { @@ -10319,7 +10323,7 @@ diff --git a/client.js b/client.js ); } function modelHint(bot, roster) { -@@ -7846,22 +7846,22 @@ +@@ -7846,22 +12008,22 @@ return t("Applies from the next message."); } function ShapeRing({ shape, size }) { @@ -10347,7 +10351,7 @@ diff --git a/client.js b/client.js key: id, type: "button", className: "bt-shape", -@@ -7869,8 +7869,8 @@ +@@ -7869,8 +12031,8 @@ title: t(shapeOf(id).label), "aria-pressed": !hasPhoto && look.shape === id, onClick: () => onShape(id) @@ -10358,7 +10362,7 @@ diff --git a/client.js b/client.js key: color, type: "button", className: "bt-color", -@@ -7878,19 +7878,19 @@ +@@ -7878,19 +12040,19 @@ title: t(capitalize(color)), "aria-pressed": look.color === color, onClick: () => onColor(color) @@ -10384,7 +10388,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); -@@ -7908,7 +7908,7 @@ +@@ -7908,7 +12070,7 @@ window.removeEventListener("keydown", onKey, true); }; }, [onClose]); @@ -10393,7 +10397,7 @@ diff --git a/client.js b/client.js if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +7922,32 @@ +@@ -7922,32 +12084,32 @@ }, [tab, bot]); const hasPhoto = Boolean(look.image); const glide = useGlide(tab); @@ -10434,7 +10438,7 @@ diff --git a/client.js b/client.js "div", { className: "bt-dropzone", -@@ -7963,15 +7963,15 @@ +@@ -7963,15 +12125,15 @@ upload(event.dataTransfer.files?.[0]); } }, @@ -10456,7 +10460,7 @@ diff --git a/client.js b/client.js const file = event.target.files?.[0]; event.target.value = ""; upload(file); -@@ -7982,26 +7982,27 @@ +@@ -7982,26 +12144,27 @@ function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -10492,7 +10496,7 @@ diff --git a/client.js b/client.js const isMainBot = isMainOf(roster, bot.id); const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; const isTile = roster.mainBotId === bot.id; -@@ -8011,112 +8011,116 @@ +@@ -8011,112 +12174,116 @@ if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -10658,7 +10662,7 @@ diff --git a/client.js b/client.js ) ) ); -@@ -8133,6 +8133,7 @@ +@@ -8133,6 +12300,7 @@ .bt-bs-row>.bt-set-copy{flex:1 1 auto} .bt-bs-label{font-size:13px;line-height:18px;font-weight:500} .bt-bs-row .bt-model-trigger{flex:none;max-width:58%} @@ -10666,7 +10670,7 @@ diff --git a/client.js b/client.js .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} .bt-bs-input:hover{background:var(--bt-hover)} .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -@@ -8155,6 +8155,643 @@ +@@ -8155,6 +12323,643 @@ @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} `; @@ -11310,7 +11314,7 @@ diff --git a/client.js b/client.js // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); -@@ -8165,36 +8165,48 @@ +@@ -8165,36 +12970,48 @@ } function startModel(roster) { const models = roster.models ?? []; @@ -11374,7 +11378,7 @@ diff --git a/client.js b/client.js if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8206,8 +8206,8 @@ +@@ -8206,8 +13023,8 @@ setError(""); setBusy(true); try { @@ -11385,7 +11389,7 @@ diff --git a/client.js b/client.js } catch (failure) { setError(failure?.message ?? String(failure)); setBusy(false); -@@ -8219,25 +8219,25 @@ +@@ -8219,25 +13036,25 @@ avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; @@ -11419,7 +11423,7 @@ diff --git a/client.js b/client.js look, hasPhoto: Boolean(image), onShape: (shape) => { -@@ -8249,27 +8249,27 @@ +@@ -8249,27 +13066,27 @@ setLook((current) => ({ ...current, color })); } }), @@ -11455,7 +11459,7 @@ diff --git a/client.js b/client.js className: "bt-input", autoFocus: !inline, value: name, -@@ -8284,75 +8284,113 @@ +@@ -8284,75 +13101,113 @@ } } }), @@ -11599,7 +11603,7 @@ diff --git a/client.js b/client.js const pick2 = (bot) => { setPicked((current) => [...current, bot.id]); setQuery(""); -@@ -8386,13 +8386,17 @@ +@@ -8386,13 +13241,17 @@ }; const created = (bot) => { close(); @@ -11620,7 +11624,7 @@ diff --git a/client.js b/client.js if (text) await actions.send(room2.id, text); actions.openSession(room2.id); close(); -@@ -8411,46 +8411,75 @@ +@@ -8411,46 +13270,75 @@ close(); return; } @@ -11707,7 +11711,7 @@ diff --git a/client.js b/client.js canGoBack: draft.fromList, onBack: draft.fromList ? () => { setDraft(null); -@@ -8467,33 +8467,41 @@ +@@ -8467,33 +13355,41 @@ if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; @@ -11764,7 +11768,7 @@ diff --git a/client.js b/client.js ref: inputRef, autoFocus: true, value: query, -@@ -8517,20 +8517,20 @@ +@@ -8517,20 +13413,20 @@ } else if (event.key === "Backspace" && query === "") unpick(); } }), @@ -11790,7 +11794,7 @@ diff --git a/client.js b/client.js "button", { key: option.key, -@@ -8542,21 +8542,36 @@ +@@ -8542,21 +13438,36 @@ onMouseEnter: () => setCursor(index), onClick: option.run }, @@ -11836,7 +11840,7 @@ diff --git a/client.js b/client.js rows: 1, value: message, placeholder, -@@ -8569,7 +8569,7 @@ +@@ -8569,7 +13480,7 @@ } } }), @@ -11845,7 +11849,7 @@ diff --git a/client.js b/client.js ) ) ) -@@ -8577,18 +8577,18 @@ +@@ -8577,18 +13488,18 @@ } // src/client/exchange.js @@ -11868,7 +11872,7 @@ diff --git a/client.js b/client.js let stop = false; let timer; let seen = null; -@@ -8612,48 +8612,48 @@ +@@ -8612,48 +13523,48 @@ const right = roster.byId[pair[1]]; let lastDay = 0; const paneLeft = usePaneLeft(); @@ -11933,7 +11937,7 @@ diff --git a/client.js b/client.js var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); var VOICE_LABELS = { listening: "Listening", -@@ -8665,14 +8665,14 @@ +@@ -8665,14 +13576,14 @@ }; var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; function useMicLevel(enabled) { @@ -11951,7 +11955,7 @@ diff --git a/client.js b/client.js if (stopped) { media.getTracks().forEach((track) => track.stop()); return; -@@ -8716,18 +8716,18 @@ +@@ -8716,18 +13627,18 @@ const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); @@ -11981,7 +11985,7 @@ diff --git a/client.js b/client.js const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; -@@ -8747,12 +8747,12 @@ +@@ -8747,12 +13658,12 @@ dictation.stop(); submit(text); }); @@ -11996,7 +12000,7 @@ diff --git a/client.js b/client.js if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +8767,16 @@ +@@ -8767,16 +13678,16 @@ }; }, [speech]); const listen = phase === "listening"; @@ -12016,7 +12020,7 @@ diff --git a/client.js b/client.js if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; -@@ -8838,7 +8838,7 @@ +@@ -8838,7 +13749,7 @@ setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); @@ -12025,7 +12029,7 @@ diff --git a/client.js b/client.js if (phase !== "thinking" || running || waiting.current === null) return; const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; if (count === waiting.current || !text.trim()) return; -@@ -8858,7 +8858,7 @@ +@@ -8858,7 +13769,7 @@ window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); @@ -12034,7 +12038,7 @@ diff --git a/client.js b/client.js const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,292 +8875,48 @@ +@@ -8875,292 +13786,48 @@ const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; @@ -12343,7 +12347,7 @@ diff --git a/client.js b/client.js // src/client/usage-data.js var HOUR = 0; var OWNER = 1; -@@ -9277,13 +9277,13 @@ +@@ -9277,13 +13944,13 @@ } return result; } @@ -12359,7 +12363,7 @@ diff --git a/client.js b/client.js } return String(Math.round(count)); } -@@ -9294,11 +9294,11 @@ +@@ -9294,11 +13961,11 @@ } // src/client/usage-page.js @@ -12373,7 +12377,7 @@ diff --git a/client.js b/client.js var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -@@ -9341,9 +9341,9 @@ +@@ -9341,9 +14008,9 @@ return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { @@ -12386,7 +12390,7 @@ diff --git a/client.js b/client.js if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); -@@ -9363,9 +9363,9 @@ +@@ -9363,9 +14030,9 @@ }, [value]); return shown; } @@ -12398,7 +12402,7 @@ diff --git a/client.js b/client.js className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +9382,7 @@ +@@ -9382,7 +14049,7 @@ sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], @@ -12407,7 +12411,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "room") { -@@ -9392,7 +9392,7 @@ +@@ -9392,7 +14059,7 @@ name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", @@ -12416,7 +12420,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "agent") { -@@ -9414,30 +9414,30 @@ +@@ -9414,30 +14081,30 @@ const names = options.map(([, text]) => text).join("\n"); const glide = useGlide(`${value} ${names}`, { box: boxRef }); @@ -12457,7 +12461,7 @@ diff --git a/client.js b/client.js "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +9455,83 @@ +@@ -9455,83 +14122,83 @@ return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { @@ -12568,7 +12572,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9540,69 @@ +@@ -9540,69 +14207,69 @@ title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -12666,7 +12670,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; -@@ -9628,17 +9628,17 @@ +@@ -9628,17 +14295,17 @@ const [from, to] = [anchor, key].sort(); onPick(from, to); }; @@ -12687,7 +12691,7 @@ diff --git a/client.js b/client.js key, type: "button", className: "bt-us-day", -@@ -9654,15 +9654,15 @@ +@@ -9654,15 +14321,15 @@ "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) @@ -12709,7 +12713,7 @@ diff --git a/client.js b/client.js ); }; const shortcuts = [ -@@ -9671,30 +9671,30 @@ +@@ -9671,30 +14338,30 @@ ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; @@ -12748,7 +12752,7 @@ diff --git a/client.js b/client.js const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); -@@ -9710,13 +9710,13 @@ +@@ -9710,13 +14377,13 @@ for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { @@ -12764,7 +12768,7 @@ diff --git a/client.js b/client.js key: ms, className: "bt-us-cell", style: { "--w": week }, -@@ -9724,7 +9724,7 @@ +@@ -9724,7 +14391,7 @@ "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) @@ -12773,7 +12777,7 @@ diff --git a/client.js b/client.js } } const tipSide = hover ? { -@@ -9733,61 +9733,61 @@ +@@ -9733,61 +14400,61 @@ } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); @@ -12861,7 +12865,7 @@ diff --git a/client.js b/client.js let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +9797,7 @@ +@@ -9797,7 +14464,7 @@ const value = await actions.usage(); await resized; if (stopped) return; @@ -12870,7 +12874,7 @@ diff --git a/client.js b/client.js setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { -@@ -9814,7 +9814,7 @@ +@@ -9814,7 +14481,7 @@ }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); @@ -12879,7 +12883,7 @@ diff --git a/client.js b/client.js if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +9823,7 @@ +@@ -9823,7 +14490,7 @@ const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; @@ -12888,7 +12892,7 @@ diff --git a/client.js b/client.js }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +9871,23 @@ +@@ -9871,23 +14538,23 @@ }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); @@ -12920,7 +12924,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -9912,7 +9912,7 @@ +@@ -9912,7 +14579,7 @@ ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; @@ -12929,7 +12933,7 @@ diff --git a/client.js b/client.js key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), -@@ -9924,7 +9924,7 @@ +@@ -9924,7 +14591,7 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); @@ -12938,7 +12942,7 @@ diff --git a/client.js b/client.js key: `m${at}`, kind: "model", glyph: models[at].glyph(32), -@@ -9936,30 +9936,30 @@ +@@ -9936,30 +14603,30 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); @@ -12979,7 +12983,7 @@ diff --git a/client.js b/client.js ) ]; } else { -@@ -9968,39 +9968,39 @@ +@@ -9968,39 +14635,39 @@ const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ @@ -13033,7 +13037,7 @@ diff --git a/client.js b/client.js body ); } -@@ -10150,33 +10150,135 @@ +@@ -10150,33 +14817,135 @@ @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} @keyframes bt-us-fade{from{opacity:0}} @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} @@ -13180,7 +13184,7 @@ diff --git a/client.js b/client.js const run = (task, after) => { setBusy(true); setError(null); -@@ -10184,72 +10184,128 @@ +@@ -10184,72 +14953,128 @@ after?.(); }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; @@ -13334,7 +13338,7 @@ diff --git a/client.js b/client.js ); function download(name, text) { const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); -@@ -10260,7 +10260,7 @@ +@@ -10260,7 +15085,7 @@ setTimeout(() => URL.revokeObjectURL(url), 1e3); } var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); @@ -13343,7 +13347,7 @@ diff --git a/client.js b/client.js const date = new Date(time); const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -@@ -10270,18 +10270,18 @@ +@@ -10270,18 +15095,18 @@ if (entry.source === "turn") return failureLabel(entry.code, entry.text); return entry.text; } @@ -13368,7 +13372,7 @@ diff --git a/client.js b/client.js void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +10289,7 @@ +@@ -10289,7 +15114,7 @@ setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); @@ -13377,7 +13381,7 @@ diff --git a/client.js b/client.js const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; -@@ -10317,59 +10317,59 @@ +@@ -10317,59 +15142,59 @@ const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ @@ -13462,7 +13466,7 @@ diff --git a/client.js b/client.js ) ) ]; -@@ -10377,8 +10377,8 @@ +@@ -10377,8 +15202,8 @@ var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); @@ -13473,7 +13477,7 @@ diff --git a/client.js b/client.js if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +10389,16 @@ +@@ -10389,15 +15214,16 @@ }; }, [toast?.token]); if (!toast) return null; @@ -13495,7 +13499,7 @@ diff --git a/client.js b/client.js ); } var FEEDBACK_CSS = ` -@@ -10416,47 +10416,56 @@ +@@ -10416,47 +15242,56 @@ .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} .bt-event-error{color:#e02135} @@ -13565,7 +13569,7 @@ diff --git a/client.js b/client.js let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); -@@ -10466,87 +10466,72 @@ +@@ -10466,87 +15301,72 @@ live = false; }; }, []); @@ -13676,7 +13680,7 @@ diff --git a/client.js b/client.js .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .bt-about-name{font-size:16px;line-height:22px;font-weight:600} .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -@@ -10569,54 +10569,59 @@ +@@ -10569,54 +15389,59 @@ `; // src/client/settings.js @@ -13753,7 +13757,7 @@ diff --git a/client.js b/client.js key: ink, type: "button", className: "bt-swatch", -@@ -10626,75 +10626,87 @@ +@@ -10626,75 +15451,87 @@ style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), @@ -13864,7 +13868,7 @@ diff --git a/client.js b/client.js key: id, type: "button", "aria-pressed": motion === id, -@@ -10706,69 +10706,69 @@ +@@ -10706,69 +15543,69 @@ ); } function BotsPage({ roster, actions, run }) { @@ -13964,7 +13968,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +10779,112 @@ +@@ -10779,104 +15616,112 @@ }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -14118,7 +14122,7 @@ diff --git a/client.js b/client.js const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; -@@ -10884,13 +10884,22 @@ +@@ -10884,13 +15729,22 @@ const isMainBot = isMainOf(roster, menu.id); const isTile = roster.mainBotId === menu.id; const mainCount = (roster.mainBotIds ?? []).length; @@ -14143,7 +14147,7 @@ diff --git a/client.js b/client.js bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -@@ -10901,20 +10901,22 @@ +@@ -10901,20 +15755,22 @@ bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null ], @@ -14172,7 +14176,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10923,14 +10923,14 @@ +@@ -10923,14 +15779,14 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -14191,7 +14195,7 @@ diff --git a/client.js b/client.js const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +10952,14 @@ +@@ -10952,14 +15808,14 @@ [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; @@ -14211,7 +14215,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10968,16 +10968,31 @@ +@@ -10968,16 +15824,31 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } @@ -14244,7 +14248,7 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11038,233 +11038,8 @@ +@@ -11038,233 +15909,8 @@ } } @@ -14479,7 +14483,7 @@ diff --git a/client.js b/client.js function transcriptOf(list, id, name, hiddenTurns = []) { const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; if (!Array.isArray(outline)) return ""; -@@ -11289,10 +11289,10 @@ +@@ -11289,10 +15935,10 @@ var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { @@ -14492,7 +14496,7 @@ diff --git a/client.js b/client.js if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; -@@ -11302,14 +11302,14 @@ +@@ -11302,14 +15948,14 @@ setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; @@ -14511,7 +14515,7 @@ diff --git a/client.js b/client.js key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +11323,42 @@ +@@ -11323,42 +15969,42 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -14569,7 +14573,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), -@@ -11367,87 +11367,195 @@ +@@ -11367,87 +16013,195 @@ void actions.browserCloseTab(bot.id, tabId); } }, "×"), @@ -14808,7 +14812,7 @@ diff --git a/client.js b/client.js if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; -@@ -11472,36 +11472,43 @@ +@@ -11472,36 +16226,43 @@ setTimeout(() => setCopied(false), 1600); }); }; @@ -14867,7 +14871,7 @@ diff --git a/client.js b/client.js ]; } const current = bot?.name ?? room2.name; -@@ -11515,12 +11515,12 @@ +@@ -11515,12 +16276,12 @@ const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ @@ -14884,7 +14888,7 @@ diff --git a/client.js b/client.js className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,87 +11535,86 @@ +@@ -11535,87 +16296,86 @@ setEditing(false); } } @@ -15014,7 +15018,7 @@ diff --git a/client.js b/client.js } var savedWidth = () => { try { -@@ -11625,8 +11625,8 @@ +@@ -11625,8 +16385,8 @@ return null; } }; @@ -15024,7 +15028,7 @@ diff --git a/client.js b/client.js var room = { rule: null, selector: "" }; var roomRule = () => { const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -@@ -11674,11 +11674,11 @@ +@@ -11674,11 +16434,11 @@ var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); @@ -15041,7 +15045,7 @@ diff --git a/client.js b/client.js const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; -@@ -11707,7 +11707,7 @@ +@@ -11707,7 +16467,7 @@ trail.cancel(); }; }); @@ -15050,7 +15054,7 @@ diff --git a/client.js b/client.js const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { -@@ -11721,13 +11721,13 @@ +@@ -11721,13 +16481,13 @@ observer.observe(box); return () => observer.disconnect(); }, []); @@ -15067,7 +15071,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -11737,13 +11737,13 @@ +@@ -11737,13 +16497,13 @@ title: bot.name, onClick: () => onView("details") }, @@ -15084,7 +15088,7 @@ diff --git a/client.js b/client.js "button", { key: tabId, -@@ -11764,9 +11764,9 @@ +@@ -11764,9 +16524,9 @@ } } }, @@ -15097,7 +15101,7 @@ diff --git a/client.js b/client.js className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), -@@ -11778,20 +11778,20 @@ +@@ -11778,20 +16538,20 @@ }, "×") ); }), @@ -15125,7 +15129,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,7 +11817,7 @@ +@@ -11817,7 +16577,7 @@ window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -15134,7 +15138,7 @@ diff --git a/client.js b/client.js setDraft(null); if (landed && !page.url) input.current?.focus(); }, [tabId, landed]); -@@ -11833,27 +11833,27 @@ +@@ -11833,27 +16593,27 @@ }; const editing = draft !== null; const secure = /^https:/i.test(page.url); @@ -15171,7 +15175,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +11877,25 @@ +@@ -11877,25 +16637,25 @@ } }) ), @@ -15207,7 +15211,7 @@ diff --git a/client.js b/client.js ) : null ) ); -@@ -11907,41 +11907,41 @@ +@@ -11907,41 +16667,41 @@ const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); @@ -15265,7 +15269,7 @@ diff --git a/client.js b/client.js let timer; const onResize = () => { setResizing(true); -@@ -11955,28 +11955,28 @@ +@@ -11955,28 +16715,28 @@ window.removeEventListener("resize", onResize); }; }, []); @@ -15301,7 +15305,7 @@ diff --git a/client.js b/client.js if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +12013,7 @@ +@@ -12013,7 +16773,7 @@ return actions[name](...args); }; } @@ -15310,7 +15314,7 @@ diff --git a/client.js b/client.js const move = fly.current; if (!move) return; fly.current = null; -@@ -12096,7 +12096,7 @@ +@@ -12096,7 +16856,7 @@ for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; @@ -15319,7 +15323,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +12113,15 @@ +@@ -12113,15 +16873,15 @@ if (event.target === event.currentTarget) setLanded(true); } }, @@ -15340,7 +15344,7 @@ diff --git a/client.js b/client.js sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, -@@ -12217,8 +12217,78 @@ +@@ -12217,8 +16977,78 @@ @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} `; @@ -15420,7 +15424,7 @@ diff --git a/client.js b/client.js var import_react_dom9 = require("react-dom"); var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; -@@ -12227,15 +12227,15 @@ +@@ -12227,15 +17057,15 @@ const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); @@ -15440,7 +15444,7 @@ diff --git a/client.js b/client.js if (open) { setKept(open); return void 0; -@@ -12245,19 +12245,26 @@ +@@ -12245,19 +17075,26 @@ }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); const pane = open ?? kept; const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); @@ -15477,7 +15481,7 @@ diff --git a/client.js b/client.js ); } -@@ -12304,7 +12304,7 @@ +@@ -12304,7 +17141,7 @@ `; }; const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); @@ -15486,7 +15490,7 @@ diff --git a/client.js b/client.js const block = (text2) => expand(flow(text2)); const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); -@@ -12372,7 +12372,7 @@ +@@ -12372,7 +17209,7 @@ const marker = ordered ? `${number++}. ` : "- "; if (body2 === "") continue; const pad2 = " ".repeat(marker.length); @@ -15495,7 +15499,7 @@ diff --git a/client.js b/client.js } return keep(lines.join("\n")); }; -@@ -12398,11 +12398,11 @@ +@@ -12398,11 +17235,11 @@ let width = 0; for (const row of rows.slice(0, limits.tableRows + 1)) { if (over()) break; @@ -15509,7 +15513,7 @@ diff --git a/client.js b/client.js carry[column].left -= 1; column += 1; } -@@ -12414,14 +12414,14 @@ +@@ -12414,14 +17251,14 @@ const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); for (let offset = 0; offset < span; offset += 1) { @@ -15527,7 +15531,7 @@ diff --git a/client.js b/client.js } width = Math.min(width, limits.tableCols); if (grid.length === 0 || width === 0) return caption ? ` -@@ -12552,7 +12552,7 @@ +@@ -12552,7 +17389,7 @@ } if (tag === "blockquote") { const quoted = block(body2); @@ -15536,7 +15540,7 @@ diff --git a/client.js b/client.js } const end = body2.trimEnd().length; const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -@@ -13052,11 +13052,117 @@ +@@ -13052,11 +17889,117 @@ }; } @@ -15655,7 +15659,7 @@ diff --git a/client.js b/client.js function disposeAll(disposers, label) { for (const dispose of [...disposers].reverse()) { try { -@@ -13075,8 +13075,8 @@ +@@ -13075,8 +18018,8 @@ }; return { reconcile(wanted) { @@ -15666,7 +15670,7 @@ diff --git a/client.js b/client.js if (!wanted[group] || mounted[group].length > 0) continue; try { mounted[group] = install[group](); -@@ -13086,7 +13086,7 @@ +@@ -13086,7 +18029,7 @@ } }, dispose() { @@ -15675,7 +15679,7 @@ diff --git a/client.js b/client.js } }; } -@@ -13105,7 +13105,7 @@ +@@ -13105,7 +18048,7 @@ } // src/client/user-message.js @@ -15684,7 +15688,7 @@ diff --git a/client.js b/client.js var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { -@@ -13119,10 +13119,63 @@ +@@ -13119,10 +18062,63 @@ } return { text: texts2.join("\n"), attachments, rest }; } @@ -15751,7 +15755,7 @@ diff --git a/client.js b/client.js } function userTextParts(text, bots, referenceLabels = []) { const parts = []; -@@ -13132,43 +13132,44 @@ +@@ -13132,43 +18128,44 @@ if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) }); parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) }); cursor = span.end; @@ -15809,7 +15813,7 @@ diff --git a/client.js b/client.js } var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0; function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) { -@@ -13178,28 +13178,52 @@ +@@ -13178,28 +18175,52 @@ const referenceLabels = data.referenceLabels ?? []; const references = openFile && openSkill ? { openFile, openSkill } : void 0; const steering = node.kind === "steering"; @@ -15871,7 +15875,7 @@ diff --git a/client.js b/client.js .bt-ubody a{color:inherit} .bt-umention{display:inline-flex;align-items:center;gap:3px;vertical-align:-3px;max-width:100%;padding:0 1px;border:0;background:none;color:inherit;font:inherit;font-weight:600;line-height:inherit;cursor:pointer;border-radius:6px;white-space:nowrap} .bt-umention:hover{text-decoration:underline;text-underline-offset:2px} -@@ -13216,42 +13216,115 @@ +@@ -13216,42 +18237,115 @@ `; // src/client/approval-notices.js @@ -16000,7 +16004,7 @@ diff --git a/client.js b/client.js migrateStorage(); const UNREAD_KEY = STORAGE.unread; let savedUnread = {}; -@@ -13274,7 +13274,13 @@ +@@ -13274,7 +18368,13 @@ } catch { seenAt = {}; } @@ -16015,7 +16019,7 @@ diff --git a/client.js b/client.js const surface = createSource({ mode: readMode(), plain: false }); let agentSessionCache = /* @__PURE__ */ new Set(); try { -@@ -13344,13 +13344,25 @@ +@@ -13344,13 +18444,25 @@ } return { ...state, unread }; }); @@ -16042,7 +16046,7 @@ diff --git a/client.js b/client.js if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); -@@ -13368,10 +13368,15 @@ +@@ -13368,10 +18480,15 @@ const live2 = {}; if (value.connectors !== void 0 && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live2.connectors = value.connectors; if (value.capabilityStatuses !== void 0 && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live2.capabilityStatuses = value.capabilityStatuses; @@ -16058,7 +16062,7 @@ diff --git a/client.js b/client.js try { const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []); localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)); -@@ -13488,20 +13488,12 @@ +@@ -13488,20 +18605,12 @@ const style = installStyles("", "mentions"); let last = ""; const render = () => { @@ -16081,7 +16085,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13520,7 +13520,7 @@ +@@ -13520,7 +18629,7 @@ const accent = isHex(prefs.accent) ? prefs.accent : void 0; const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal"; document.body.dataset.btMotion = motion; @@ -16090,7 +16094,7 @@ diff --git a/client.js b/client.js if (next === key) return; key = next; style.textContent = themeCss(themes[id], accent); -@@ -13536,7 +13536,7 @@ +@@ -13536,7 +18645,7 @@ }; render(); const offRoster = roster.subscribe(render); @@ -16099,7 +16103,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13588,6 +13588,7 @@ +@@ -13588,6 +18697,7 @@ // or the click appears to do nothing. isAgentSession: (id) => isAgentSessionId(id), openSession: (id) => { @@ -16107,7 +16111,7 @@ diff --git a/client.js b/client.js if (isAgentSessionId(id)) return actions.openAgentSession(id); ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state); setPlain(false); -@@ -13653,12 +13653,12 @@ +@@ -13653,12 +18763,12 @@ }, // `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. @@ -16123,7 +16127,7 @@ diff --git a/client.js b/client.js expected = target; ctx.uiWorkspace.openSession(target); }, -@@ -13708,10 +13708,15 @@ +@@ -13708,10 +18818,15 @@ }, // `id` opens one Bot (page 'bots') or one group chat (page 'groups'). openSettings: (page = "general", id = null) => { @@ -16141,7 +16145,7 @@ diff --git a/client.js b/client.js }, openMemory: (botId) => { ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null })); -@@ -13719,6 +13719,19 @@ +@@ -13719,6 +18834,19 @@ closeMemory: () => { ui2.set((state) => state.memory ? { ...state, memory: null } : state); }, @@ -16161,7 +16165,7 @@ diff --git a/client.js b/client.js openConnectors: () => { ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null })); }, -@@ -13728,7 +13728,7 @@ +@@ -13728,7 +18856,7 @@ if (mode !== "bot" && mode !== "agent") return; if (surface.getSnapshot().mode === mode) return; if (mode === "agent") { @@ -16170,7 +16174,7 @@ diff --git a/client.js b/client.js } writeMode(mode); surface.set((state) => ({ ...state, mode })); -@@ -13757,6 +13757,12 @@ +@@ -13757,6 +18885,12 @@ const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null); ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } })); }, @@ -16183,7 +16187,7 @@ diff --git a/client.js b/client.js endVisit: (id) => { leaveVisits([id]); ui2.set((state) => { -@@ -13810,8 +13810,14 @@ +@@ -13810,8 +18944,14 @@ updateRoom: (id, patch) => after(call("update-room", { id, ...patch })), send: (sessionId, text) => after(call("send", { sessionId: partFor(sessionId), text })), answerQuestion: (sessionId, questionId, answer) => after(call("answer-question", { sessionId: partFor(sessionId), questionId, answer })), @@ -16198,7 +16202,7 @@ diff --git a/client.js b/client.js stopMemberWork: (roomId, botId) => after(call("stop-member-work", { roomId, botId })), // Values go out in these requests and never come back in a response. secretSet: (payload) => after(call("secret-set", payload)), -@@ -13819,15 +13819,26 @@ +@@ -13819,15 +18959,26 @@ secretCancel: (requestId) => after(call("secret-cancel", { requestId })), secretUpdate: (name, scope) => after(call("secret-update", { name, scope })), secretDelete: (name) => after(call("secret-delete", { name })), @@ -16226,7 +16230,7 @@ diff --git a/client.js b/client.js scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }), exchange: (a, b) => call("exchange", { a, b }).catch(() => []), roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []), -@@ -13846,6 +13846,8 @@ +@@ -13846,6 +18997,8 @@ noteClientError(where, error); ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, @@ -16235,7 +16239,7 @@ diff --git a/client.js b/client.js dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { -@@ -13890,6 +13890,7 @@ +@@ -13890,6 +19043,7 @@ actions.closePanel = () => { ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state); }; @@ -16243,7 +16247,7 @@ diff --git a/client.js b/client.js actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null; actions.canSignIn = () => Boolean(actions.signInEntry()); actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 })); -@@ -13964,7 +13964,13 @@ +@@ -13964,7 +19118,13 @@ }); const browserPages = browsers?.pages ?? createSource({}); const browserTabs = browsers?.tabs ?? createSource({}); @@ -16258,7 +16262,7 @@ diff --git a/client.js b/client.js ctx.effect(() => noticeApprovals({ work, roster, -@@ -13989,7 +13989,17 @@ +@@ -13989,7 +19149,17 @@ } }; }; const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component)); @@ -16276,7 +16280,7 @@ diff --git a/client.js b/client.js const installAtMenu = () => { const fiber = ctx.inject(["inputTriggers"], (scope) => { scope.effect(() => { -@@ -14001,7 +14001,12 @@ +@@ -14001,7 +19171,12 @@ const launched = (sessionId) => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE; const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map((source) => triggers.registerSource(source)); atMenu = { controllerOf }; @@ -16289,7 +16293,7 @@ diff --git a/client.js b/client.js atMenu = null; for (const off of offs) off(); }; -@@ -14010,6 +14010,14 @@ +@@ -14010,6 +19185,14 @@ return () => fiber.dispose(); }; actions.canReferenceChat = () => atMenu !== null; @@ -16304,7 +16308,7 @@ diff --git a/client.js b/client.js actions.referenceChat = (sessionId, span) => { const controller = atMenu?.controllerOf(sessionId); if (controller === void 0 || span === void 0) return; -@@ -14019,33 +14019,46 @@ +@@ -14019,33 +19202,46 @@ installCompiledStyles(SHELL_CSS, "shell"), installCompiledStyles(SIDEBAR_CSS, "sidebar"), installCompiledStyles(SECRET_CSS, "secrets"), @@ -16355,7 +16359,7 @@ diff --git a/client.js b/client.js shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell), shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell), shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell), -@@ -14057,15 +14057,24 @@ +@@ -14057,15 +19253,24 @@ // every message. shadow("conversation.chat.node", { key: "question-reply", priority: -10, locale: "chat" }, AnsweredCell), shadow("conversation.chat.node", { key: "compaction", priority: -10, locale: "chat" }, Nothing), @@ -16383,7 +16387,7 @@ diff --git a/client.js b/client.js shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice), shadow("conversation.composer", { priority: -10, -@@ -14074,9 +14074,18 @@ +@@ -14074,9 +19279,18 @@ shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock), shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock), shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock), diff --git a/plugins/social/client.js b/plugins/social/client.js index 1e228af7f..edcb3de53 100644 --- a/plugins/social/client.js +++ b/plugins/social/client.js @@ -960,14 +960,22 @@ window.__ModuleLoader__.load({ // The interface's translator (#892): core's dictionary carries this plugin's Members-card and dialog copy, // so a zh UI reads it in Chinese. An older host has none: the English stands, with {params} filled in. // #892: the entry, the dialog's title and its copy name both ways in (core's dictionary has the zh). - const JOIN_OR_CREATE = 'Join or create by number or link' + // #1021: one row for two intents read as one label that names neither. The picker offers + // **Join by number or link** and **Start a face-to-face room** as separate rows; both open this + // screen, which titles itself after the row that was tapped, and only the join path carries the + // link and scan fields, because those are ways into someone else's group. + const JOIN_BY_NUMBER = 'Join by number or link' + const START_FACE_ROOM = 'Start a face-to-face room' // #1053: a fresh code's first step is starting the room. #999: the same answer also means a // room with this number may already have opened, so the hint still points at an invite link. const NOBODY_TITLE = 'Start a new room with {code}' const NOBODY_HINT = 'No one is waiting under this number. If someone already opened the room, the number no longer works — ask them for an invite link.' const START_NEW_ROOM = 'Start a new room' const JOIN_OR_CREATE_HINT = 'Enter a number, or paste a group invite link' + const START_FACE_HINT = 'Pick the number everyone nearby types' const JOIN_OR_CREATE_ABOUT = 'Pick your Bots. Enter the same 6-digit number as everyone nearby to build a room together, or paste a group invite link or scan its QR code to join that group.' + // The start path has no invitation in it: a room is built by typing one number together. + const START_FACE_ABOUT = 'Pick your Bots and choose a 6-digit number. Everyone nearby types the same number to build the room together.' const LINK_PLACEHOLDER = 'Or paste a group invite link (https://…/r/…)' const tr = (text, params) => { const fn = globalThis.__flybot__?.t @@ -1391,7 +1399,10 @@ window.__ModuleLoader__.load({ const othersIn = (members, mine) => (members ?? []).some(member => !mine(member)) // Separate Face-to-face room screen (#92): pick Bots, join by number, live waiting room. - function FaceToFaceGroup({ actions, onClose, roster, remoteChats } = {}) { + // `mode` is the picker row that opened it (#1021): `join` keeps every way in, `start` is the + // room being built without an invitation. + function FaceToFaceGroup({ actions, onClose, roster, remoteChats, mode } = {}) { + const starting = mode === 'start' const [step, setStep] = useState('setup') const [bots, setBots] = useState([]) const [selected, setSelected] = useState([]) @@ -1775,13 +1786,16 @@ window.__ModuleLoader__.load({ // #1052: the helper is for a started number only. Empty stays quiet; six digits clear it. const digitsPartial = digitCount > 0 && !digitsTyped const nobody = step === 'nobody' ? faceWaitCode(room, code) : '' + // #1021: the screen names the row that opened it, so a person who tapped "join" never reads a + // title about starting a room (and the other way round). + const title = starting ? START_FACE_ROOM : JOIN_BY_NUMBER return h('div', { - className: 'fs-dialog-back', 'data-flybot': 'face-group', role: 'dialog', 'aria-label': tr(JOIN_OR_CREATE), + className: 'fs-dialog-back', 'data-flybot': 'face-group', role: 'dialog', 'aria-label': tr(title), onClick: event => { if (event?.target === event?.currentTarget) dismiss() }, }, h('div', { className: 'fs-dialog' }, h('div', { className: 'fs-dialog-head' }, - h('div', { className: 'fs-name', 'data-flybot': 'face-group-title' }, tr(JOIN_OR_CREATE)), + h('div', { className: 'fs-name', 'data-flybot': 'face-group-title' }, tr(title)), h('button', { type: 'button', className: 'fs-btn fs-btn-sm', onClick: dismiss }, tr('Close'))), step === 'nobody' ? h('div', { className: 'fs-wait', 'data-flybot': 'face-nobody' }, // #999: `nobody` means the join answer held no other home, not that the number is unused — @@ -1820,7 +1834,7 @@ window.__ModuleLoader__.load({ disabled: !!busy || !room.roomId, onClick: enter, }, 'Enter chat'), }))) : h('form', { onSubmit: join, 'data-flybot': 'face-join' }, - h('div', { className: 'fs-hint', 'data-flybot': 'face-join-about' }, tr(JOIN_OR_CREATE_ABOUT)), + h('div', { className: 'fs-hint', 'data-flybot': 'face-join-about' }, tr(starting ? START_FACE_ABOUT : JOIN_OR_CREATE_ABOUT)), h('ul', { className: 'fs-tiles fs-pick', 'data-flybot': 'face-pick', 'aria-label': 'Your Bots' }, bots.map(bot => h(FaceTile, { key: bot.id, @@ -1848,7 +1862,9 @@ window.__ModuleLoader__.load({ digitsPartial ? h('div', { className: 'fs-hint', 'data-flybot': 'face-join-digits', role: 'status', }, tr('Enter 6 digits')) : null, - h('div', { className: 'fs-link-row', 'data-flybot': 'face-link-join' }, + // #1021: a link and a scanned QR are ways into a group that already exists, so the start + // path — building a room out of one number typed together — does not offer them. + starting ? null : h('div', { className: 'fs-link-row', 'data-flybot': 'face-link-join' }, h('input', { className: 'fs-link-input', value: invite.link, type: 'url', autoComplete: 'off', spellCheck: false, placeholder: tr(LINK_PLACEHOLDER), 'aria-label': tr('Group invite link'), @@ -1861,13 +1877,13 @@ window.__ModuleLoader__.load({ invite.status ? h('div', { className: 'fs-hint fs-scan-hint', 'data-flybot': 'face-link-status', 'data-tone': invite.status.tone, role: 'status', }, invite.status.text) : null), - preview ? h(InvitePreviewCard, { + preview && !starting ? h(InvitePreviewCard, { preview, busy, kind: 'face-link-preview', onClear: clearInvite, joinLabel: preview.local ? tr('Join group') : tr('Join'), onJoin: () => (preview.local ? joinLocal(preview.link) : joinTicket(preview.link ?? invite.link, preview.ticket)), }) : null, - h('div', { className: 'fs-scan-row' }, + starting ? null : h('div', { className: 'fs-scan-row' }, h('button', { type: 'button', className: 'fs-btn', 'data-flybot': 'face-scan', disabled: !!busy, onClick: beginScan, @@ -2809,21 +2825,23 @@ window.__ModuleLoader__.load({ function makeOpener() { let handler = null let pending = false + let queued = null return { - open() { - if (handler) handler() - else pending = true + // #1021: the row that opens a panel passes which one it is, so the panel can name it. + open(arg) { + if (handler) handler(arg) + else { pending = true; queued = arg } }, bind(fn) { handler = fn - if (pending) { pending = false; fn() } + if (pending) { pending = false; fn(queued) } return () => { if (handler === fn) handler = null } }, } } const facePanel = makeOpener() const addFriendPanel = makeOpener() - const openFaceToFace = () => facePanel.open() + const openFaceToFace = (mode) => facePanel.open(mode === 'start' ? 'start' : 'join') const openAddFriend = () => addFriendPanel.open() function FriendsBots({ remoteChats, actions, roster } = {}) { @@ -2831,6 +2849,7 @@ window.__ModuleLoader__.load({ const [error, setError] = useState('') const [adding, setAdding] = useState(false) const [faceOpen, setFaceOpen] = useState(false) + const [faceMode, setFaceMode] = useState('join') const [shareRequests, setShareRequests] = useState([]) const [acceptNotices, setAcceptNotices] = useState([]) // null until the first status answer: a reload must not toast notices that were already there (#1054). @@ -2867,7 +2886,7 @@ window.__ModuleLoader__.load({ timer?.unref?.() return () => clearInterval(timer) }, []) - useEffect(() => facePanel.bind(() => setFaceOpen(true)), []) + useEffect(() => facePanel.bind(mode => { setFaceMode(mode === 'start' ? 'start' : 'join'); setFaceOpen(true) }), []) useEffect(() => addFriendPanel.bind(() => setAdding(true)), []) const unavailable = String(error).startsWith('Social is not running:') if (unavailable) { @@ -2880,7 +2899,7 @@ window.__ModuleLoader__.load({ h(ShareRequests, { items: shareRequests, actions, onDismiss: dismissShareRequest }), h(FaceRooms, { actions, enabled: true, roster, remoteChats }), adding ? h(AddFriend, { actions, remoteChats, onChange: loadApprovals, onDone: () => setAdding(false) }) : null, - faceOpen ? h(FaceToFaceGroup, { actions, roster, remoteChats, onClose: () => setFaceOpen(false) }) : null) + faceOpen ? h(FaceToFaceGroup, { actions, roster, remoteChats, mode: faceMode, onClose: () => setFaceOpen(false) }) : null) } // Someone accepted an invite to one of my Bots (#1054). One-way stays; this card is the inviter's @@ -3843,18 +3862,27 @@ window.__ModuleLoader__.load({ } offs = [ slots().register('flybot.sidebar.section', { id: 'social', order: 100, component: FriendsBots }), + // #1021: two rows, because joining someone else's group and starting a room are different + // intentions. They open the same screen with the same handlers; the row says which one. slots().register('flybot.new-chat.action', { - id: 'social-face-group', + id: 'social-face-join', order: 15, - // #892: one entry for both ways in: a number entered together, or a group's invite link or QR. - label: JOIN_OR_CREATE, + label: JOIN_BY_NUMBER, hint: JOIN_OR_CREATE_HINT, icon: NearbyIcon, - run: ({ close } = {}) => { openFaceToFace(); close?.() }, + run: ({ close } = {}) => { openFaceToFace('join'); close?.() }, }), slots().register('flybot.new-chat.action', { - id: 'social-add-friend', + id: 'social-face-start', order: 16, + label: START_FACE_ROOM, + hint: START_FACE_HINT, + icon: NearbyIcon, + run: ({ close } = {}) => { openFaceToFace('start'); close?.() }, + }), + slots().register('flybot.new-chat.action', { + id: 'social-add-friend', + order: 17, label: 'Add friend', hint: 'Paste an invite link or code', icon: AddPersonIcon, diff --git a/plugins/social/test/social.test.js b/plugins/social/test/social.test.js index 10dfefc7c..e1d104425 100644 --- a/plugins/social/test/social.test.js +++ b/plugins/social/test/social.test.js @@ -762,7 +762,7 @@ test('client.js registers a sidebar section and @ rows into the FlyBot registry' runInNewContext(readFileSync(new URL('../client.js', import.meta.url), 'utf8'), sandbox) const plugin = registered[0].factory(() => ({ createElement: () => null, useEffect() {}, useState: v => [v, () => {}] })) plugin.apply({}) - assert.deepEqual(entries.map(([slot, entry]) => `${slot}#${entry.id}`), ['flybot.sidebar.section#social', 'flybot.new-chat.action#social-face-group', 'flybot.new-chat.action#social-add-friend', 'flybot.composer.mention#social', 'flybot.account.header#social-handle', 'flybot.bot.details#social-invite', 'flybot.approvals#social-held-reply', 'flybot.settings.page#social', 'flybot.room.overview#social-face', 'flybot.room.members#social-room-invite', 'flybot.session.dock#social-group-strip', 'flybot.session.dock#social-room-end', 'flybot.session.dock#social-approval', 'flybot.session.dock#social-thinking', 'flybot.session.dock#social-paused']) + assert.deepEqual(entries.map(([slot, entry]) => `${slot}#${entry.id}`), ['flybot.sidebar.section#social', 'flybot.new-chat.action#social-face-join', 'flybot.new-chat.action#social-face-start', 'flybot.new-chat.action#social-add-friend', 'flybot.composer.mention#social', 'flybot.account.header#social-handle', 'flybot.bot.details#social-invite', 'flybot.approvals#social-held-reply', 'flybot.settings.page#social', 'flybot.room.overview#social-face', 'flybot.room.members#social-room-invite', 'flybot.session.dock#social-group-strip', 'flybot.session.dock#social-room-end', 'flybot.session.dock#social-approval', 'flybot.session.dock#social-thinking', 'flybot.session.dock#social-paused']) // Transport errors are worded in English from their code, never the localized host text. assert.match(plugin.errorText(Object.assign(new Error('请求失败(HTTP 404)'), { code: 'HTTP_404' })), /^The FlyBot host did not answer \(HTTP 404\)/) assert.equal(plugin.errorText(Object.assign(new Error('x'), { code: 'bot/refused' })), 'x') @@ -3480,7 +3480,8 @@ test('social section has no + Add; Add friend and Face-to-face room live on the assert.equal(closed.nodes.some(n => n.type === 'button' && n.children.includes('Join with a number')), false) assert.equal(closed.nodes.some(n => n.props['data-flybot'] === 'face-join' || n.props['data-flybot'] === 'face-group-entry'), false) assert.ok(closed.entries.some(([slot, entry]) => slot === 'flybot.room.overview' && entry.id === 'social-face')) - assert.ok(closed.entries.some(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-face-group')) + assert.ok(closed.entries.some(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-face-join')) + assert.ok(closed.entries.some(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-face-start')) assert.ok(closed.entries.some(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-add-friend')) assert.equal(closed.entries.some(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-accept-invite'), false) // Copy actions live in Add friend, not in the short + menu (#181). @@ -3544,26 +3545,88 @@ test('social sidebar mount starts status without waiting on a friends roster', a assert.equal(started.includes('social/settings'), false) }) -test('the sidebar + menu gets a Face-to-face room action from social', () => { +test('the sidebar + menu offers joining and starting as two rows (#1021)', () => { const { entries, pluginOf } = loadSocialClient(async () => ({})) pluginOf([]).apply({}) - const row = entries.find(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-face-group') - assert.ok(row) - // #892: one entry for both ways in, a number entered together or a group's invite link. - assert.equal(row[1].label, 'Join or create by number or link') - assert.match(row[1].hint, /number/) - assert.match(row[1].hint, /invite link/) + const row = id => entries.find(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === id) + const join = row('social-face-join') + const start = row('social-face-start') + assert.ok(join) + assert.ok(start) + // #1021: the one label that named a create path and a join path at once is gone. + assert.equal(join[1].label, 'Join by number or link') + assert.equal(start[1].label, 'Start a face-to-face room') + assert.equal(entries.some(([, entry]) => entry.label === 'Join or create by number or link'), false) + // #892: the join row keeps the number-and-link hint; the start row says the number is typed together. + assert.match(join[1].hint, /number/) + assert.match(join[1].hint, /invite link/) + assert.match(start[1].hint, /number/) + assert.doesNotMatch(start[1].hint, /link/) // #385: each social row has its own mark and a short one-line hint with no closing period. - const friend = entries.find(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-add-friend') - assert.equal(typeof row[1].icon, 'function') - assert.equal(typeof friend[1].icon, 'function') - assert.notEqual(row[1].icon, friend[1].icon) - for (const hint of [row[1].hint, friend[1].hint]) { - assert.doesNotMatch(hint, /\.$/) - assert.ok(hint.length <= 45, hint) + const friend = row('social-add-friend') + for (const entry of [join[1], start[1], friend[1]]) { + assert.equal(typeof entry.icon, 'function') + assert.equal(typeof entry.run, 'function') + assert.doesNotMatch(entry.hint, /\.$/) + assert.ok(entry.hint.length <= 45, entry.hint) } - assert.equal(row[1].order, 15) - assert.equal(typeof row[1].run, 'function') + assert.notEqual(join[1].icon, friend[1].icon) + assert.deepEqual([join[1].order, start[1].order, friend[1].order], [15, 16, 17]) +}) + +test('#1021: the join row opens the screen with the 6-digit input and the link field, the start row the one without', () => { + const { registered, entries, walk } = loadSocialClient(async (endpoint) => { + if (endpoint === 'social/status') return { connected: true, bots: [{ id: 'a', name: 'Chief', isMain: true }], approvals: [] } + if (endpoint === 'social/face-rooms') return { rooms: [] } + if (endpoint === 'social/remote-bots') return { homes: [] } + if (endpoint === 'social/settings') return { autoAccept: {} } + return {} + }) + const hooks = [] + let hookI = 0 + const plugin = registered[0].factory(() => ({ + createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat() }), + Fragment: 'F', + useEffect: fn => { fn?.() }, + useState: (init) => { + const i = hookI++ + if (hooks[i] === undefined) hooks[i] = typeof init === 'function' ? init() : init + return [hooks[i], (value) => { hooks[i] = typeof value === 'function' ? value(hooks[i]) : value }] + }, + useRef: v => ({ current: v }), + useMemo: f => f(), + })) + plugin.apply({}) + const section = entries.find(([slot]) => slot === 'flybot.sidebar.section')[1] + hookI = 0 + walk(section.component({})) + const row = id => entries.find(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === id) + const open = id => { + let closed = false + row(id)[1].run({ close: () => { closed = true } }) + assert.equal(closed, true, 'the picker closes behind the row') + hookI = 0 + return walk(section.component({})) + } + const find = (tree, name) => tree.find(n => n.props['data-flybot'] === name) + const hasCodeInput = tree => tree.some(n => n.props['aria-label'] === '6-digit room number') + const hasLinkInput = tree => tree.some(n => n.props.className === 'fs-link-input') + + const join = open('social-face-join') + assert.equal(find(join, 'face-group').props['aria-label'], 'Join by number or link') + assert.equal(find(join, 'face-group-title').children.join(''), 'Join by number or link') + assert.ok(hasCodeInput(join), 'the join-shaped row is the one carrying the number') + assert.ok(hasLinkInput(join), 'and the invite link') + assert.match(find(join, 'face-join-about').children.join(''), /paste a group invite link/) + + const start = open('social-face-start') + assert.equal(find(start, 'face-group').props['aria-label'], 'Start a face-to-face room') + assert.equal(find(start, 'face-group-title').children.join(''), 'Start a face-to-face room') + assert.ok(hasCodeInput(start), 'a room is still built out of one number') + assert.equal(hasLinkInput(start), false, 'a link is a way into a group that already exists') + assert.equal(find(start, 'face-link-join'), undefined) + assert.equal(find(start, 'face-scan'), undefined) + assert.match(find(start, 'face-join-about').children.join(''), /same number/) }) test('sidebar + Add friend opens the join panel in Friends\' Bots; header stays without + Add', () => { @@ -3597,7 +3660,7 @@ test('sidebar + Add friend opens the join panel in Friends\' Bots; header stays assert.equal(before.some(n => n.props['data-flybot'] === 'add-friend'), false) const add = entries.find(([slot, entry]) => slot === 'flybot.new-chat.action' && entry.id === 'social-add-friend') assert.equal(add[1].label, 'Add friend') - assert.equal(add[1].order, 16) + assert.equal(add[1].order, 17) const closed = [] add[1].run({ close: () => closed.push(true) }) assert.deepEqual(closed, [true]) @@ -11691,8 +11754,9 @@ test('#1064: a pending claim is named on the Members card and Seat them asks the test('#892: the dialog names both ways in: a number entered together, or a group invite link or its QR', () => { const tree = localLinkDialog(async () => ({}), { link: '', status: null }).render() const find = name => tree.find(n => n.props['data-flybot'] === name) - assert.equal(find('face-group').props['aria-label'], 'Join or create by number or link') - assert.equal(find('face-group-title').children.join(''), 'Join or create by number or link') + // #1021: the screen is titled after the row that opened it; this one is the join path. + assert.equal(find('face-group').props['aria-label'], 'Join by number or link') + assert.equal(find('face-group-title').children.join(''), 'Join by number or link') const about = find('face-join-about').children.join('') assert.match(about, /same 6-digit number/) assert.match(about, /paste a group invite link or scan its QR code/)