@khorsheed/dsh-room 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.en.md +29 -62
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +28 -61
  5. package/lib/client.js +14620 -5705
  6. package/lib/index.js +4069 -730
  7. package/lib/journal-B4FOGO6S.js +482 -0
  8. package/lib/journal-Rid9AAnD.js +485 -0
  9. package/lib/tool.js +112 -166
  10. package/lib/typert.host.js +2031 -374
  11. package/lib/typert.remote-client.d.ts +34 -10
  12. package/lib/typert.remote-client.js +1599 -210
  13. package/lib/types/agent-setup.d.ts +1 -1
  14. package/lib/types/client/InviteAgentAction.js +4 -16
  15. package/lib/types/client/InviteDialog.d.ts +28 -6
  16. package/lib/types/client/InviteDialog.js +76 -12
  17. package/lib/types/client/MemberCard.d.ts +5 -1
  18. package/lib/types/client/MemberCard.js +4 -4
  19. package/lib/types/client/MembersView.d.ts +1 -1
  20. package/lib/types/client/MembersView.js +58 -22
  21. package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
  22. package/lib/types/client/RemoveMemberConfirm.js +85 -0
  23. package/lib/types/client/RoomApprovalView.d.ts +11 -0
  24. package/lib/types/client/RoomApprovalView.js +28 -0
  25. package/lib/types/client/RoomComposer.d.ts +1 -1
  26. package/lib/types/client/RoomComposer.js +195 -29
  27. package/lib/types/client/RoomDecisionView.d.ts +22 -0
  28. package/lib/types/client/RoomDecisionView.js +35 -0
  29. package/lib/types/client/RoomHelpView.d.ts +17 -0
  30. package/lib/types/client/RoomHelpView.js +56 -0
  31. package/lib/types/client/RoomInboxView.d.ts +10 -0
  32. package/lib/types/client/RoomInboxView.js +14 -0
  33. package/lib/types/client/RoomQuestionView.d.ts +13 -0
  34. package/lib/types/client/RoomQuestionView.js +61 -0
  35. package/lib/types/client/RoomRelayView.js +1 -1
  36. package/lib/types/client/RoomRunView.d.ts +3 -1
  37. package/lib/types/client/RoomRunView.js +12 -5
  38. package/lib/types/client/RoomSettingsView.d.ts +19 -0
  39. package/lib/types/client/RoomSettingsView.js +30 -0
  40. package/lib/types/client/RoomSpeechView.d.ts +22 -8
  41. package/lib/types/client/RoomSpeechView.js +94 -11
  42. package/lib/types/client/RoomStatsLine.d.ts +26 -5
  43. package/lib/types/client/RoomStatsLine.js +53 -7
  44. package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
  45. package/lib/types/client/RoomTaskCapsule.js +37 -0
  46. package/lib/types/client/TasksTab.d.ts +41 -0
  47. package/lib/types/client/TasksTab.js +43 -0
  48. package/lib/types/client/TasksView.d.ts +44 -0
  49. package/lib/types/client/TasksView.js +149 -0
  50. package/lib/types/client/TeamView.d.ts +33 -0
  51. package/lib/types/client/TeamView.js +210 -0
  52. package/lib/types/client/at-menu.d.ts +127 -0
  53. package/lib/types/client/at-menu.js +173 -0
  54. package/lib/types/client/draft-chips.d.ts +67 -0
  55. package/lib/types/client/draft-chips.js +95 -0
  56. package/lib/types/client/executions.d.ts +0 -30
  57. package/lib/types/client/executions.js +4 -34
  58. package/lib/types/client/icons-local.d.ts +25 -0
  59. package/lib/types/client/icons-local.js +10 -0
  60. package/lib/types/client/icons.d.ts +2 -1
  61. package/lib/types/client/icons.js +1 -0
  62. package/lib/types/client/index.js +276 -72
  63. package/lib/types/client/locales.d.ts +482 -52
  64. package/lib/types/client/locales.js +490 -60
  65. package/lib/types/client/nodes.d.ts +112 -17
  66. package/lib/types/client/nodes.js +197 -26
  67. package/lib/types/client/slots.d.ts +117 -47
  68. package/lib/types/client/task-tag.d.ts +32 -0
  69. package/lib/types/client/task-tag.js +33 -0
  70. package/lib/types/client/tasks.d.ts +50 -0
  71. package/lib/types/client/tasks.js +82 -0
  72. package/lib/types/client/team.d.ts +73 -0
  73. package/lib/types/client/team.js +187 -0
  74. package/lib/types/decisions.d.ts +49 -0
  75. package/lib/types/decisions.js +101 -0
  76. package/lib/types/deliveries.d.ts +17 -1
  77. package/lib/types/deliveries.js +82 -6
  78. package/lib/types/dispatch.d.ts +102 -4
  79. package/lib/types/dispatch.js +498 -109
  80. package/lib/types/help.d.ts +137 -0
  81. package/lib/types/help.js +216 -0
  82. package/lib/types/helps.d.ts +26 -0
  83. package/lib/types/helps.js +47 -0
  84. package/lib/types/index.d.ts +292 -52
  85. package/lib/types/index.js +1421 -199
  86. package/lib/types/jobs.d.ts +22 -0
  87. package/lib/types/jobs.js +77 -0
  88. package/lib/types/journal.d.ts +17 -27
  89. package/lib/types/journal.js +120 -43
  90. package/lib/types/messaging.d.ts +65 -0
  91. package/lib/types/messaging.js +160 -0
  92. package/lib/types/plan-service.js +7 -1
  93. package/lib/types/report-gate.d.ts +81 -0
  94. package/lib/types/report-gate.js +89 -0
  95. package/lib/types/tool-definition.d.ts +43 -20
  96. package/lib/types/tool-definition.js +145 -212
  97. package/lib/types/tool.d.ts +2 -2
  98. package/lib/types/tool.js +3 -2
  99. package/lib/types/types.d.ts +755 -78
  100. package/locale/zh.json +1 -1
  101. package/package.json +3 -3
  102. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  103. package/lib/types/client/RoomActivityDock.js +0 -105
  104. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  105. package/lib/types/client/RoomDockCapsules.js +0 -277
  106. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  107. package/lib/types/client/RoomPlanTab.js +0 -13
  108. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  109. package/lib/types/client/RoomTaskLineView.js +0 -21
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Cross-session help, on both ends.
4
+ *
5
+ * - `room-help` (the asking room): one line per thread — who asked which
6
+ * session, where it stands — that opens to the request and every message
7
+ * on the thread (the helper's questions, its reply with the handoff, the
8
+ * asker's follow-ups), plus a jump into the helper's session.
9
+ * - `room-help-request` (the helper's session): the request the room queued
10
+ * there, as a card naming the asking room and member, with a jump back.
11
+ */
12
+ import { useEffect, useState, useSyncExternalStore } from 'react';
13
+ import { NUDGE_AFTER } from '../helps.js';
14
+ import css from './RoomHelpView.module.css';
15
+ const NO_STORE = () => () => { };
16
+ /** Default minutes before an unanswered request turns amber (the room setting's default). */
17
+ const REMINDER_MINUTES = 30;
18
+ /** The thread row in the asking room. */
19
+ export function RoomHelpView({ node, sessionId, roomStore, openSession, linkSession, dismissNudge, t }) {
20
+ const help = node.data.help;
21
+ const [open, setOpen] = useState(false);
22
+ // An unanswered request (queued, or the helper still on it) past the room's
23
+ // reminder turns amber with how long it has waited; nothing is cancelled.
24
+ const state = useSyncExternalStore(roomStore?.subscribe ?? NO_STORE, () => roomStore?.getCached(sessionId));
25
+ const minutes = state?.settings?.helpReminderMinutes ?? REMINDER_MINUTES;
26
+ // The third ask of one plain session, once answered: a one-time hint to keep
27
+ // it on the member list (unless it is already there, or the hint was dismissed).
28
+ const target = help.target.sessionId;
29
+ const asks = (state?.helps ?? []).filter(entry => entry.target.sessionId === target && entry.target.room !== true);
30
+ const nudge = help.state === 'replied' && help.target.room !== true && linkSession !== undefined
31
+ && asks.length >= NUDGE_AFTER && asks[NUDGE_AFTER - 1]?.id === help.id
32
+ && state?.linked?.some(entry => entry.sessionId === target) !== true
33
+ && state?.helpNudgesDismissed?.includes(target) !== true;
34
+ const waiting = help.state === 'queued' || help.state === 'running';
35
+ const [now, setNow] = useState(() => Date.now());
36
+ useEffect(() => {
37
+ if (!waiting)
38
+ return;
39
+ const timer = setInterval(() => { setNow(Date.now()); }, 30_000);
40
+ return () => { clearInterval(timer); };
41
+ }, [waiting]);
42
+ const waited = Math.floor((now - help.since) / 60_000);
43
+ const overdue = waiting && waited >= minutes;
44
+ return (_jsxs("div", { className: css.thread, "data-state": help.state, "data-overdue": overdue ? '' : undefined, children: [_jsxs("div", { className: css.line, children: [_jsx("span", { "aria-hidden": true, className: css.icon, children: help.state === 'replied' ? '✓' : help.state === 'failed' ? '!' : '↗' }), _jsx("span", { className: css.text, children: t('help.title', { from: help.fromName, target: help.target.title }) }), _jsxs("span", { className: css.tag, "data-state": overdue ? 'overdue' : help.state, children: [t(`help.state.${help.state}`), help.state === 'failed' && help.error !== undefined ? `:${help.error}` : '', overdue && t('help.waited', { minutes: waited })] }), _jsx("button", { type: "button", className: css.action, "aria-expanded": open, onClick: () => { setOpen(value => !value); }, children: t(open ? 'help.collapse' : 'help.expand') }), _jsx("button", { type: "button", className: css.action, onClick: () => { openSession(help.target.sessionId); }, children: t('help.open') })] }), nudge && (_jsxs("div", { className: css.nudge, children: [_jsx("span", { children: t('help.nudge', { count: asks.length }) }), _jsx("button", { type: "button", className: css.action, onClick: () => { linkSession(target, help.target.title); }, children: t('help.nudgeLink') }), dismissNudge !== undefined && _jsx("button", { type: "button", className: css.action, onClick: () => { dismissNudge(target); }, children: t('help.nudgeDismiss') })] })), open && (_jsxs("div", { className: css.body, children: [_jsxs("div", { className: css.entry, children: [_jsx("span", { className: css.label, children: t('help.request') }), _jsx("p", { className: css.copy, children: help.text })] }), help.messages.map((message, index) => (_jsxs("div", { className: css.entry, "data-kind": message.kind, children: [_jsx("span", { className: css.label, children: message.kind === 'message'
45
+ ? t('help.msg.message', { from: help.fromName })
46
+ : t(message.kind === 'reply' ? (message.fallback === true ? 'help.msg.fallback' : 'help.msg.reply') : 'help.msg.question') }), _jsx("p", { className: css.copy, children: message.text }), message.handoff !== undefined && _jsxs("p", { className: css.handoff, children: [_jsx("b", { children: t('help.handoff') }), message.handoff] })] }, index)))] }))] }));
47
+ }
48
+ /** Lines past which the request card folds. */
49
+ const FOLD_LINES = 6;
50
+ /** The request card in the helper's session. */
51
+ export function RoomHelpRequestView({ node, openSession, t }) {
52
+ const data = node.data;
53
+ const [open, setOpen] = useState(false);
54
+ const long = data.text.split('\n').length > FOLD_LINES || data.text.length > 360;
55
+ return (_jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.cardHead, children: [_jsx("span", { "aria-hidden": true, className: css.icon, children: "\u2198" }), _jsxs("span", { className: css.cardTitle, children: [t(data.op === 'message' ? 'help.card.message' : 'help.card.request', { room: data.roomTitle, from: data.from }), data.member !== undefined && t('help.card.to', { member: data.member })] }), data.held === true && _jsx("span", { className: css.tag, children: t('help.card.held') }), _jsx("button", { type: "button", className: css.action, onClick: () => { openSession(data.room); }, children: t('help.card.back') })] }), _jsx("p", { className: css.cardBody, "data-folded": long && !open ? 'true' : undefined, children: data.text }), long && (_jsx("button", { type: "button", className: css.more, onClick: () => { setOpen(value => !value); }, children: t(open ? 'help.card.less' : 'help.card.more') }))] }));
56
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Held-reports line ('room-inbox' keyed renderer): the report gate kept some
3
+ * results in a member's inbox instead of waking it — one dim line saying so,
4
+ * with the model's verdict and a 现在交给 button that wakes the member now.
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ import type { RoomInboxViewProps } from './slots.ts';
8
+ /** The held-reports line. */
9
+ export declare function RoomInboxView({ node, releaseReports, t }: RoomInboxViewProps): ReactNode;
10
+ //# sourceMappingURL=RoomInboxView.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Held-reports line ('room-inbox' keyed renderer): the report gate kept some
4
+ * results in a member's inbox instead of waking it — one dim line saying so,
5
+ * with the model's verdict and a 现在交给 button that wakes the member now.
6
+ */
7
+ import { useState } from 'react';
8
+ import css from './RoomInboxView.module.css';
9
+ /** The held-reports line. */
10
+ export function RoomInboxView({ node, releaseReports, t }) {
11
+ const data = node.data;
12
+ const [busy, setBusy] = useState(false);
13
+ return (_jsxs("div", { className: css.row, role: "status", children: [_jsxs("span", { className: css.text, children: [t('inbox.line', { member: data.member, count: data.held.length }), data.probability !== undefined && _jsx("span", { className: css.why, children: t('inbox.why', { p: data.probability.toFixed(2) }) })] }), _jsx("button", { type: "button", className: css.action, disabled: busy, onClick: () => { setBusy(true); void releaseReports(data.member).finally(() => { setBusy(false); }); }, children: t('inbox.release', { member: data.member }) })] }));
14
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A member's question card ('room-question' keyed renderer): the questions a
3
+ * CLI member asked through its bridge's `ask_user_question`, answered by
4
+ * picking — one option (or several on a multi-pick question) plus an optional
5
+ * 其他 free-text line — instead of a numbered list in the member's reply that
6
+ * the human answers by typing. Submitting resolves the member's waiting call;
7
+ * the card then folds to the given answers (or 已过期 when the call ended).
8
+ */
9
+ import { type ReactNode } from 'react';
10
+ import type { RoomQuestionViewProps } from './slots.ts';
11
+ /** The question card. */
12
+ export declare function RoomQuestionView({ node, answerQuestion, t }: RoomQuestionViewProps): ReactNode;
13
+ //# sourceMappingURL=RoomQuestionView.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A member's question card ('room-question' keyed renderer): the questions a
4
+ * CLI member asked through its bridge's `ask_user_question`, answered by
5
+ * picking — one option (or several on a multi-pick question) plus an optional
6
+ * 其他 free-text line — instead of a numbered list in the member's reply that
7
+ * the human answers by typing. Submitting resolves the member's waiting call;
8
+ * the card then folds to the given answers (or 已过期 when the call ended).
9
+ */
10
+ import { useState } from 'react';
11
+ import { memberColor } from './member-color.js';
12
+ import css from './RoomQuestionView.module.css';
13
+ /** The question card. */
14
+ export function RoomQuestionView({ node, answerQuestion, t }) {
15
+ const data = node.data;
16
+ const [drafts, setDrafts] = useState({});
17
+ const [busy, setBusy] = useState(false);
18
+ const [error, setError] = useState(undefined);
19
+ const pending = data.state === 'pending';
20
+ const draftOf = (id) => drafts[id] ?? { selected: [], custom: '' };
21
+ const setDraft = (id, next) => { setDrafts(current => ({ ...current, [id]: next })); };
22
+ const answered = (id) => draftOf(id).selected.length > 0 || draftOf(id).custom.trim() !== '';
23
+ const complete = data.questions.every(question => answered(question.id));
24
+ const pick = (id, label, multi) => {
25
+ const draft = draftOf(id);
26
+ const has = draft.selected.includes(label);
27
+ setDraft(id, { ...draft, selected: multi ? (has ? draft.selected.filter(entry => entry !== label) : [...draft.selected, label]) : (has ? [] : [label]) });
28
+ };
29
+ const submit = async () => {
30
+ if (busy || !complete)
31
+ return;
32
+ setBusy(true);
33
+ try {
34
+ const answers = data.questions.map((question) => {
35
+ const draft = draftOf(question.id);
36
+ const custom = draft.custom.trim();
37
+ return { id: question.id, selected: draft.selected, ...custom === '' ? {} : { custom } };
38
+ });
39
+ setError(await answerQuestion(data.questionId, answers));
40
+ }
41
+ finally {
42
+ setBusy(false);
43
+ }
44
+ };
45
+ /** The folded answer text of one question. */
46
+ const answerText = (id) => {
47
+ const answer = data.answers?.find(entry => entry.id === id);
48
+ return [...answer?.selected ?? [], ...answer?.custom === undefined ? [] : [answer.custom]].join(';');
49
+ };
50
+ return (_jsxs("div", { className: css.card, "data-state": data.state, style: { '--member-color': memberColor(data.member) }, children: [_jsxs("div", { className: css.head, children: [_jsx("span", { className: css.avatar, "aria-hidden": true, children: data.member.slice(0, 1).toUpperCase() }), _jsx("span", { className: css.title, children: t('question.title', { member: data.member }) }), !pending && _jsx("span", { className: css.status, children: t(data.state === 'answered' ? 'question.answered' : 'question.cancelled') })] }), data.questions.map((question) => {
51
+ const multi = question.multiSelect === true;
52
+ const draft = draftOf(question.id);
53
+ return (_jsxs("fieldset", { className: css.question, disabled: !pending || busy, children: [_jsxs("legend", { className: css.prompt, children: [question.header !== undefined && _jsx("span", { className: css.chip, children: question.header }), _jsx("span", { children: question.question }), pending && multi && _jsx("span", { className: css.hint, children: t('question.multi') })] }), pending ? (_jsxs(_Fragment, { children: [(question.options ?? []).map(option => (_jsxs("label", { className: css.option, "data-picked": draft.selected.includes(option.label), children: [_jsx("input", { type: multi ? 'checkbox' : 'radio', name: `${data.questionId}-${question.id}`, checked: draft.selected.includes(option.label), onChange: () => { pick(question.id, option.label, multi); }, onClick: (event) => {
54
+ // A picked radio unpicks on a second click (the draft allows none).
55
+ if (!multi && draft.selected.includes(option.label)) {
56
+ event.preventDefault();
57
+ pick(question.id, option.label, multi);
58
+ }
59
+ } }), _jsxs("span", { className: css.optionText, children: [_jsx("span", { className: css.optionLabel, children: option.label }), option.description !== undefined && _jsx("span", { className: css.optionDescription, children: option.description })] })] }, option.label))), _jsx("input", { className: css.other, value: draft.custom, "aria-label": t('question.other'), placeholder: (question.options ?? []).length > 0 ? `${t('question.other')}:${t('question.otherPlaceholder')}` : t('question.otherPlaceholder'), onChange: (event) => { setDraft(question.id, { ...draft, custom: event.target.value }); } })] })) : data.state === 'answered' && (_jsx("div", { className: css.answer, children: t('question.answer', { answer: answerText(question.id) }) }))] }, question.id));
60
+ }), pending && (_jsxs("div", { className: css.actions, children: [error !== undefined && _jsx("span", { className: css.error, children: error }), _jsx("button", { type: "button", className: css.submit, disabled: !complete || busy, onClick: () => { void submit(); }, children: t('question.submit') })] }))] }));
61
+ }
@@ -16,5 +16,5 @@ export function RoomRelayView({ node, confirmRelay, dismissRelay, t }) {
16
16
  break;
17
17
  case 'pending': break;
18
18
  }
19
- return (_jsxs("div", { className: css.row, "data-state": data.state, children: [_jsxs("span", { className: css.text, children: [t('relay.line', { from: data.from, to: data.to, content: data.content }), suffix === '' ? '' : ` ${suffix}`] }), data.state === 'pending' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.action, onClick: () => { void confirmRelay(data.relayId); }, children: t('relay.confirm') }), _jsx("button", { type: "button", className: css.action, onClick: () => { void dismissRelay(data.relayId); }, children: t('relay.dismiss') })] }))] }));
19
+ return (_jsxs("div", { className: css.row, "data-state": data.state, children: [_jsxs("span", { className: css.text, children: [data.reason !== undefined && data.state === 'pending' && _jsx("span", { className: css.reason, children: t(`relay.reason.${data.reason}`) }), t('relay.line', { from: data.from, to: data.to, content: data.content }), suffix === '' ? '' : ` ${suffix}`] }), data.state === 'pending' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.action, onClick: () => { void confirmRelay(data.relayId); }, children: data.reason === 'hop-limit' ? t('relay.continue') : t('relay.confirm') }), _jsx("button", { type: "button", className: css.action, onClick: () => { void dismissRelay(data.relayId); }, children: data.reason === 'hop-limit' ? t('relay.stop') : t('relay.dismiss') })] }))] }));
20
20
  }
