@khorsheed/dsh-room 0.2.4 → 0.2.5

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 (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/client.js +2256 -1074
  3. package/lib/index.js +548 -36
  4. package/lib/typert.host.js +249 -65
  5. package/lib/typert.remote-client.d.ts +4 -1
  6. package/lib/typert.remote-client.js +208 -53
  7. package/lib/types/client/InviteDialog.js +11 -4
  8. package/lib/types/client/RoomAttachments.d.ts +20 -0
  9. package/lib/types/client/RoomAttachments.js +36 -0
  10. package/lib/types/client/RoomComposer.d.ts +6 -12
  11. package/lib/types/client/RoomComposer.js +116 -12
  12. package/lib/types/client/RoomQuestionView.d.ts +10 -2
  13. package/lib/types/client/RoomQuestionView.js +18 -5
  14. package/lib/types/client/RoomRunView.d.ts +1 -1
  15. package/lib/types/client/RoomRunView.js +15 -5
  16. package/lib/types/client/RoomSpeechView.d.ts +24 -1
  17. package/lib/types/client/RoomSpeechView.js +39 -3
  18. package/lib/types/client/RoomStatsLine.d.ts +3 -1
  19. package/lib/types/client/RoomStatsLine.js +3 -1
  20. package/lib/types/client/RoomTaskCapsule.d.ts +6 -1
  21. package/lib/types/client/RoomTaskCapsule.js +2 -2
  22. package/lib/types/client/TasksView.js +1 -1
  23. package/lib/types/client/TeamView.js +1 -1
  24. package/lib/types/client/icons.d.ts +2 -1
  25. package/lib/types/client/icons.js +1 -0
  26. package/lib/types/client/index.js +94 -15
  27. package/lib/types/client/locales.d.ts +44 -0
  28. package/lib/types/client/locales.js +44 -0
  29. package/lib/types/client/nodes.d.ts +22 -3
  30. package/lib/types/client/nodes.js +28 -5
  31. package/lib/types/client/path-mentions.d.ts +34 -0
  32. package/lib/types/client/path-mentions.js +39 -0
  33. package/lib/types/client/slots.d.ts +51 -1
  34. package/lib/types/dispatch.d.ts +50 -0
  35. package/lib/types/dispatch.js +115 -1
  36. package/lib/types/index.d.ts +69 -1
  37. package/lib/types/index.js +362 -20
  38. package/lib/types/jobs.js +7 -3
  39. package/lib/types/journal.d.ts +1 -1
  40. package/lib/types/journal.js +18 -2
  41. package/lib/types/types.d.ts +77 -0
  42. package/package.json +10 -6
  43. package/lib/journal-B4FOGO6S.js +0 -482
  44. package/lib/journal-BWL3-i0s.js +0 -462
  45. package/lib/journal-Rid9AAnD.js +0 -485
@@ -22,6 +22,9 @@ import { MarkdownText, Tooltip, writeClipboard } from '@deepseek-ai/dsh-client-u
22
22
  import { IconCheckOutlineMedium, IconChevronDownOutlineMedium, IconCopyOutlineMedium, IconLinkOutlineMedium } from './icons.js';
23
23
  import { IconRequestDetailOutline16 } from './icons-local.js';
24
24
  import { memberColor } from './member-color.js';
25
+ import { pathMentions } from './path-mentions.js';
26
+ import { RoomQuestionView } from './RoomQuestionView.js';
27
+ import { askCardData } from './nodes.js';
25
28
  import { formatClock, formatDurationMs } from './format.js';
26
29
  import { formatTokens } from './RoomStatsLine.js';
27
30
  import css from './RoomSpeechView.module.css';
@@ -67,8 +70,36 @@ function markdownLabels(t) {
67
70
  footnotes: t('markdown.footnotes'),
68
71
  };
69
72
  }
70
- /** The member speech row. */
71
- export function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, t }) {
73
+ /**
74
+ * Split a reply around the questions its round asked, in the order it said
75
+ * and asked them (offsets recorded when the reply landed; -1 = the split
76
+ * point was not found, so the question opens the reply marked mid-round).
77
+ * A question the log does not know (an older cache) is left out.
78
+ * @param data - the speech node data.
79
+ * @param askLog - the room's question log.
80
+ * @returns the parts, text and questions interleaved.
81
+ */
82
+ export function replyParts(data, askLog) {
83
+ const logged = new Map((askLog ?? []).map(ask => [ask.id, ask]));
84
+ const asks = (data.asks ?? []).filter(entry => logged.has(entry.id)).sort((a, b) => a.at - b.at);
85
+ if (asks.length === 0)
86
+ return [{ kind: 'text', from: 0, text: data.text }];
87
+ const parts = [];
88
+ let cursor = 0;
89
+ for (const entry of asks) {
90
+ const at = Math.max(cursor, Math.min(entry.at, data.text.length));
91
+ const piece = data.text.slice(cursor, at);
92
+ if (piece.trim() !== '')
93
+ parts.push({ kind: 'text', from: cursor, text: piece });
94
+ parts.push({ kind: 'ask', ask: logged.get(entry.id), mid: entry.at < 0 });
95
+ cursor = at;
96
+ }
97
+ const rest = data.text.slice(cursor);
98
+ if (rest.trim() !== '')
99
+ parts.push({ kind: 'text', from: cursor, text: rest });
100
+ return parts;
101
+ }
102
+ export function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, openPath, answerQuestion, t }) {
72
103
  const data = node.data;
73
104
  const [copied, setCopied] = useState(false);
74
105
  // The human's own choice for this speech (remembered), else the default.
@@ -106,6 +137,9 @@ export function RoomSpeechView({ node, sessionId, roomStore, openSession, openDe
106
137
  foldChoices.write(sessionId, data.seq, next);
107
138
  };
108
139
  const labels = useMemo(() => markdownLabels(t), [t]);
140
+ // Paths the member wrote in inline code open in the sidebar's file viewer.
141
+ const memberCwd = member?.cwd;
142
+ const mentions = useMemo(() => openPath === undefined ? undefined : pathMentions((path) => { openPath(sessionId, path); }, memberCwd), [openPath, sessionId, memberCwd]);
109
143
  const copy = () => {
110
144
  void writeClipboard(data.text).then((ok) => {
111
145
  if (!ok)
@@ -115,7 +149,9 @@ export function RoomSpeechView({ node, sessionId, roomStore, openSession, openDe
115
149
  });
116
150
  };
117
151
  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
152
+ 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: [replyParts(data, state?.askLog).map(part => part.kind === 'text'
153
+ ? _jsx(MarkdownText, { text: part.text, labels: labels, fileMentions: mentions }, `t${part.from}`)
154
+ : _jsx(RoomQuestionView, { ...{ node: { data: askCardData(part.ask) }, answerQuestion, t }, inline: { mid: part.mid } }, part.ask.id)), 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
155
  ? _jsx(UsageChip, { tokens: tokens, execution: execution, provider: member?.kind === 'cli' ? member.provider : undefined, durationMs: data.durationMs, t: t })
120
156
  : data.durationMs !== undefined && _jsx("span", { className: css.meta, children: formatDurationMs(data.durationMs) }), _jsx("span", { className: css.meta, children: formatClock(data.time) })] })] })] }));
