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 00000000..c52dff14 --- /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 8d17e47b..1fd075ba 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 32104873..0233eccd 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 f8dc757f..de8291ac 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 c4294789..ad9e80a7 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 2eb56a81..fc0162fe 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 73f08cb5..59ba66bf 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 884ecf58..1d9ba501 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 1e228af7..edcb3de5 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 10dfefc7..e1d10442 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/)