@@ -6,7 +6,9 @@
6
6
  * no handle), and a trailing stop button cancels through the Remote. A
7
7
  * failed run stays as a dim error row with the journaled reason (truncated,
8
8
  * the full text on hover); done/cancelled are hidden by the Definition and
9
- * never reach here.
9
+ * never reach here. A RUNNING member's row takes the speech card's shape —
10
+ * avatar and light card, the row as the card's header, the live output
11
+ * below — so the reply card that replaces it lands in the same place.
10
12
  */
11
13
  import { type ReactNode } from 'react';
12
14
  import type { RoomRunViewProps } from './slots.ts';
@@ -7,13 +7,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  * no handle), and a trailing stop button cancels through the Remote. A
8
8
  * failed run stays as a dim error row with the journaled reason (truncated,
9
9
  * the full text on hover); done/cancelled are hidden by the Definition and
10
- * never reach here.
10
+ * never reach here. A RUNNING member's row takes the speech card's shape —
11
+ * avatar and light card, the row as the card's header, the live output
12
+ * below — so the reply card that replaces it lands in the same place.
11
13
  */
12
14
  import { useEffect, useState, useSyncExternalStore } from 'react';
13
15
  import { StateDot, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
14
16
  import { IconStopFillMedium } from './icons.js';
15
17
  import { formatDurationMs } from './format.js';
18
+ import { memberColor } from './member-color.js';
16
19
  import css from './RoomRunView.module.css';
20
+ import speech from './RoomSpeechView.module.css';
17
21
  /** The member run row. */
18
22
  export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMember, renderMemberOutput, t }) {
19
23
  const state = useSyncExternalStore(roomStore.subscribe, () => roomStore.getCached(sessionId));
@@ -43,8 +47,11 @@ export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMem
43
47
  jump();
44
48
  }