121
157
  }
@@ -76,7 +76,9 @@ export interface RoomStatsLineProps {
76
76
  readonly useProjection: UseProjection;
77
77
  /** The room's replayed state: the members chip reads its execution records. */
78
78
  readonly state?: RoomState | undefined;
79
+ /** Members chip only: an external coordinator's room, where the room session's own figures are not the coordinator's. */
80
+ readonly membersOnly?: boolean | undefined;
79
81
  readonly t: RoomComposerProps['t'];
80
82
  }
81
- export declare const RoomStatsLine: import("react").MemoExoticComponent<({ useProjection, state, t }: RoomStatsLineProps) => ReactNode>;
83
+ export declare const RoomStatsLine: import("react").MemoExoticComponent<({ useProjection, state, membersOnly, t }: RoomStatsLineProps) => ReactNode>;
82
84
  //# sourceMappingURL=RoomStatsLine.d.ts.map
@@ -103,7 +103,7 @@ export function memberUsage(state) {
103
103
  const order = state.members.map(member => member.name);
104
104
  return [...rows.values()].sort((a, b) => order.indexOf(a.member) - order.indexOf(b.member));
105
105
  }
106
- export const RoomStatsLine = memo(function RoomStatsLine({ useProjection, state, t }) {
106
+ export const RoomStatsLine = memo(function RoomStatsLine({ useProjection, state, membersOnly = false, t }) {
107
107
  const members = memberUsage(state);
108
108
  const [open, setOpen] = useState(false);
109
109
  const membersRef = useRef(null);
@@ -156,6 +156,8 @@ export const RoomStatsLine = memo(function RoomStatsLine({ useProjection, state,
156
156
  output: formatTokens(usage.outputTokens),
157
157
  }));
158
158
  }
159
+ if (membersOnly)
160
+ groups.length = 0;
159
161
  const line = groups.join(' | ');
160
162
  // The row elides with ellipsis when overlong; a delayed hover tooltip
161
163
  // carries the full line, enabled only while content is actually clipped
@@ -12,8 +12,13 @@ import type { RoomState } from '../types.ts';
12
12
  import type { RoomComposerProps } from './slots.ts';
13
13
  type Props = Pick<RoomComposerProps, 'openTasks' | 'reconcileDelivery' | 'openPlanSession' | 'invite' | 'listProviders' | 'roomCwd' | 'browseDirectory' | 'modelSurface' | 'renderHarnessModelPicker' | 't'> & {
14
14
  readonly state: RoomState;
15
+ /** A member waiting on a question inside its round: its capsule jumps back to the question. */
16
+ readonly waiting?: {
17
+ readonly member: string;
18
+ readonly jump: () => void;
19
+ } | undefined;
15
20
  };
16
21
  /** The capsule row. */
17
- export declare function RoomTaskCapsule({ state, openTasks, reconcileDelivery, openPlanSession, invite, listProviders, roomCwd, browseDirectory, modelSurface, renderHarnessModelPicker, t, }: Props): ReactNode;
22
+ export declare function RoomTaskCapsule({ state, openTasks, reconcileDelivery, openPlanSession, invite, listProviders, roomCwd, browseDirectory, modelSurface, renderHarnessModelPicker, waiting, t, }: Props): ReactNode;
18
23
  export {};
19
24
  //# sourceMappingURL=RoomTaskCapsule.d.ts.map
@@ -14,7 +14,7 @@ import { RoomRecoveryView } from './RoomRecoveryView.js';
14
14
  import { attentionCount, needsAttention } from './tasks.js';
15
15
  import css from './RoomTaskCapsule.module.css';
16
16
  /** The capsule row. */
17
- export function RoomTaskCapsule({ state, openTasks, reconcileDelivery, openPlanSession, invite, listProviders, roomCwd, browseDirectory, modelSurface, renderHarnessModelPicker, t, }) {
17
+ export function RoomTaskCapsule({ state, openTasks, reconcileDelivery, openPlanSession, invite, listProviders, roomCwd, browseDirectory, modelSurface, renderHarnessModelPicker, waiting, t, }) {
18
18
  const [inviteOpen, setInviteOpen] = useState(false);
19
19
  const [providers, setProviders] = useState(undefined);
20
20
  const jobs = state.jobs ?? [];
@@ -33,5 +33,5 @@ export function RoomTaskCapsule({ state, openTasks, reconcileDelivery, openPlanS
33
33
  const running = jobs.filter(job => job.status === 'running').length;
34
34
  const attention = attentionCount(state);
35
35
  const firstAttention = jobs.find(job => needsAttention(state, job));
36
- return _jsx("div", { className: css.root, children: _jsxs("div", { className: css.capsules, children: [_jsxs("button", { type: "button", className: css.capsule, "data-testid": "room-task-capsule", ...running > 0 ? { 'data-running': '' } : {}, onClick: () => { openTasks(); }, children: [running > 0 && _jsx("span", { className: css.dot, "aria-hidden": true }), running > 0 ? t('job.capsule.running', { count: running }) : t('job.capsule.idle', { count: jobs.length })] }), attention > 0 && _jsx("button", { type: "button", className: css.capsule, "data-attention": "", onClick: () => { openTasks(firstAttention?.id); }, children: t('job.capsule.attention', { count: attention }) })] }) });
36
+ return _jsx("div", { className: css.root, children: _jsxs("div", { className: css.capsules, children: [_jsxs("button", { type: "button", className: css.capsule, "data-testid": "room-task-capsule", ...running > 0 ? { 'data-running': '' } : {}, onClick: () => { openTasks(); }, children: [running > 0 && _jsx("span", { className: css.dot, "aria-hidden": true }), running > 0 ? t('job.capsule.running', { count: running }) : t('job.capsule.idle', { count: jobs.length })] }), attention > 0 && _jsx("button", { type: "button", className: css.capsule, "data-attention": "", onClick: () => { openTasks(firstAttention?.id); }, children: t('job.capsule.attention', { count: attention }) }), waiting !== undefined && _jsxs("button", { type: "button", className: css.capsule, "data-attention": "", "data-waiting": "", onClick: waiting.jump, children: [_jsx("span", { className: css.dot, "aria-hidden": true }), t('question.capsule', { member: waiting.member })] })] }) });
37
37
  }
@@ -126,7 +126,7 @@ function JobDetail({ state, job, now, actions, back, t }) {
126
126
  }
127
127
  };
128
128
  const stopping = [...new Set(open.map(row => memberOf(state, row.memberId)?.name ?? row.memberId))];
129
- return _jsxs("div", { className: css.detail, "data-task": job.id, children: [_jsxs("button", { type: "button", className: css.back, onClick: back, children: [_jsx(IconChevronLeftOutlineMedium, { size: 18 }), t('job.back')] }), _jsxs("div", { className: css.card, children: [_jsxs("strong", { className: css.cardTitle, children: [label, " \u00B7 ", job.title] }), _jsxs("div", { className: css.rowMeta, children: [_jsx("span", { className: css.tag, "data-status": status, children: t(`job.status.${status}`) }), _jsx("span", { className: css.muted, children: t('job.elapsed', { time: formatDurationMs(jobElapsed(job, now)) }) }), _jsx(Avatars, { state: state, ids: job.members })] }), _jsx("ul", { className: css.messages, children: job.messages.map(entry => _jsxs("li", { children: [_jsx("span", { className: css.muted, children: formatClock(entry.time) }), _jsxs("span", { className: css.messageText, children: [t('job.you'), entry.text] }), entry.by !== undefined && _jsx("span", { className: css.attr, children: entry.by === 'model'
129
+ return _jsxs("div", { className: css.detail, "data-task": job.id, children: [_jsxs("button", { type: "button", className: css.back, onClick: back, children: [_jsx(IconChevronLeftOutlineMedium, { size: 18 }), t('job.back')] }), _jsxs("div", { className: css.card, children: [_jsxs("strong", { className: css.cardTitle, children: [label, " \u00B7 ", job.title] }), _jsxs("div", { className: css.rowMeta, children: [_jsx("span", { className: css.tag, "data-status": status, children: t(`job.status.${status}`) }), _jsx("span", { className: css.muted, children: t('job.elapsed', { time: formatDurationMs(jobElapsed(job, now)) }) }), _jsx(Avatars, { state: state, ids: job.members })] }), _jsx("ul", { className: css.messages, children: job.messages.map(entry => _jsxs("li", { children: [_jsx("span", { className: css.muted, children: formatClock(entry.time) }), _jsxs("span", { className: css.messageText, children: [entry.from === undefined ? t('job.you') : t('job.from', { name: memberOf(state, entry.from)?.name ?? entry.from }), entry.text] }), entry.by !== undefined && _jsx("span", { className: css.attr, children: entry.by === 'model'
130
130
  ? t('job.by.model', { probability: (entry.probability ?? 0).toFixed(2) })
131
131
  : t(`job.by.${entry.by}`) })] }, entry.id)) }), _jsx(Bar, { rows: rows, from: job.startedAt, to: job.endedAt ?? now, now: now })] }), _jsxs("div", { className: css.actions, children: [open.length > 0 && actions.stopJob !== undefined && !confirming
132
132
  && _jsx("button", { type: "button", className: css.danger, onClick: () => { setConfirming(true); }, children: t('job.stop') }), actions.compose !== undefined && _jsx("button", { type: "button", className: css.primary, onClick: () => { actions.compose?.({ task: job.id, label }); }, children: t('job.followUp') }), actions.compose !== undefined && _jsx("span", { className: css.muted, children: t('job.followUpHint', { label }) })] }), confirming && _jsxs("div", { className: css.confirm, role: "alertdialog", "aria-label": t('job.stop'), children: [_jsx("p", { children: t('job.stopConfirm', { members: stopping.join('、') }) }), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.danger, disabled: busy, onClick: () => { void stop(); }, children: t('job.stopYes') }), _jsx("button", { type: "button", className: css.small, onClick: () => { setConfirming(false); }, children: t('goal.cancel') })] })] }), message !== null && _jsx("p", { role: "alert", className: css.error, children: message }), actions.reconcile !== undefined && _jsx(RoomRecoveryView, { deliveries: rows, members: state.members, reconcile: actions.reconcile, openSession: actions.openSession, t: t }), relays.map(relay => _jsxs("div", { className: css.need, children: [_jsx("p", { children: t('job.relay', { from: relay.from, to: relay.to }) }), _jsx("p", { className: css.messageText, children: relay.content }), _jsxs("div", { className: css.actions, children: [actions.confirmRelay !== undefined && _jsx("button", { type: "button", className: css.small, onClick: () => { void actions.confirmRelay?.(relay.id); }, children: t('job.relayConfirm') }), actions.dismissRelay !== undefined && _jsx("button", { type: "button", className: css.small, onClick: () => { void actions.dismissRelay?.(relay.id); }, children: t('job.relayDismiss') })] })] }, relay.id)), job.goalId !== undefined && job.goalId === state.plan?.id && actions.planCommand !== undefined && _jsxs("section", { className: css.section, children: [_jsx("h4", { children: t('job.goalTag', { objective: state.plan.objective }) }), _jsx(RoomPlanView, { plan: state.plan, members: state.members, command: actions.planCommand, openSession: actions.openSession, stopMember: actions.stopDelivery, t: t })] }), _jsxs("section", { className: css.section, children: [_jsx("h4", { children: t('job.subtasks') }), _jsx(SubtaskTree, { state: state, nodes: subtaskTree(state, job), now: now, actions: actions, t: t })] }), _jsxs("section", { className: css.section, children: [_jsx("h4", { children: t('job.timeline') }), _jsx(Timeline, { state: state, job: job, now: now, t: t })] })] });
@@ -22,7 +22,7 @@ function nodeElement(root, id) {
22
22
  const WIDE = 600;
23
23
  function StatusDot({ node, t }) {
24
24
  const label = node.status === 'running' ? t('members.status.running') : node.status === 'failed' ? t('members.status.failed') : t('members.status.idle');
25
- return _jsxs("span", { className: css.status, "data-status": node.status, children: [_jsx("i", { "aria-hidden": true }), label] });
25
+ return _jsxs("span", { className: css.status, "data-status": node.status, title: label, children: [_jsx("i", { "aria-hidden": true }), _jsx("span", { children: label })] });
26
26
  }
27
27
  function Avatar({ node }) {
28
28
  return _jsx("span", { className: css.avatar, style: { '--member-color': memberColor(node.member.name) }, "aria-hidden": true, children: node.member.name.slice(0, 1).toUpperCase() });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
- * (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
3
+ * (v0.2.1-rc.3, harness 1f81c047fc) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
4
  * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
5
  * no host runtime identity, identical on every line. Re-run the generator
6
6
  * after adding an icon import or bumping the harness checkout.
@@ -23,6 +23,7 @@ export declare const IconChevronRightOutlineMedium: ({ size, className }: IconPr
23
23
  export declare const IconChevronUpOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
24
24
  export declare const IconCopyOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
25
25
  export declare const IconLinkOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
26
+ export declare const IconPaperclipOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
26
27
  export declare const IconQueueOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
27
28
  export declare const IconStopFillMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
28
29
  //# sourceMappingURL=icons.d.ts.map
@@ -8,5 +8,6 @@ export const IconChevronRightOutlineMedium = ({ size = 14, className }) => (_jsx
8
8
  export const IconChevronUpOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12 10L8.70711 6.70711C8.31658 6.31658 7.68342 6.31658 7.29289 6.70711L4 10", stroke: "currentColor" }) }));
9
9
  export const IconCopyOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("rect", { x: "1.52075", y: "4.07373", width: "10.3932", height: "10.3932", rx: "2", stroke: "currentColor" }), _jsx("path", { d: "M11.9792 1.53296C13.36 1.53296 14.4792 2.65225 14.4792 4.03296V9.42847C14.4792 10.3756 13.9521 11.1987 13.1755 11.6228V10.3298C13.3652 10.0787 13.4792 9.7674 13.4792 9.42847V4.03296C13.4792 3.20453 12.8077 2.53296 11.9792 2.53296H6.58374C6.27966 2.53301 5.99684 2.6235 5.7605 2.77905H4.42358C4.85652 2.03463 5.66056 1.53304 6.58374 1.53296H11.9792Z", fill: "currentColor" })] }));
10
10
  export const IconLinkOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M6.59961 9.40051C6.82779 9.6334 7.10015 9.81842 7.40074 9.94472C7.70132 10.071 8.02409 10.1361 8.35013 10.1361C8.67618 10.1361 8.99894 10.071 9.29953 9.94472C9.60011 9.81842 9.87247 9.6334 10.1007 9.40051L12.9015 6.59967C13.3658 6.13541 13.6266 5.50572 13.6266 4.84915C13.6266 4.19258 13.3658 3.56289 12.9015 3.09863C12.4372 2.63436 11.8075 2.37354 11.151 2.37354C10.4944 2.37354 9.86472 2.63436 9.40045 3.09863L9.05034 3.44873", stroke: "currentColor" }), _jsx("path", { d: "M9.40051 6.59959C9.17233 6.3667 8.89997 6.18169 8.59939 6.05538C8.2988 5.92907 7.97603 5.86401 7.64999 5.86401C7.32395 5.86401 7.00118 5.92907 6.70059 6.05538C6.40001 6.18169 6.12765 6.3667 5.89946 6.59959L3.09863 9.40043C2.63436 9.8647 2.37354 10.4944 2.37354 11.151C2.37354 11.8075 2.63436 12.4372 3.09863 12.9015C3.56289 13.3657 4.19258 13.6266 4.84915 13.6266C5.50572 13.6266 6.13541 13.3657 6.59967 12.9015L6.94978 12.5514", stroke: "currentColor" })] }));
11
+ export const IconPaperclipOutlineMedium = ({ size = 16, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12.75 4.5V9.5C12.75 10.7598 12.2496 11.968 11.3588 12.8588C10.468 13.7496 9.25978 14.25 8 14.25C6.74022 14.25 5.53204 13.7496 4.64124 12.8588C3.75045 11.968 3.25 10.7598 3.25 9.5V5C3.25 4.13805 3.59241 3.3114 4.2019 2.7019C4.8114 2.09241 5.63805 1.75 6.5 1.75C7.36195 1.75 8.1886 2.09241 8.7981 2.7019C9.40759 3.3114 9.75 4.13805 9.75 5V9.5C9.75 9.96413 9.56563 10.4092 9.23744 10.7374C8.90925 11.0656 8.46413 11.25 8 11.25C7.53587 11.25 7.09075 11.0656 6.76256 10.7374C6.43437 10.4092 6.25 9.96413 6.25 9.5V5.5", stroke: "currentColor" }) }));
11
12
  export const IconQueueOutlineMedium = ({ size = 14, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M5 6.75H11", stroke: "currentColor" }), _jsx("path", { d: "M5 9H8", stroke: "currentColor" }), _jsx("path", { d: "M2.37067 11.2497C1.5872 9.89252 1.32042 8.29798 1.61945 6.7597C1.91847 5.22141 2.76317 3.84293 3.99801 2.87809C5.23285 1.91325 6.7747 1.427 8.33964 1.50888C9.90458 1.59076 11.3873 2.23526 12.5147 3.32369C13.6422 4.41232 14.3384 5.8717 14.4751 7.43304C14.6118 8.99438 14.1797 10.5525 13.2585 11.8205C12.3372 13.0885 10.9889 13.9809 9.4617 14.3334C8.18666 14.6277 6.8587 14.529 5.64964 14.0601C5.17095 13.8745 4.76937 13.4929 4.26509 13.3963C3.67389 13.2832 2.95232 13.5595 2.0377 14.3334", stroke: "currentColor" })] }));
12
13
  export const IconStopFillMedium = ({ size = 16, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12.5 2.5H3.5C2.94772 2.5 2.5 2.94772 2.5 3.5V12.5C2.5 13.0523 2.94772 13.5 3.5 13.5H12.5C13.0523 13.5 13.5 13.0523 13.5 12.5V3.5C13.5 2.94772 13.0523 2.5 12.5 2.5Z", fill: "currentColor" }) }));
@@ -1,10 +1,11 @@
1
1
  import roomRemote from '@khorsheed/dsh-room/remote';
2
2
  import { presetDisplayText } from '@deepseek-ai/dsh-agent-preset-registry/display';
3
3
  import { RoomRequestIds } from './request-ids.js';
4
+ import { fileAddressFor } from '@deepseek-ai/dsh-util-workspace-path';
4
5
  import { en, zh } from './locales.js';
5
6
  import { InviteAgentAction } from './InviteAgentAction.js';
6
7
  import { MembersView } from './MembersView.js';
7
- import { RoomComposer, selectRoomComposer } from './RoomComposer.js';
8
+ import { RoomComposer, roomInteractionKey, selectRoomComposer } from './RoomComposer.js';
8
9
  import { ROOM_TASKS_KIND, ROOM_TASKS_TAB_ID, RoomTasksTab } from './TasksTab.js';
9
10
  import { RoomTaskTags } from './task-tag.js';
10
11
  import { RoomSpeechView } from './RoomSpeechView.js';
@@ -68,13 +69,56 @@ export async function apply(ctx) {
68
69
  // RoomStore's cached verdict as the actual-room escape.
69
70
  const roomChrome = new RoomPresetVisibility(ctx, sessionId => roomStore.isRoomCached(sessionId) === true);
70
71
  const requestIds = new RoomRequestIds();
71
- const submit = async (sessionId, text, targets, task) => {
72
+ /** Answer a member's question card (the row of its own, or inline in its round). */
73
+ const answerFor = (sessionId) => async (questionId, answers) => {
74
+ if (remote === undefined)
75
+ return t('composer.error.generic');
76
+ const carried = await remote.answerQuestion({ sessionId, id: questionId, answers });
77
+ void roomStore.refresh(sessionId);
78
+ if (!carried.ok)
79
+ return t('composer.error.generic');
80
+ return carried.value.ok ? undefined : t('question.expired');
81
+ };
82
+ /**
83
+ * Open a path a member named in the sidebar's file viewer — the same file
84
+ * address the main conversation's file links open (a relative path resolves
85
+ * against the room session's workspace).
86
+ */
87
+ const openPath = ctx.get('sidebarRight') === undefined ? undefined : (sessionId, path) => {
88
+ const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd;
89
+ try {
90
+ ctx.get('sidebarRight').openResource(fileAddressFor(sessionId, cwd, path));
91
+ }
92
+ catch { /* no viewer claims file addresses: the link stays inert */ }
93
+ };
94
+ // The official conversation service's draft attachments (pick, upload,
95
+ // serialize): the room composer re-uses them so its attachments behave
96
+ // exactly like the official composer's. A host without them: no attachments.
97
+ // Read per inject, never once at apply: the conversation service may come up after this plugin.
98
+ const attachmentsFor = () => {
99
+ const conversation = ctx.get('conversation');
100
+ const attachmentsFace = conversation?.createDrafts === undefined || conversation.resolveDraftAttachments === undefined
101
+ || conversation.releaseDraftAttachment === undefined || conversation.retryFileUpload === undefined
102
+ || conversation.fileUploads === undefined || conversation.serializeDraftAttachments === undefined
103
+ ? undefined
104
+ : {
105
+ create: (sessionId, files) => conversation.createDrafts.call(conversation, sessionId, files),
106
+ resolve: ids => conversation.resolveDraftAttachments.call(conversation, ids),
107
+ release: (id) => { conversation.releaseDraftAttachment.call(conversation, id); },
108
+ retry: (sessionId, id) => { conversation.retryFileUpload.call(conversation, sessionId, id); },
109
+ uploads: conversation.fileUploads,
110
+ serialize: async (ids) => (await conversation.serializeDraftAttachments.call(conversation, ids)).attachments,
111
+ };
112
+ return attachmentsFace;
113
+ };
114
+ const submit = async (sessionId, text, targets, task, attachments) => {
72
115
  if (remote === undefined)
73
116
  return { ok: false, message: t('composer.error.generic') };
74
117
  const requestId = requestIds.forInput(sessionId, text, targets);
75
118
  const carried = await remote.postMessage({
76
119
  sessionId, text, requestId, ...targets === undefined || targets.length === 0 ? {} : { targets },
77
120
  ...task === undefined ? {} : { task },
121
+ ...attachments === undefined || attachments.length === 0 ? {} : { attachments },
78
122
  });
79
123
  if (!carried.ok)
80
124
  return { ok: false, message: t('composer.error.generic') };
@@ -82,7 +126,9 @@ export async function apply(ctx) {
82
126
  if (!result.ok) {
83
127
  return result.error.code === 'unknown-targets'
84
128
  ? { ok: false, message: t('composer.error.unknownTargets', { names: result.error.names.join(' ') }) }
85
- : { ok: false, message: t('composer.error.generic') };
129
+ : result.error.code === 'attachment-invalid'
130
+ ? { ok: false, message: t('composer.error.attachment', { reason: result.error.message }) }
131
+ : { ok: false, message: t('composer.error.generic') };
86
132
  }
87
133
  requestIds.complete(sessionId, requestId);
88
134
  void roomStore.refresh(sessionId);
@@ -371,6 +417,44 @@ export async function apply(ctx) {
371
417
  // 0.1.2 folded the event-Definition registry into `uiConversation.events`
372
418
  // (commit be531688f3); the deferred arm never fires in a composition
373
419
  // without the service, losing only the room chat rows, never the boot.
420
+ // The session list's 等你处理: a room's waiting question/approval cards are
421
+ // published as the host's pending interactions (lowest precedence, so an
422
+ // official approval in the same session still wins). The host tracks the
423
+ // cards of every live room; a light poll keeps the list current.
424
+ ctx.inject(['uiSession'], (sctx) => {
425
+ if (remote === undefined)
426
+ return;
427
+ const uiSession = sctx.uiSession;
428
+ if (uiSession.registerPendingInteraction === undefined)
429
+ return;
430
+ const publish = uiSession.registerPendingInteraction(() => -1);
431
+ const shown = new Map();
432
+ const sync = async () => {
433
+ const carried = await remote.pendingCards({}).catch(() => undefined);
434
+ if (carried === undefined || !carried.ok)
435
+ return;
436
+ const keys = new Set(carried.value.map(card => roomInteractionKey(card.kind, card.id)));
437
+ for (const [key, remove] of shown) {
438
+ if (keys.has(key))
439
+ continue;
440
+ remove();
441
+ shown.delete(key);
442
+ }
443
+ for (const card of carried.value) {
444
+ const key = roomInteractionKey(card.kind, card.id);
445
+ if (!shown.has(key))
446
+ shown.set(key, publish({ key, kind: card.kind, sessionId: card.sessionId }, async () => { }));
447
+ }
448
+ };
449
+ void sync();
450
+ const timer = setInterval(() => { void sync(); }, 4_000);
451
+ sctx.effect(() => () => {
452
+ clearInterval(timer);
453
+ for (const remove of shown.values())
454
+ remove();
455
+ shown.clear();
456
+ }, 'room: pending cards');
457
+ });
374
458
  ctx.inject(['uiConversation'], (lctx) => {
375
459
  lctx.uiConversation.events.register(roomSpeechDefinition);
376
460
  lctx.uiConversation.events.register(roomRunDefinition);
@@ -500,6 +584,7 @@ export async function apply(ctx) {
500
584
  },
501
585
  taskTags,
502
586
  submit,
587
+ ...(() => { const face = attachmentsFor(); return face === undefined ? {} : { attachments: face }; })(),
503
588
  renderMemberConfiguration: ctx.get('localAgentUi')?.renderMemberConfiguration,
504
589
  renderMemberInbox: ctx.get('localAgentUi')?.renderMemberInbox,
505
590
  stopMember: (name, expectedRunId) => { void remote?.cancel({ sessionId, name, ...expectedRunId === undefined ? {} : { expectedRunId } }).then(() => roomStore.refresh(sessionId)); },
@@ -601,9 +686,11 @@ export async function apply(ctx) {
601
686
  name: 'conversation.chat.node',
602
687
  key: 'room-speech',
603
688
  locale: NS,
604
- inject: () => ({
689
+ inject: (sessionId) => ({
605
690
  roomStore,
606
691
  openSession,
692
+ answerQuestion: answerFor(sessionId),
693
+ ...openPath === undefined ? {} : { openPath },
607
694
  // Context Lens is another plugin: reach it through the sidebar by its
608
695
  // tab kind, only when its namespace is mounted; a refused open is a no-op.
609
696
  ...ctx.get('remote.requestInspector') === undefined || ctx.get('sidebarRight') === undefined ? {} : {
@@ -625,6 +712,8 @@ export async function apply(ctx) {
625
712
  roomStore,
626
713
  openSession,
627
714
  cancelMember: member => cancelMember(sessionId, member),
715
+ answerQuestion: answerFor(sessionId),
716
+ ...openPath === undefined ? {} : { openPath },
628
717
  }),
629
718
  }, RoomRunView));
630
719
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'room-event', locale: NS }, RoomEventView));
@@ -668,17 +757,7 @@ export async function apply(ctx) {
668
757
  name: 'conversation.chat.node',
669
758
  key: 'room-question',
670
759
  locale: NS,
671
- inject: (sessionId) => ({
672
- answerQuestion: async (questionId, answers) => {
673
- if (remote === undefined)
674
- return t('composer.error.generic');
675
- const carried = await remote.answerQuestion({ sessionId, id: questionId, answers });
676
- void roomStore.refresh(sessionId);
677
- if (!carried.ok)
678
- return t('composer.error.generic');
679
- return carried.value.ok ? undefined : t('question.expired');
680
- },
681
- }),
760
+ inject: (sessionId) => ({ answerQuestion: answerFor(sessionId) }),
682
761
  }, RoomQuestionView));
683
762
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
684
763
  name: 'conversation.chat.node',
@@ -94,6 +94,20 @@ export declare const zh: {
94
94
  'composer.stop': string;
95
95
  'composer.error.unknownTargets': string;
96
96
  'composer.error.generic': string;
97
+ 'composer.interrupted': string;
98
+ 'composer.continueAll': string;
99
+ 'composer.continueText': string;
100
+ 'composer.error.attachment': string;
101
+ 'attach.add': string;
102
+ 'attach.remove': string;
103
+ 'attach.uploading': string;
104
+ 'attach.failed': string;
105
+ 'attach.retry': string;
106
+ 'attach.open': string;
107
+ 'attach.preview': string;
108
+ 'attach.closePreview': string;
109
+ 'attach.drop': string;
110
+ 'attach.unsupported': string;
97
111
  'composer.model.picker': string;
98
112
  'composer.model.select': string;
99
113
  'composer.model.loading': string;
@@ -132,6 +146,7 @@ export declare const zh: {
132
146
  'usage.output': string;
133
147
  'usage.duration': string;
134
148
  'run.working': string;
149
+ 'run.waiting': string;
135
150
  'run.failed': string;
136
151
  'run.stop': string;
137
152
  'run.durationUnknown': string;
@@ -349,6 +364,7 @@ export declare const zh: {
349
364
  'job.status.stopped': string;
350
365
  'job.elapsed': string;
351
366
  'job.you': string;
367
+ 'job.from': string;
352
368
  'job.by.human': string;
353
369
  'job.by.coordinator': string;
354
370
  'job.by.model': string;
@@ -434,12 +450,18 @@ export declare const zh: {
434
450
  'decision.rule.failure': string;
435
451
  'decision.rule.last': string;
436
452
  'question.title': string;
453
+ 'question.waiting': string;
454
+ 'question.asked': string;
455
+ 'question.midRound': string;
456
+ 'question.capsule': string;
437
457
  'question.other': string;
438
458
  'question.otherPlaceholder': string;
439
459
  'question.submit': string;
440
460
  'question.multi': string;
441
461
  'question.answered': string;
442
462
  'question.cancelled': string;
463
+ 'question.lateOpen': string;
464
+ 'question.answeredLate': string;
443
465
  'question.answer': string;
444
466
  'question.expired': string;
445
467
  'invite.modelLabel': string;
@@ -548,6 +570,20 @@ export declare const en: {
548
570
  'composer.stop': string;
549
571
  'composer.error.unknownTargets': string;
550
572
  'composer.error.generic': string;
573
+ 'composer.interrupted': string;
574
+ 'composer.continueAll': string;
575
+ 'composer.continueText': string;
576
+ 'composer.error.attachment': string;
577
+ 'attach.add': string;
578
+ 'attach.remove': string;
579
+ 'attach.uploading': string;
580
+ 'attach.failed': string;
581
+ 'attach.retry': string;
582
+ 'attach.open': string;
583
+ 'attach.preview': string;
584
+ 'attach.closePreview': string;
585
+ 'attach.drop': string;
586
+ 'attach.unsupported': string;
551
587
  'composer.model.picker': string;
552
588
  'composer.model.select': string;
553
589
  'composer.model.loading': string;
@@ -586,6 +622,7 @@ export declare const en: {
586
622
  'usage.output': string;
587
623
  'usage.duration': string;
588
624
  'run.working': string;
625
+ 'run.waiting': string;
589
626
  'run.failed': string;
590
627
  'run.stop': string;
591
628
  'run.durationUnknown': string;
@@ -803,6 +840,7 @@ export declare const en: {
803
840
  'job.status.stopped': string;
804
841
  'job.elapsed': string;
805
842
  'job.you': string;
843
+ 'job.from': string;
806
844
  'job.by.human': string;
807
845
  'job.by.coordinator': string;
808
846
  'job.by.model': string;
@@ -888,12 +926,18 @@ export declare const en: {
888
926
  'decision.rule.failure': string;
889
927
  'decision.rule.last': string;
890
928
  'question.title': string;
929
+ 'question.waiting': string;
930
+ 'question.asked': string;
931
+ 'question.midRound': string;
932
+ 'question.capsule': string;
891
933
  'question.other': string;
892
934
  'question.otherPlaceholder': string;
893
935
  'question.submit': string;
894
936
  'question.multi': string;
895
937
  'question.answered': string;
896
938
  'question.cancelled': string;
939
+ 'question.lateOpen': string;
940
+ 'question.answeredLate': string;
897
941
  'question.answer': string;
898
942
  'question.expired': string;
899
943
  'invite.modelLabel': string;
@@ -94,6 +94,20 @@ export const zh = {
94
94
  'composer.stop': '停止生成',
95
95
  'composer.error.unknownTargets': '未知成员:{names}',
96
96
  'composer.error.generic': '发送失败,请重试',
97
+ 'composer.interrupted': '{count} 位成员被重启打断:{names}',
98
+ 'composer.continueAll': '全部继续',
99
+ 'composer.continueText': '继续',
100
+ 'composer.error.attachment': '附件没能发送:{reason}',
101
+ 'attach.add': '添加图片或文件',
102
+ 'attach.remove': '移除 {name}',
103
+ 'attach.uploading': '上传中…',
104
+ 'attach.failed': '上传失败,点击重试',
105
+ 'attach.retry': '重新上传 {name}',
106
+ 'attach.open': '查看原图 {name}',
107
+ 'attach.preview': '原图预览',
108
+ 'attach.closePreview': '关闭原图预览',
109
+ 'attach.drop': '松开即可添加到消息',
110
+ 'attach.unsupported': '仅支持 PNG、JPG、WebP、GIF 格式的图片',
97
111
  'composer.model.picker': 'DSH 模型',
98
112
  'composer.model.select': '选择模型',
99
113
  'composer.model.loading': '正在加载模型…',
@@ -132,6 +146,7 @@ export const zh = {
132
146
  'usage.output': '输出',
133
147
  'usage.duration': '耗时',
134
148
  'run.working': '{member} 正在工作…',
149
+ 'run.waiting': '{member} 在等你回答',
135
150
  'run.failed': '{member} 运行失败',
136
151
  'run.stop': '停止',
137
152
  'run.durationUnknown': '耗时未知',
@@ -349,6 +364,7 @@ export const zh = {
349
364
  'job.status.stopped': '已停止',
350
365
  'job.elapsed': '用时 {time}',
351
366
  'job.you': '你:',
367
+ 'job.from': '{name}:',
352
368
  'job.by.human': '你指定的',
353
369
  'job.by.coordinator': '协调者归入',
354
370
  'job.by.model': '决策模型归入 · {probability}',
@@ -434,12 +450,18 @@ export const zh = {
434
450
  'decision.rule.failure': '成员没有正常完成',
435
451
  'decision.rule.last': '最后一个结果',
436
452
  'question.title': '{member} 问你',
453
+ 'question.waiting': '{member} 在等你回答',
454
+ 'question.asked': '问你',
455
+ 'question.midRound': '提问于这一轮中途',
456
+ 'question.capsule': '{member} 等你回答 ↑',
437
457
  'question.other': '其他',
438
458
  'question.otherPlaceholder': '写下你的回答…',
439
459
  'question.submit': '提交回答',
440
460
  'question.multi': '可多选',
441
461
  'question.answered': '已回答',
442
462
  'question.cancelled': '已过期,未作答',
463
+ 'question.lateOpen': '已过期,仍可回答',
464
+ 'question.answeredLate': '已补答',
443
465
  'question.answer': '你的回答:{answer}',
444
466
  'question.expired': '这组问题已经结束了',
445
467
  'invite.modelLabel': '模型',
@@ -540,6 +562,20 @@ export const en = {
540
562
  'composer.stop': 'Stop generating',
541
563
  'composer.error.unknownTargets': 'Unknown members: {names}',
542
564
  'composer.error.generic': 'Could not send; please retry',
565
+ 'composer.interrupted': '{count} member(s) interrupted by a restart: {names}',
566
+ 'composer.continueAll': 'Continue all',
567
+ 'composer.continueText': 'Continue',
568
+ 'composer.error.attachment': 'An attachment could not be sent: {reason}',
569
+ 'attach.add': 'Add images or files',
570
+ 'attach.remove': 'Remove {name}',
571
+ 'attach.uploading': 'Uploading…',
572
+ 'attach.failed': 'Upload failed — click to retry',
573
+ 'attach.retry': 'Upload {name} again',
574
+ 'attach.open': 'View the original of {name}',
575
+ 'attach.preview': 'Original image',
576
+ 'attach.closePreview': 'Close the original image',
577
+ 'attach.drop': 'Drop to add to the message',
578
+ 'attach.unsupported': 'Only PNG, JPG, WebP and GIF images are supported',
543
579
  'composer.model.picker': 'Main agent model',
544
580
  'composer.model.select': 'Select model',
545
581
  'composer.model.loading': 'Loading models…',
@@ -578,6 +614,7 @@ export const en = {
578
614
  'usage.output': 'Output',
579
615
  'usage.duration': 'Run time',
580
616
  'run.working': '{member} is working…',
617
+ 'run.waiting': '{member} is waiting for your answer',
581
618
  'run.failed': "{member}'s run failed",
582
619
  'run.stop': 'Stop',
583
620
  'run.durationUnknown': 'Duration unknown',
@@ -795,6 +832,7 @@ export const en = {
795
832
  'job.status.stopped': 'stopped',
796
833
  'job.elapsed': 'took {time}',
797
834
  'job.you': 'You: ',
835
+ 'job.from': '{name}: ',
798
836
  'job.by.human': 'filed by you',
799
837
  'job.by.coordinator': 'filed by the coordinator',
800
838
  'job.by.model': 'filed by the decision model · {probability}',
@@ -880,12 +918,18 @@ export const en = {
880
918
  'decision.rule.failure': 'the member did not finish normally',
881
919
  'decision.rule.last': 'the last result',
882
920
  'question.title': '{member} asks you',
921
+ 'question.waiting': '{member} is waiting for your answer',
922
+ 'question.asked': 'Asked you',
923
+ 'question.midRound': 'asked mid-round',
924
+ 'question.capsule': '{member} waits for your answer ↑',
883
925
  'question.other': 'Other',
884
926
  'question.otherPlaceholder': 'Type your answer…',
885
927
  'question.submit': 'Submit answers',
886
928
  'question.multi': 'Pick any',
887
929
  'question.answered': 'Answered',
888
930
  'question.cancelled': 'Expired without an answer',
931
+ 'question.lateOpen': 'Expired — you can still answer',
932
+ 'question.answeredLate': 'Answered late',
889
933
  'question.answer': 'Your answer: {answer}',
890
934
  'question.expired': 'These questions already ended',
891
935
  'invite.modelLabel': 'Model',