45
49
  };
46
- return (_jsxs("div", { children: [_jsxs("div", { className: css.run, "data-state": failed ? 'failed' : 'running', role: "button", tabIndex: 0, onClick: jump, onKeyDown: onKeyDown, children: [_jsx(StateDot, { state: failed ? 'error' : 'ongoing' }), _jsx("span", { className: css.title, children: failed ? t('run.failed', { member: data.member }) : t('run.working', { member: data.member }) }), _jsxs("span", { className: css.elapsed, "aria-hidden": true, children: ["\u00B7 ", elapsed === undefined ? t('run.durationUnknown') : formatDurationMs(elapsed)] }), failed && data.error !== undefined && (_jsx("span", { className: css.reason, title: data.error, children: data.error })), !failed && (_jsx(Tooltip, { label: t('run.stop'), side: "bottom", children: _jsx("button", { type: "button", className: css.stop, "aria-label": t('run.stop'), onClick: (event) => {
47
- event.stopPropagation();
48
- void cancelMember(data.member);
49
- }, children: _jsx(IconStopFillMedium, {}) }) }))] }), !failed && childSessionId !== undefined && renderMemberOutput?.(childSessionId, data.startedAt)] }));
50
+ const row = (_jsxs("div", { className: css.run, "data-state": failed ? 'failed' : 'running', role: "button", tabIndex: 0, onClick: jump, onKeyDown: onKeyDown, children: [_jsx(StateDot, { state: failed ? 'error' : 'ongoing' }), _jsx("span", { className: css.title, children: failed ? t('run.failed', { member: data.member }) : t('run.working', { member: data.member }) }), _jsxs("span", { className: css.elapsed, "aria-hidden": true, children: ["\u00B7 ", elapsed === undefined ? t('run.durationUnknown') : formatDurationMs(elapsed)] }), failed && data.error !== undefined && (_jsx("span", { className: css.reason, title: data.error, children: data.error })), !failed && (_jsx(Tooltip, { label: t('run.stop'), side: "bottom", children: _jsx("button", { type: "button", className: css.stop, "aria-label": t('run.stop'), onClick: (event) => {
51
+ event.stopPropagation();
52
+ void cancelMember(data.member);
53
+ }, children: _jsx(IconStopFillMedium, {}) }) }))] }));
54
+ if (failed)
55
+ return _jsx("div", { children: row });
56
+ return (_jsxs("div", { className: speech.speech, style: { '--member-color': memberColor(data.member) }, children: [_jsx("span", { className: speech.avatar, "aria-hidden": true, children: data.member.slice(0, 1).toUpperCase() }), _jsxs("div", { className: `${speech.card} ${css.liveCard}`, children: [row, childSessionId !== undefined && renderMemberOutput?.(childSessionId, data.startedAt)] })] }));
50
57
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The members tab's 协作设置, built on the host primitives so it reads like
3
+ * the host's own settings: a full-row DisclosureRow (closed by default, the
4
+ * current values as its collapsed summary), SegmentedControls for the
5
+ * choices and Switches. Two groups: 协作 (how members message each other,
6
+ * the hop limit of one chain of messages, pause) and 自动判断 — the room's
7
+ * decision scenarios (decisions.ts), one switch each; report timing carries
8
+ * its threshold and longest wait in a box of its own.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { RoomSettings } from '../types.ts';
12
+ import type { MembersViewProps, RoomMutationOutcome } from './slots.ts';
13
+ /** The settings block. */
14
+ export declare function RoomSettingsView({ settings, onChange, t }: {
15
+ readonly settings: RoomSettings;
16
+ readonly onChange: (patch: Partial<RoomSettings>) => Promise<RoomMutationOutcome>;
17
+ readonly t: MembersViewProps['t'];
18
+ }): ReactNode;
19
+ //# sourceMappingURL=RoomSettingsView.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The members tab's 协作设置, built on the host primitives so it reads like
4
+ * the host's own settings: a full-row DisclosureRow (closed by default, the
5
+ * current values as its collapsed summary), SegmentedControls for the
6
+ * choices and Switches. Two groups: 协作 (how members message each other,
7
+ * the hop limit of one chain of messages, pause) and 自动判断 — the room's
8
+ * decision scenarios (decisions.ts), one switch each; report timing carries
9
+ * its threshold and longest wait in a box of its own.
10
+ */
11
+ import { useState } from 'react';
12
+ import { DisclosureRow, SegmentedControl, Switch } from '@deepseek-ai/dsh-client-ui-primitives';
13
+ import { IconAgentPresetOutlineMedium } from './icons.js';
14
+ import css from './RoomSettingsView.module.css';
15
+ /** The settings block. */
16
+ export function RoomSettingsView({ settings, onChange, t }) {
17
+ const [open, setOpen] = useState(false);
18
+ const [error, setError] = useState(null);
19
+ const change = (patch) => {
20
+ void onChange(patch).then(outcome => { setError(outcome.ok ? null : outcome.message); });
21
+ };
22
+ const judged = settings.reportTiming === 'judge';
23
+ const step = (value, delta, min, max) => Math.min(max, Math.max(min, Math.round((value + delta) * 10) / 10));
24
+ const summary = [
25
+ settings.memberMessages === 'free' ? t('settings.free') : t('settings.confirm'),
26
+ t('settings.hopSummary', { count: settings.hopLimit }),
27
+ ...settings.paused ? [t('settings.pausedSummary')] : [],
28
+ ].join(' · ');
29
+ return (_jsx("div", { className: css.settings, children: _jsx(DisclosureRow, { icon: _jsx(IconAgentPresetOutlineMedium, {}), title: t('settings.title'), open: open, expandable: true, expandOnRowClick: true, keepContentWhenOpen: true, onToggle: () => { setOpen(value => !value); }, collapsedContent: _jsx("span", { className: settings.paused ? css.valuesPaused : css.values, children: summary }), children: _jsxs("div", { className: css.rows, children: [_jsx("div", { className: css.group, children: t('settings.groupCollab') }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.messages') }), _jsx("span", { children: t('settings.messagesHint') })] }), _jsx(SegmentedControl, { id: "room-settings-messages", label: t('settings.messages'), value: settings.memberMessages, options: [{ value: 'free', label: t('settings.free') }, { value: 'confirm', label: t('settings.confirm') }], onChange: (memberMessages) => { change({ memberMessages }); } })] }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.hops') }), _jsx("span", { children: t('settings.hopsHint') })] }), _jsxs("span", { className: css.stepper, children: [_jsx("button", { type: "button", "aria-label": t('settings.hopsLess'), disabled: settings.hopLimit <= 2, onClick: () => { change({ hopLimit: settings.hopLimit - 1 }); }, children: "\u2212" }), _jsx("b", { "aria-live": "polite", children: settings.hopLimit }), _jsx("button", { type: "button", "aria-label": t('settings.hopsMore'), disabled: settings.hopLimit >= 50, onClick: () => { change({ hopLimit: settings.hopLimit + 1 }); }, children: "\uFF0B" })] })] }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.pause') }), _jsx("span", { children: t('settings.pauseHint') })] }), _jsx(Switch, { checked: settings.paused, label: t('settings.pause'), onChange: (paused) => { change({ paused }); } })] }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.helpReminder') }), _jsx("span", { children: t('settings.helpReminderHint') })] }), _jsxs("span", { className: css.stepper, children: [_jsx("button", { type: "button", "aria-label": t('settings.less'), disabled: settings.helpReminderMinutes <= 10, onClick: () => { change({ helpReminderMinutes: settings.helpReminderMinutes - 10 }); }, children: "\u2212" }), _jsx("b", { "aria-live": "polite", children: t('settings.minutes', { count: settings.helpReminderMinutes }) }), _jsx("button", { type: "button", "aria-label": t('settings.more'), disabled: settings.helpReminderMinutes >= 120, onClick: () => { change({ helpReminderMinutes: settings.helpReminderMinutes + 10 }); }, children: "\uFF0B" })] })] }), _jsx("div", { className: css.group, children: t('settings.groupAuto') }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.autoFiling') }), _jsx("span", { children: t('settings.autoFilingHint') })] }), _jsx(Switch, { checked: settings.autoTaskFiling, label: t('settings.autoFiling'), onChange: (autoTaskFiling) => { change({ autoTaskFiling }); } })] }), _jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.autoReport') }), _jsx("span", { children: t('settings.autoReportHint') })] }), _jsx(Switch, { checked: judged, label: t('settings.autoReport'), onChange: (on) => { change({ reportTiming: on ? 'judge' : 'each' }); } })] }), judged && (_jsxs("div", { className: css.subbox, children: [_jsxs("div", { className: css.subrow, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.reportThreshold') }), _jsx("span", { children: t('settings.reportThresholdHint') })] }), _jsxs("span", { className: css.stepper, children: [_jsx("button", { type: "button", "aria-label": t('settings.less'), disabled: settings.reportThreshold <= 0.1, onClick: () => { change({ reportThreshold: step(settings.reportThreshold, -0.1, 0.1, 0.9) }); }, children: "\u2212" }), _jsx("b", { "aria-live": "polite", children: settings.reportThreshold.toFixed(1) }), _jsx("button", { type: "button", "aria-label": t('settings.more'), disabled: settings.reportThreshold >= 0.9, onClick: () => { change({ reportThreshold: step(settings.reportThreshold, 0.1, 0.1, 0.9) }); }, children: "\uFF0B" })] })] }), _jsxs("div", { className: css.subrow, children: [_jsxs("span", { className: css.label, children: [_jsx("b", { children: t('settings.reportWait') }), _jsx("span", { children: t('settings.reportWaitHint') })] }), _jsxs("span", { className: css.stepper, children: [_jsx("button", { type: "button", "aria-label": t('settings.less'), disabled: settings.reportWaitMinutes <= 1, onClick: () => { change({ reportWaitMinutes: settings.reportWaitMinutes - 1 }); }, children: "\u2212" }), _jsx("b", { "aria-live": "polite", children: t('settings.minutes', { count: settings.reportWaitMinutes }) }), _jsx("button", { type: "button", "aria-label": t('settings.more'), disabled: settings.reportWaitMinutes >= 30, onClick: () => { change({ reportWaitMinutes: settings.reportWaitMinutes + 1 }); }, children: "\uFF0B" })] })] })] })), error !== null && _jsx("p", { className: css.error, role: "alert", children: error })] }) }) }));
30
+ }
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Member speech node ('room-speech' keyed renderer): a 2px member-color rail
3
- * bands the whole block (identity row: 16px member-color dot + refChip-styled
4
- * name capsule + provider in tertiary grey; the unframed full-column markdown
5
- * body — the official MarkdownText, typographically identical to main-agent
6
- * speech; and the action row), so one glance tells WHOSE paragraph it is. A
7
- * body past {@link COLLAPSE_CHARS} starts clamped to ~10 lines under a
8
- * bg-base fade with an expand/collapse toggle. The action row replicates the
2
+ * Member speech node ('room-speech' keyed renderer): a conversation entry —
3
+ * the member's color avatar beside a light card that blocks off one member's
4
+ * turn (several agents take turns in a room, so each turn reads as its own
5
+ * block): a header (name, public role, provider, time), the markdown body —
6
+ * the official MarkdownText, typographically identical to main-agent speech —
7
+ * and a quiet footer row. A
8
+ * body past {@link COLLAPSE_CHARS} may fold to ~10 lines under a bg-base fade
9
+ * with an expand/collapse toggle (the fade expands too). The newest reply to
10
+ * the room starts unfolded — it is what the human is reading — and folds once
11
+ * a newer reply lands; reports to another member and older replies start
12
+ * folded. The human's own expand/collapse wins and is remembered per speech
13
+ * (see {@link foldChoices}). The action row replicates the
9
14
  * official MessageIconActions chrome: copy, child-session jump (when the
10
15
  * speech carries its handle), honest duration, and a hover-revealed clock.
11
16
  * Deliberately absent: branch (forking the room session has undefined
@@ -13,6 +18,15 @@
13
18
  */
14
19
  import { type ReactNode } from 'react';
15
20
  import type { RoomSpeechViewProps } from './slots.ts';
21
+ /**
22
+ * The human's expand/collapse choices for one room's speeches, by speech seq.
23
+ * A per-viewer convenience in browser storage: unreadable storage (private
24
+ * window, blocked site data) only means the default folding applies.
25
+ */
26
+ export declare const foldChoices: {
27
+ read(sessionId: string): Record<string, boolean>;
28
+ write(sessionId: string, seq: number, expanded: boolean): void;
29
+ };
16
30
  /** The member speech row. */
17
- export declare function RoomSpeechView({ node, sessionId, roomStore, openSession, t }: RoomSpeechViewProps): ReactNode;
31
+ export declare function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, t }: RoomSpeechViewProps): ReactNode;
18
32
  //# sourceMappingURL=RoomSpeechView.d.ts.map
@@ -1,12 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Member speech node ('room-speech' keyed renderer): a 2px member-color rail
4
- * bands the whole block (identity row: 16px member-color dot + refChip-styled
5
- * name capsule + provider in tertiary grey; the unframed full-column markdown
6
- * body — the official MarkdownText, typographically identical to main-agent
7
- * speech; and the action row), so one glance tells WHOSE paragraph it is. A
8
- * body past {@link COLLAPSE_CHARS} starts clamped to ~10 lines under a
9
- * bg-base fade with an expand/collapse toggle. The action row replicates the
3
+ * Member speech node ('room-speech' keyed renderer): a conversation entry —
4
+ * the member's color avatar beside a light card that blocks off one member's
5
+ * turn (several agents take turns in a room, so each turn reads as its own
6
+ * block): a header (name, public role, provider, time), the markdown body —
7
+ * the official MarkdownText, typographically identical to main-agent speech —
8
+ * and a quiet footer row. A
9
+ * body past {@link COLLAPSE_CHARS} may fold to ~10 lines under a bg-base fade
10
+ * with an expand/collapse toggle (the fade expands too). The newest reply to
11
+ * the room starts unfolded — it is what the human is reading — and folds once
12
+ * a newer reply lands; reports to another member and older replies start
13
+ * folded. The human's own expand/collapse wins and is remembered per speech
14
+ * (see {@link foldChoices}). The action row replicates the
10
15
  * official MessageIconActions chrome: copy, child-session jump (when the
11
16
  * speech carries its handle), honest duration, and a hover-revealed clock.
12
17
  * Deliberately absent: branch (forking the room session has undefined
@@ -14,9 +19,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
19
  */
15
20
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
16
21
  import { MarkdownText, Tooltip, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
17
- import { IconCheckOutlineMedium, IconCopyOutlineMedium, IconLinkOutlineMedium } from './icons.js';
22
+ import { IconCheckOutlineMedium, IconChevronDownOutlineMedium, IconCopyOutlineMedium, IconLinkOutlineMedium } from './icons.js';
23
+ import { IconRequestDetailOutline16 } from './icons-local.js';
18
24
  import { memberColor } from './member-color.js';
19
25
  import { formatClock, formatDurationMs } from './format.js';
26
+ import { formatTokens } from './RoomStatsLine.js';
20
27
  import css from './RoomSpeechView.module.css';
21
28
  /**
22
29
  * Speeches longer than this start collapsed. Chars, not rendered lines: a
@@ -24,6 +31,35 @@ import css from './RoomSpeechView.module.css';
24
31
  * lines of prose.
25
32
  */
26
33
  const COLLAPSE_CHARS = 600;
34
+ /** Browser storage key of one room's remembered expand/collapse choices. */
35
+ const FOLD_KEY_PREFIX = 'dsh-room:speech-fold:';
36
+ /** Choices kept per room (newest speeches win): bounds the stored map. */
37
+ const FOLD_KEEP = 200;
38
+ /**
39
+ * The human's expand/collapse choices for one room's speeches, by speech seq.
40
+ * A per-viewer convenience in browser storage: unreadable storage (private
41
+ * window, blocked site data) only means the default folding applies.
42
+ */
43
+ export const foldChoices = {
44
+ read(sessionId) {
45
+ try {
46
+ const raw = globalThis.localStorage?.getItem(FOLD_KEY_PREFIX + sessionId);
47
+ const parsed = raw == null ? undefined : JSON.parse(raw);
48
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
49
+ }
50
+ catch {
51
+ return {};
52
+ }
53
+ },
54
+ write(sessionId, seq, expanded) {
55
+ try {
56
+ const choices = { ...foldChoices.read(sessionId), [String(seq)]: expanded };
57
+ const kept = Object.keys(choices).map(Number).sort((a, b) => b - a).slice(0, FOLD_KEEP);
58
+ globalThis.localStorage?.setItem(FOLD_KEY_PREFIX + sessionId, JSON.stringify(Object.fromEntries(kept.map(key => [key, choices[key]]))));
59
+ }
60
+ catch { /* a convenience: never fail the view */ }
61
+ },
62
+ };
27
63
  /** Localized Markdown chrome (code-fence copy buttons, footnotes) over the room namespace. */
28
64
  function markdownLabels(t) {
29
65
  return {
@@ -32,10 +68,11 @@ function markdownLabels(t) {
32
68
  };
33
69
  }
34
70
  /** The member speech row. */
35
- export function RoomSpeechView({ node, sessionId, roomStore, openSession, t }) {
71
+ export function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, t }) {
36
72
  const data = node.data;
37
73
  const [copied, setCopied] = useState(false);
38
- const [expanded, setExpanded] = useState(false);
74
+ // The human's own choice for this speech (remembered), else the default.
75
+ const [chosen, setChosen] = useState(() => foldChoices.read(sessionId)[String(node.data.seq)]);
39
76
  const copyTimer = useRef(null);
40
77
  useEffect(() => () => {
41
78
  if (copyTimer.current !== null)
@@ -47,8 +84,27 @@ export function RoomSpeechView({ node, sessionId, roomStore, openSession, t }) {
47
84
  ? undefined
48
85
  : member.kind === 'main-agent' ? t('member.kind.main') : member.provider;
49
86
  const child = data.childSessionId;
87
+ // The run's billed tokens, as the host's 用量 chip shows them (recorded
88
+ // when the run settles; absent for runs the producer reported none for).
89
+ const execution = data.runId === undefined ? undefined : state?.executions?.find(entry => entry.id === data.runId);
90
+ // The billed total is the sum of the disjoint buckets when they were
91
+ // recorded (an older record's total may leave the cache out).
92
+ const usage = execution?.usage;
93
+ const tokens = usage === undefined ? execution?.tokens
94
+ : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
95
+ // The newest speech keeps its action row shown; older rows reveal on hover
96
+ // (the host's turn-tail rule). A speech newer than the cached state's is the newest.
97
+ const newestSpeech = data.seq >= (state?.latestSpeechSeq ?? data.seq);
50
98
  const collapsible = data.text.length > COLLAPSE_CHARS;
99
+ // The newest reply to the room reads unfolded. A reply newer than the
100
+ // cached state's (the refresh is debounced) is the newest already.
101
+ const newestReply = data.reportedTo === undefined && data.seq >= (state?.latestReplySeq ?? data.seq);
102
+ const expanded = chosen ?? newestReply;
51
103
  const clamped = collapsible && !expanded;
104
+ const toggle = (next) => {
105
+ setChosen(next);
106
+ foldChoices.write(sessionId, data.seq, next);
107
+ };
52
108
  const labels = useMemo(() => markdownLabels(t), [t]);
53
109
  const copy = () => {
54
110
  void writeClipboard(data.text).then((ok) => {
@@ -58,5 +114,32 @@ export function RoomSpeechView({ node, sessionId, roomStore, openSession, t }) {
58
114
  copyTimer.current = setTimeout(() => { setCopied(false); }, 1000);
59
115
  });
60
116
  };
61
- return (_jsxs("div", { className: css.speech, "data-time-hover-root": "", style: { borderLeft: `2px solid ${memberColor(data.member)}` }, children: [_jsxs("div", { className: css.identity, children: [_jsx("span", { className: css.dot, style: { background: memberColor(data.member) }, "aria-hidden": true }), _jsx("span", { className: css.chip, children: data.member }), provider !== undefined && provider !== '' && _jsx("span", { className: css.provider, children: provider }), data.interrupted !== undefined && _jsx("span", { className: css.meta, children: t(data.interrupted === 'cancelled' ? 'speech.stopped' : 'speech.failed') })] }), _jsxs("div", { className: clamped ? `${css.body} ${css.clamped}` : css.body, children: [_jsx(MarkdownText, { text: data.text, labels: labels }), clamped && _jsx("div", { className: css.fade, "aria-hidden": true })] }), collapsible && (_jsx("button", { type: "button", className: css.expandToggle, onClick: () => { setExpanded(value => !value); }, children: expanded ? t('speech.collapse') : t('speech.expand') })), _jsxs("div", { className: css.actions, children: [_jsx(Tooltip, { label: copied ? t('action.copied') : t('action.copy'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": copied ? t('action.copied') : t('action.copy'), onClick: copy, children: copied ? _jsx(IconCheckOutlineMedium, {}) : _jsx(IconCopyOutlineMedium, {}) }) }), child !== undefined && (_jsx(Tooltip, { label: t('speech.jump'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('speech.jump'), onClick: () => { openSession(child); }, children: _jsx(IconLinkOutlineMedium, {}) }) })), data.durationMs !== undefined && (_jsx("span", { className: css.meta, children: formatDurationMs(data.durationMs) })), _jsx("span", { className: css.time, children: formatClock(data.time) })] })] }));
117
+ const color = memberColor(data.member);
118
+ return (_jsxs("div", { className: css.speech, "data-time-hover-root": "", "data-actions-reveal": newestSpeech ? 'always' : 'hover', style: { '--member-color': color }, children: [_jsx("span", { className: css.avatar, "aria-hidden": true, children: data.member.slice(0, 1).toUpperCase() }), _jsxs("div", { className: css.stack, children: [_jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.identity, children: [_jsx("span", { className: css.name, children: data.member }), member?.role !== undefined && _jsx("span", { className: css.role, children: member.role }), provider !== undefined && provider !== '' && _jsx("span", { className: css.provider, children: provider }), data.interrupted !== undefined && _jsx("span", { className: css.interrupted, children: t(data.interrupted === 'cancelled' ? 'speech.stopped' : 'speech.failed') })] }), _jsxs("div", { className: clamped ? `${css.body} ${css.clamped}` : css.body, children: [_jsx(MarkdownText, { text: data.text, labels: labels }), clamped && _jsx("div", { className: css.fade, "aria-hidden": true, onClick: () => { toggle(true); } }), collapsible && (_jsx("div", { className: css.foldBar, children: _jsx(Tooltip, { label: expanded ? t('speech.collapse') : t('speech.expand'), side: "bottom", children: _jsx("button", { type: "button", className: css.foldToggle, "aria-label": expanded ? t('speech.collapse') : t('speech.expand'), "aria-expanded": expanded, onClick: () => { toggle(!expanded); }, children: _jsx(IconChevronDownOutlineMedium, {}) }) }) }))] }), data.reportedTo !== undefined && (_jsx("span", { className: css.reported, children: t('speech.reportedTo', { member: data.reportedTo }) }))] }), _jsxs("div", { className: css.actions, children: [_jsx(Tooltip, { label: copied ? t('action.copied') : t('action.copy'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": copied ? t('action.copied') : t('action.copy'), onClick: copy, children: copied ? _jsx(IconCheckOutlineMedium, {}) : _jsx(IconCopyOutlineMedium, {}) }) }), child !== undefined && (_jsx(Tooltip, { label: t('speech.jump'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('speech.jump'), onClick: () => { openSession(child); }, children: _jsx(IconLinkOutlineMedium, {}) }) })), child !== undefined && data.messageId !== undefined && openDetail !== undefined && (_jsx(Tooltip, { label: t('speech.detail'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('speech.detail'), onClick: () => { openDetail(child, data.messageId); }, children: _jsx(IconRequestDetailOutline16, {}) }) })), tokens !== undefined && tokens > 0
119
+ ? _jsx(UsageChip, { tokens: tokens, execution: execution, provider: member?.kind === 'cli' ? member.provider : undefined, durationMs: data.durationMs, t: t })
120
+ : data.durationMs !== undefined && _jsx("span", { className: css.meta, children: formatDurationMs(data.durationMs) }), _jsx("span", { className: css.meta, children: formatClock(data.time) })] })] })] }));
121
+ }
122
+ /**
123
+ * 用量 with its breakdown on hover or focus — the host's turn usage panel:
124
+ * total, provider / model, cache hit, the disjoint input buckets, output, and
125
+ * this round's run time. Rows the harness did not report are left out.
126
+ */
127
+ function UsageChip({ tokens, execution, provider, durationMs, t }) {
128
+ const [open, setOpen] = useState(false);
129
+ const usage = execution?.usage;
130
+ const billed = usage === undefined ? 0 : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
131
+ const exact = (n) => n.toLocaleString('en-US');
132
+ const route = [provider, execution?.model].filter(Boolean).join('/');
133
+ const rows = [
134
+ ...route === '' ? [] : [[t('usage.route'), route]],
135
+ ...usage !== undefined && billed > 0 ? [[t('usage.cacheHit'), `${Math.round((usage.cacheReadTokens ?? 0) / billed * 1000) / 10}%`]] : [],
136
+ ...usage === undefined ? [] : [
137
+ [t('usage.uncached'), `${exact(usage.inputTokens)} tok`],
138
+ ...usage.cacheReadTokens === undefined ? [] : [[t('usage.cacheRead'), `${exact(usage.cacheReadTokens)} tok`]],
139
+ ...usage.cacheWriteTokens === undefined ? [] : [[t('usage.cacheWrite'), `${exact(usage.cacheWriteTokens)} tok`]],
140
+ [t('usage.output'), `${exact(usage.outputTokens)} tok`],
141
+ ],
142
+ ...durationMs === undefined ? [] : [[t('usage.duration'), formatDurationMs(durationMs)]],
143
+ ];
144
+ return (_jsxs("span", { className: css.usage, onMouseEnter: () => { setOpen(true); }, onMouseLeave: () => { setOpen(false); }, children: [_jsx("button", { type: "button", className: css.usageChip, "aria-expanded": open, onFocus: () => { setOpen(true); }, onBlur: () => { setOpen(false); }, children: t('speech.usage', { tokens: formatTokens(tokens) }) }), open && (_jsxs("span", { className: css.usagePanel, role: "tooltip", children: [_jsxs("span", { className: css.usageHead, children: [_jsx("span", { children: t('usage.title') }), _jsxs("span", { children: [exact(tokens), " tok"] })] }), rows.map(([label, value]) => _jsxs("span", { className: css.usageRow, children: [_jsx("span", { children: label }), _jsx("span", { children: value })] }, label))] }))] }));
62
145
  }
@@ -9,14 +9,17 @@
9
9
  * degrade). Unlike the official row there is no window-fold fallback — the
10
10
  * takeover has no business re-deriving what the host already projects.
11
11
  *
12
- * Semantics: the figures are the ROOM SESSION's own — its main agent's
13
- * turns and billing. CLI members consume in their own child sessions, so
14
- * this row is never a whole-room aggregate.
12
+ * Semantics: the projected figures are the ROOM SESSION's own — its main
13
+ * agent's turns and billing. Members consume in their own child sessions, so
14
+ * when the room has member runs the row adds a members chip (how many
15
+ * members worked, their billed tokens) whose breakdown lists each member's
16
+ * model, rounds, tokens and run time from the room's execution records.
15
17
  */
16
18
  import { type ReactNode } from 'react';
17
19
  import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client';
18
20
  import type { TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client';
19
21
  import type { RoomComposerProps } from './slots.ts';
22
+ import type { RoomState } from '../types.ts';
20
23
  /**
21
24
  * Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
22
25
  * digits). Copied from the official StatsLine.
@@ -52,10 +55,28 @@ export declare function billedInputTokens(usage: TokenUsageProjection): number;
52
55
  * @returns rounded integer percent, or null when no input was billed.
53
56
  */
54
57
  export declare function cacheHitPercent(usage: TokenUsageProjection): number | null;
55
- /** Props: the projection read seat plus the room locale seat. */
58
+ /** One member's share of the room's work. */
59
+ export interface MemberUsage {
60
+ readonly member: string;
61
+ readonly model?: string;
62
+ readonly rounds: number;
63
+ readonly tokens: number;
64
+ readonly elapsedMs: number;
65
+ }
66
+ /**
67
+ * Per-member usage from the room's execution records: every run but the
68
+ * main agent's (its turns are the projected figures already), in roster
69
+ * order. Members without a run do not appear.
70
+ * @param state - the replayed room state.
71
+ * @returns one row per member that ran.
72
+ */
73
+ export declare function memberUsage(state: RoomState | undefined): readonly MemberUsage[];
74
+ /** Props: the projection read seat, the room state, and the room locale seat. */
56
75
  export interface RoomStatsLineProps {
57
76
  readonly useProjection: UseProjection;
77
+ /** The room's replayed state: the members chip reads its execution records. */
78
+ readonly state?: RoomState | undefined;
58
79
  readonly t: RoomComposerProps['t'];
59
80
  }
60
- export declare const RoomStatsLine: import("react").MemoExoticComponent<({ useProjection, t }: RoomStatsLineProps) => ReactNode>;
81
+ export declare const RoomStatsLine: import("react").MemoExoticComponent<({ useProjection, state, t }: RoomStatsLineProps) => ReactNode>;
61
82
  //# sourceMappingURL=RoomStatsLine.d.ts.map