@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
@@ -172,8 +172,8 @@ export function InviteDialog({ mode, member, providers, presets, sessionPreset,
172
172
  // Edit mode: instructions may clear (empty = no preset injected); the name
173
173
  // is the only hard requirement.
174
174
  const canSubmit = mode === 'edit'
175
- ? validName(name)
176
- : (native || localAgentAvailable) && chosen !== '' && validName(name);
175
+ ? validName(name) && !(name !== member?.name && existingNames.includes(name))
176
+ : (native || localAgentAvailable) && chosen !== '' && validName(name) && !existingNames.includes(name);
177
177
  // The model row: the same control the provider settings cards use. An unset
178
178
  // field DISPLAYS the provider's current default dimmed (inherited, never
179
179
  // pinned); the chevron menu leads with the 默认 item (picking it clears the
@@ -211,6 +211,11 @@ export function InviteDialog({ mode, member, providers, presets, sessionPreset,
211
211
  ...instructions.trim() === '' ? {} : { instructions: instructions.trim() },
212
212
  ...role.trim() === '' ? {} : { role: role.trim() },
213
213
  };
214
+ // A typed name that cannot be saved: invalid (whitespace, @) or another member's.
215
+ const nameProblem = name === '' ? undefined
216
+ : !validName(name) ? 'invite.error.invalid'
217
+ : name !== member?.name && existingNames.includes(name) ? 'invite.error.duplicate'
218
+ : undefined;
214
219
  const rollDice = () => {
215
220
  const rolled = rollName(existingNames, name);
216
221
  if (rolled !== undefined)
@@ -275,6 +280,8 @@ export function InviteDialog({ mode, member, providers, presets, sessionPreset,
275
280
  })() })] })), mode === 'edit' && _jsxs("div", { className: css.editHead, children: [_jsx(MemberCard, { preview: true, member: previewMember, run: undefined, elapsedMs: undefined, openSession: () => { }, onEdit: () => { }, onRemove: () => { }, t: t }), _jsxs("dl", { className: css.locked, children: [_jsxs("div", { children: [_jsx("dt", { children: t('invite.provider') }), _jsx("dd", { children: member?.kind === 'main-agent' || member?.kind === 'native' ? 'DSH' : member?.provider ?? '—' })] }), _jsxs("div", { children: [_jsx("dt", { children: t('invite.cliPreset') }), _jsx("dd", { children: member?.preset === undefined
276
281
  ? t('invite.cliPresetNone')
277
282
  : presets?.find(entry => entry.id === member.preset)?.name ?? member.preset })] }), _jsx("span", { className: css.lockedNote, children: t('invite.lockedNote') })] })] }), native ? null : mode === 'edit' && memberConfiguration !== undefined
278
- ? _jsxs("div", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.modelLabel') }), memberConfiguration] })
279
- : memberConfiguration ?? (mode === 'invite' && chosenHarness !== undefined && (modelSurface === undefined || surface !== undefined) ? renderHarnessModelPicker?.(chosenHarness, { value: model, onChange: setModel, effort: { value: effort, onChange: setEffort }, disabled: busy, fields: { row: css.fieldPair, field: css.field, label: css.label, select: css.select } }) : undefined) ?? modelField, _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.name') }), _jsxs("span", { className: css.nameRow, children: [_jsx("input", { className: css.input, value: name, placeholder: "ada", onChange: event => { setName(event.target.value); } }), _jsx("button", { type: "button", className: css.dice, title: t('invite.randomName'), "aria-label": t('invite.randomName'), onClick: rollDice, children: "\uD83C\uDFB2" })] }), _jsx("span", { className: css.hint, children: t('invite.nameHint') })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.role') }), _jsx("input", { className: css.input, value: role, maxLength: ROOM_ROLE_MAX, placeholder: t('invite.rolePlaceholder'), onChange: event => { setRole(event.target.value); } }), _jsx("span", { className: css.hint, children: t('invite.roleHint') })] }), mode === 'invite' && (_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.firstTask') }), _jsx("textarea", { className: css.textarea, rows: 2, value: firstTask, onChange: event => { setFirstTask(event.target.value); } }), _jsx("span", { className: css.hint, children: t('invite.firstTaskHint') })] })), _jsxs("details", { className: css.advanced, open: advancedOpen, onToggle: (event) => { setAdvancedOpen(event.currentTarget.open); }, children: [_jsx("summary", { className: css.advancedSummary, children: t('invite.advanced') }), _jsxs("div", { className: css.advancedBody, children: [_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.instructions') }), _jsx("textarea", { className: css.textarea, rows: 3, value: instructions, placeholder: t('invite.instructionsPlaceholder'), onChange: event => { setInstructions(event.target.value); } }), _jsx("span", { className: css.hint, children: mode === 'edit' ? t('invite.instructionsHintEdit') : t('invite.instructionsHint') })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.cwd') }), _jsxs("span", { className: css.cwdRow, children: [_jsx("input", { className: css.input, value: cwd, placeholder: inheritedCwd ?? '', readOnly: true, "aria-label": t('invite.cwd') }), _jsx("button", { type: "button", className: css.browse, disabled: browsing, onClick: () => { void browse(); }, children: t('invite.browse') }), cwd !== '' && (_jsx("button", { type: "button", className: css.browse, "aria-label": t('invite.cwdReset'), onClick: () => { setCwd(''); }, children: "\u2715" }))] }), _jsx("span", { className: css.hint, children: t('invite.cwdHint') })] })] })] })] }), mode === 'invite' && _jsxs("div", { className: css.previewCol, children: [_jsx("span", { className: css.previewLabel, children: t('invite.preview') }), _jsx(MemberCard, { preview: true, member: previewMember, run: undefined, elapsedMs: undefined, openSession: () => { }, onEdit: () => { }, onRemove: () => { }, t: t })] })] }), error !== null && _jsx("div", { className: css.error, role: "alert", children: error }), _jsxs("div", { className: css.buttons, children: [_jsx("button", { type: "button", className: css.button, onClick: onClose, children: t('invite.cancel') }), _jsx("button", { type: "button", className: css.primary, disabled: !canSubmit || busy, onClick: () => { void submit(); }, children: mode === 'invite' ? t('invite.submit') : t('invite.save') })] })] }) }));
283
+ ? _jsxs("div", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.modelLabel') }), _jsx("div", { className: css.memberConfiguration, children: memberConfiguration })] })
284
+ : memberConfiguration ?? (mode === 'invite' && chosenHarness !== undefined && (modelSurface === undefined || surface !== undefined) ? renderHarnessModelPicker?.(chosenHarness, { value: model, onChange: setModel, effort: { value: effort, onChange: setEffort }, disabled: busy, fields: { row: css.fieldPair, field: css.field, label: css.label, select: css.select } }) : undefined) ?? modelField, _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.name') }), _jsxs("span", { className: css.nameRow, children: [_jsx("input", { className: css.input, value: name, placeholder: "ada", onChange: event => { setName(event.target.value); } }), _jsx("button", { type: "button", className: css.dice, title: t('invite.randomName'), "aria-label": t('invite.randomName'), onClick: rollDice, children: "\uD83C\uDFB2" })] }), nameProblem === undefined
285
+ ? _jsx("span", { className: css.hint, children: t('invite.nameHint') })
286
+ : _jsx("span", { className: `${css.hint} ${css.hintError}`, role: "alert", children: t(nameProblem) })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.role') }), _jsx("input", { className: css.input, value: role, maxLength: ROOM_ROLE_MAX, placeholder: t('invite.rolePlaceholder'), onChange: event => { setRole(event.target.value); } }), _jsx("span", { className: css.hint, children: t('invite.roleHint') })] }), mode === 'invite' && (_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.firstTask') }), _jsx("textarea", { className: css.textarea, rows: 2, value: firstTask, onChange: event => { setFirstTask(event.target.value); } }), _jsx("span", { className: css.hint, children: t('invite.firstTaskHint') })] })), _jsxs("details", { className: css.advanced, open: advancedOpen, onToggle: (event) => { setAdvancedOpen(event.currentTarget.open); }, children: [_jsx("summary", { className: css.advancedSummary, children: t('invite.advanced') }), _jsxs("div", { className: css.advancedBody, children: [_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.instructions') }), _jsx("textarea", { className: css.textarea, rows: 3, value: instructions, placeholder: t('invite.instructionsPlaceholder'), onChange: event => { setInstructions(event.target.value); } }), _jsx("span", { className: css.hint, children: mode === 'edit' ? t('invite.instructionsHintEdit') : t('invite.instructionsHint') })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.label, children: t('invite.cwd') }), _jsxs("span", { className: css.cwdRow, children: [_jsx("input", { className: css.input, value: cwd, placeholder: inheritedCwd ?? '', readOnly: true, "aria-label": t('invite.cwd') }), _jsx("button", { type: "button", className: css.browse, disabled: browsing, onClick: () => { void browse(); }, children: t('invite.browse') }), cwd !== '' && (_jsx("button", { type: "button", className: css.browse, "aria-label": t('invite.cwdReset'), onClick: () => { setCwd(''); }, children: "\u2715" }))] }), _jsx("span", { className: css.hint, children: t('invite.cwdHint') })] })] })] })] }), mode === 'invite' && _jsxs("div", { className: css.previewCol, children: [_jsx("span", { className: css.previewLabel, children: t('invite.preview') }), _jsx(MemberCard, { preview: true, member: previewMember, run: undefined, elapsedMs: undefined, openSession: () => { }, onEdit: () => { }, onRemove: () => { }, t: t })] })] }), error !== null && _jsx("div", { className: css.error, role: "alert", children: error }), _jsxs("div", { className: css.buttons, children: [_jsx("button", { type: "button", className: css.button, onClick: onClose, children: t('invite.cancel') }), _jsx("button", { type: "button", className: css.primary, disabled: !canSubmit || busy, onClick: () => { void submit(); }, children: mode === 'invite' ? t('invite.submit') : t('invite.save') })] })] }) }));
280
287
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The room composer's draft-attachment rail: image thumbnails (click opens
3
+ * the original in the primitives' lightbox) and file cards with their upload
4
+ * state (uploading / failed → click to retry), each with a hover ✕ — the
5
+ * official composer's rail (ui-attachment's ComposerAttachments + FileCard)
6
+ * redrawn on the same tokens, because that package exposes no client
7
+ * component a plugin bundle can import. The drafts themselves are the
8
+ * official conversation service's (RoomAttachmentsFace).
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { RoomAttachmentsFace, RoomComposerProps, RoomDraftAttachment } from './slots.ts';
12
+ /** The rail over the textarea; renders nothing without drafts. */
13
+ export declare function RoomAttachments({ attachments, face, onRemove, onRetry, t }: {
14
+ readonly attachments: readonly RoomDraftAttachment[];
15
+ readonly face: RoomAttachmentsFace;
16
+ readonly onRemove: (id: string) => void;
17
+ readonly onRetry: (id: string) => void;
18
+ readonly t: RoomComposerProps['t'];
19
+ }): ReactNode;
20
+ //# sourceMappingURL=RoomAttachments.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The room composer's draft-attachment rail: image thumbnails (click opens
4
+ * the original in the primitives' lightbox) and file cards with their upload
5
+ * state (uploading / failed → click to retry), each with a hover ✕ — the
6
+ * official composer's rail (ui-attachment's ComposerAttachments + FileCard)
7
+ * redrawn on the same tokens, because that package exposes no client
8
+ * component a plugin bundle can import. The drafts themselves are the
9
+ * official conversation service's (RoomAttachmentsFace).
10
+ */
11
+ import { useState, useSyncExternalStore } from 'react';
12
+ import { FileTypeIcon, ImageLightbox, fileSizeText } from '@deepseek-ai/dsh-client-ui-primitives';
13
+ import css from './RoomAttachments.module.css';
14
+ /** The rail over the textarea; renders nothing without drafts. */
15
+ export function RoomAttachments({ attachments, face, onRemove, onRetry, t }) {
16
+ const uploads = useSyncExternalStore(face.uploads.subscribe, face.uploads.getSnapshot);
17
+ const [preview, setPreview] = useState(undefined);
18
+ if (attachments.length === 0)
19
+ return null;
20
+ return (_jsxs("div", { className: css.rail, children: [attachments.map((attachment) => {
21
+ const name = attachment.file.name;
22
+ const remove = (_jsx("button", { type: "button", className: css.remove, "aria-label": t('attach.remove', { name }), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { onRemove(attachment.id); }, children: _jsx("svg", { viewBox: "0 0 12 12", width: "8", height: "8", "aria-hidden": true, children: _jsx("path", { d: "M2 2l8 8M10 2l-8 8", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) }));
23
+ if (attachment.kind === 'image') {
24
+ return (_jsxs("div", { className: css.imageItem, children: [_jsx("button", { type: "button", className: css.thumbnail, "aria-label": t('attach.open', { name }), onClick: () => { setPreview(attachment); }, children: _jsx("img", { src: attachment.previewUrl, alt: name }) }), remove] }, attachment.id));
25
+ }
26
+ const upload = uploads[attachment.id];
27
+ const state = upload?.status ?? 'uploading';
28
+ const meta = state === 'uploading' ? t('attach.uploading')
29
+ : state === 'error' ? t('attach.failed')
30
+ : typeof fileSizeText === 'function' ? fileSizeText(attachment.file.size) : '';
31
+ const body = (_jsxs(_Fragment, { children: [_jsx("span", { className: css.name, children: name }), _jsx("span", { className: state === 'error' ? `${css.meta} ${css.metaFailed}` : css.meta, children: meta })] }));
32
+ return (_jsxs("div", { className: state === 'error' ? `${css.card} ${css.failed}` : css.card, title: name, children: [_jsx("span", { className: css.icon, "aria-hidden": true, children: state === 'uploading' || typeof FileTypeIcon !== 'function' ? _jsx("span", { className: css.spinner, "data-idle": state === 'uploading' ? undefined : '' }) : _jsx(FileTypeIcon, { path: name }) }), state === 'error'
33
+ ? _jsx("button", { type: "button", className: `${css.body} ${css.retry}`, "aria-label": t('attach.retry', { name }), onClick: () => { onRetry(attachment.id); }, children: body })
34
+ : _jsx("span", { className: css.body, children: body }), remove] }, attachment.id));
35
+ }), preview !== undefined && typeof ImageLightbox === 'function' && (_jsx(ImageLightbox, { src: preview.previewUrl, alt: preview.file.name, labels: { dialog: t('attach.preview'), close: t('attach.closePreview') }, onClose: () => { setPreview(undefined); } }))] }));
36
+ }
@@ -3,19 +3,13 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
3
  import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client';
4
4
  import type { RoomComposerMatch, RoomComposerProps } from './slots.ts';
5
5
  /**
6
- * Pure composer-chain selector: elect exactly the cached-room sessions.
7
- * Pending interactions (questions/approvals) belong to the official
8
- * ApprovalPanel — a chain entry at priority 1. Election runs ascending, so
9
- * this -10 entry would shadow it: decline and let the interaction render
10
- * (the member-channel rule, local-agent 84a2ed0). A cache miss declines too —
11
- * the freshly opened room shows the official bar for the first pull's
12
- * duration (accepted, see room-store.ts).
13
- * @param owner - the composer chain currency dispatched by ConversationRoot.
14
- * @param isRoomCached - the room store's cache probe (injected: the selector
15
- * must stay a pure function of the owner props plus stable state reads).
16
- * @returns the room match, or null to pass the election down the chain.
6
+ * The pending-interaction key a room card publishes under.
7
+ * @param kind - question or approval.
8
+ * @param id - the card id.
9
+ * @returns the key (prefixed, so the composer selector can tell it apart).
17
10
  */
11
+ export declare function roomInteractionKey(kind: string, id: string): string;
18
12
  export declare function selectRoomComposer(owner: ComposerChainProps, isRoomCached: (sessionId: SessionId) => boolean): RoomComposerMatch | null;
19
13
  /** The room composer takeover component. */
20
- export declare function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, taskTags, openPlanSession, reconcileDelivery, roomCwd, invite, listProviders, browseDirectory, modelSurface, renderHarnessModelPicker, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, openReference, listSessions, insertSessionReference, useSession, useProjection, useInput, inputActions, t, }: RoomComposerProps): ReactNode;
14
+ export declare function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, taskTags, openPlanSession, reconcileDelivery, roomCwd, invite, listProviders, browseDirectory, modelSurface, renderHarnessModelPicker, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, openReference, listSessions, insertSessionReference, useSession, useProjection, useInput, inputActions, t, attachments: attachmentFace, }: RoomComposerProps): ReactNode;
21
15
  //# sourceMappingURL=RoomComposer.d.ts.map
@@ -64,7 +64,8 @@ import { RoomTaskCapsule } from './RoomTaskCapsule.js';
64
64
  import { RoomTodoStrip } from './RoomTodoStrip.js';
65
65
  import { RoomQueueStrip } from './RoomQueueStrip.js';
66
66
  import { RoomModelPicker } from './RoomModelPicker.js';
67
- import { IconChevronRightOutlineMedium } from './icons.js';
67
+ import { IconChevronRightOutlineMedium, IconPaperclipOutlineMedium } from './icons.js';
68
+ import { RoomAttachments } from './RoomAttachments.js';
68
69
  import css from './RoomComposer.module.css';
69
70
  const NO_TAGS = () => () => { };
70
71
  const EMPTY_LEGACY_QUEUE = [];
@@ -105,15 +106,33 @@ function detectMention(draft, caret) {
105
106
  * must stay a pure function of the owner props plus stable state reads).
106
107
  * @returns the room match, or null to pass the election down the chain.
107
108
  */
109
+ /** Key prefix of the room's own pending interactions (its question/approval cards). */
110
+ const ROOM_INTERACTION_PREFIX = 'room:';
111
+ /**
112
+ * The pending-interaction key a room card publishes under.
113
+ * @param kind - question or approval.
114
+ * @param id - the card id.
115
+ * @returns the key (prefixed, so the composer selector can tell it apart).
116
+ */
117
+ export function roomInteractionKey(kind, id) {
118
+ return `${ROOM_INTERACTION_PREFIX}${kind}:${id}`;
119
+ }
108
120
  export function selectRoomComposer(owner, isRoomCached) {
109
- if (owner.pendingInteraction !== undefined)
121
+ // The room's own waiting cards ride the pending channel only for the
122
+ // session list (roomInteractionKey): they never hand the composer away.
123
+ if (owner.pendingInteraction !== undefined && owner.pendingInteraction.key?.startsWith(ROOM_INTERACTION_PREFIX) !== true)
110
124
  return null;
111
125
  const sessionId = owner.sessionId ?? owner.session?.sessionId;
112
126
  return sessionId !== undefined && isRoomCached(sessionId) ? { room: true } : null;
113
127
  }
114
128
  const NO_CHIPS = [];
129
+ const NO_IDS = [];
130
+ /** The failed-run reason recovery journals for a round a host restart cut off (dispatch.ts). */
131
+ const RESTART_INTERRUPTED = 'Execution outcome is unknown after restart';
132
+ const NO_UPLOADS = {};
133
+ const NO_UPLOAD_STORE = { subscribe: () => () => { }, getSnapshot: () => NO_UPLOADS };
115
134
  /** The room composer takeover component. */
116
- export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, taskTags, openPlanSession, reconcileDelivery, roomCwd, invite, listProviders, browseDirectory, modelSurface, renderHarnessModelPicker, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, openReference, listSessions, insertSessionReference, useSession, useProjection, useInput, inputActions, t, }) {
135
+ export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, taskTags, openPlanSession, reconcileDelivery, roomCwd, invite, listProviders, browseDirectory, modelSurface, renderHarnessModelPicker, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, openReference, listSessions, insertSessionReference, useSession, useProjection, useInput, inputActions, t, attachments: attachmentFace, }) {
117
136
  const state = useSyncExternalStore(roomStore.subscribe, () => roomStore.getCached(sessionId));
118
137
  const tag = useSyncExternalStore(taskTags?.subscribe ?? NO_TAGS, () => taskTags?.get(sessionId));
119
138
  // The draft is the session's official input machine (the module header):
@@ -130,6 +149,16 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, ta
130
149
  // What the textarea shows: the typed text around the chips.
131
150
  const value = chipMode ? freeText(draft, chips) : draft;
132
151
  const [busy, setBusy] = useState(false);
152
+ // Draft attachments: ids live on the session's input machine (beside the
153
+ // draft text, surviving navigation like it); the drafts and their uploads
154
+ // are the official conversation service's (RoomAttachmentsFace).
155
+ const attachmentActions = inputActions;
156
+ const attachable = attachmentFace !== undefined && attachmentActions.addAttachments !== undefined;
157
+ const attachmentIds = useInput(input => input.attachmentIds) ?? NO_IDS;
158
+ const drafts = attachable && attachmentIds.length > 0 ? attachmentFace.resolve(attachmentIds) : [];
159
+ const uploads = useSyncExternalStore(attachmentFace?.uploads.subscribe ?? NO_UPLOAD_STORE.subscribe, attachmentFace?.uploads.getSnapshot ?? NO_UPLOAD_STORE.getSnapshot);
160
+ const uploadsPending = drafts.some(draft => draft.kind === 'file' && uploads[draft.id]?.status !== 'ready');
161
+ const fileInputRef = useRef(null);
133
162
  const [error, setError] = useState(null);
134
163
  const [mention, setMention] = useState(null);
135
164
  /** Menu-picked addressees: a pick is explicit addressing wherever the
@@ -302,24 +331,89 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, ta
302
331
  const restored = mention.start + name.length + 2;
303
332
  requestAnimationFrame(() => areaRef.current?.setSelectionRange(restored, restored));
304
333
  };
334
+ /** Add picked, pasted, or dropped files as drafts on this session. */
335
+ const addFiles = (files) => {
336
+ if (!attachable || files.length === 0)
337
+ return;
338
+ try {
339
+ const created = attachmentFace.create(sessionId, files);
340
+ if (attachmentActions.addAttachments?.(created.map(entry => entry.id)) !== true) {
341
+ for (const entry of created)
342
+ attachmentFace.release(entry.id);
343
+ setError(t('composer.error.generic'));
344
+ return;
345
+ }
346
+ setError(null);
347
+ }
348
+ catch (caught) {
349
+ setError(caught instanceof Error && caught.name === 'UnsupportedImageMediaTypeError' ? t('attach.unsupported') : caught instanceof Error ? caught.message : String(caught));
350
+ }
351
+ };
352
+ const removeAttachment = (id) => {
353
+ // The machine's removal answers nothing on current kits: drop the id, then the draft.
354
+ attachmentActions.removeAttachment?.(id);
355
+ attachmentFace?.release(id);
356
+ };
357
+ const onPaste = (event) => {
358
+ const files = [...event.clipboardData.files];
359
+ if (!attachable || files.length === 0)
360
+ return;
361
+ event.preventDefault();
362
+ addFiles(files);
363
+ };
364
+ // Drag-and-drop is the official composer's: the hidden official bar keeps
365
+ // its document-level drop listener and overlay, and adds dropped files to
366
+ // this same input machine (the drafts render in the rail above). Taking a
367
+ // drop here added it twice; stopping it left that overlay stuck over the
368
+ // page (it saw the drag enter but never the drop).
369
+ // The oldest question a member's round is waiting on (shown inline in its card).
370
+ const waitingAsk = state?.askLog?.find(ask => ask.state === 'pending' && ask.runId !== undefined);
371
+ // Members whose latest round a host restart cut off (outcome unknown).
372
+ const interrupted = (state?.runs ?? [])
373
+ .filter(run => run.state === 'failed' && run.error?.startsWith(RESTART_INTERRUPTED) === true)
374
+ .map(run => run.member)
375
+ .filter(name => state?.members.some(member => member.name === name) === true);
376
+ const continueAll = async () => {
377
+ if (busy || interrupted.length === 0)
378
+ return;
379
+ setBusy(true);
380
+ setError(null);
381
+ try {
382
+ const outcome = await submit(sessionId, t('composer.continueText'), interrupted);
383
+ if (!outcome.ok)
384
+ setError(outcome.message);
385
+ }
386
+ finally {
387
+ setBusy(false);
388
+ }
389
+ };
305
390
  const send = async () => {
306
391
  const text = draft.trim();
307
- if (text === '' || busy)
392
+ const ids = attachable ? attachmentIds : NO_IDS;
393
+ if ((text === '' && ids.length === 0) || busy || uploadsPending)
308
394
  return;
309
395
  setBusy(true);
310
396
  setError(null);
311
397
  try {
312
398
  // Two-argument form while nothing was menu-picked: the injected face's
313
399
  // third parameter is the pick channel only.
314
- const outcome = tag !== undefined
315
- ? await submit(sessionId, text, picked, tag.task)
316
- : picked.length > 0
317
- ? await submit(sessionId, text, picked)
318
- : await submit(sessionId, text);
400
+ const wire = ids.length === 0 ? undefined : await attachmentFace.serialize(ids);
401
+ const outcome = wire !== undefined
402
+ ? await submit(sessionId, text, picked, tag?.task, wire)
403
+ : tag !== undefined
404
+ ? await submit(sessionId, text, picked, tag.task)
405
+ : picked.length > 0
406
+ ? await submit(sessionId, text, picked)
407
+ : await submit(sessionId, text);
319
408
  if (!outcome.ok) {
320
409
  setError(outcome.message);
321
410
  return;
322
411
  }
412
+ if (ids.length > 0) {
413
+ attachmentActions.pruneAttachments?.([]);
414
+ for (const id of ids)
415
+ attachmentFace.release(id);
416
+ }
323
417
  inputActions.setDraft('');
324
418
  // Persist the cleared draft when the host's kit exposes the flush (the
325
419
  // send bypasses the machine's own submit, which would do it).
@@ -380,7 +474,17 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, ta
380
474
  void send();
381
475
  }
382
476
  };
383
- return (_jsxs("div", { className: css.root, children: [_jsxs("div", { className: css.dock, children: [!external && useProjection !== undefined && _jsx(RoomTodoStrip, { useProjection: useProjection, t: t }), state !== undefined && _jsx(RoomTaskCapsule, { state: state, openTasks: openTasks, reconcileDelivery: reconcileDelivery, openPlanSession: openPlanSession, roomCwd: roomCwd, invite: invite, listProviders: () => listProviders(sessionId), browseDirectory: browseDirectory, modelSurface: modelSurface, renderHarnessModelPicker: renderHarnessModelPicker, t: t }), _jsx(RoomQueueStrip, { items: [...external ? [] : queued, ...coordinatorQueue], t: t })] }), error !== null && _jsx("div", { className: css.error, role: "alert", children: error }), external && coordinator.childSessionId !== undefined && renderMemberInbox?.(coordinator.childSessionId), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.coordinator, style: { '--member-color': memberColor(coordinator?.name ?? 'dsh') }, children: [_jsx("span", { className: css.coordinatorDot, "aria-hidden": true }), _jsx("span", { className: css.coordinatorName, children: coordinator?.name ?? 'dsh' }), _jsxs("span", { children: ["\u00B7 ", t('coordinator.label'), " \u00B7 ", external ? coordinator.provider : 'DSH'] })] }), mention !== null && candidates.length > 0 && (_jsxs("ul", { className: css.menu, role: "listbox", "aria-label": t('composer.at.label'), children: [candidates.map((row, index) => (_jsxs("li", { children: [index === 0 && memberRows.length > 0 && others.length > 0 && _jsx("div", { className: css.menuGroup, children: t('composer.at.members') }), index === memberRows.length && (_jsx("div", { className: css.menuGroup, children: t(mention.query === '' ? 'composer.at.recent' : 'composer.at.sessions') })), _jsx("button", { type: "button", role: "option", "aria-selected": index === mention.index, className: index === mention.index ? css.itemActive : css.item, "data-kid": row.kind === 'roomMember' && row.nested === true ? '' : undefined, onMouseDown: (event) => {
477
+ return (_jsxs("div", { className: css.root, children: [_jsxs("div", { className: css.dock, children: [!external && useProjection !== undefined && _jsx(RoomTodoStrip, { useProjection: useProjection, t: t }), state !== undefined && _jsx(RoomTaskCapsule, { state: state, openTasks: openTasks, reconcileDelivery: reconcileDelivery, openPlanSession: openPlanSession, roomCwd: roomCwd, invite: invite, listProviders: () => listProviders(sessionId), browseDirectory: browseDirectory, modelSurface: modelSurface, renderHarnessModelPicker: renderHarnessModelPicker,
478
+ // A member waiting on a question inside its round: the block may be
479
+ // scrolled far up a long reply — its capsule brings it back.
480
+ waiting: waitingAsk === undefined ? undefined : {
481
+ member: waitingAsk.member,
482
+ jump: () => {
483
+ const target = document.querySelector(`[data-ask-id="${CSS.escape(waitingAsk.id)}"]`);
484
+ target?.scrollIntoView({ block: 'center', behavior: 'smooth' });
485
+ target?.querySelector('input, button')?.focus({ preventScroll: true });
486
+ },
487
+ }, t: t }), _jsx(RoomQueueStrip, { items: [...external ? [] : queued, ...coordinatorQueue], t: t }), interrupted.length > 0 && (_jsxs("div", { className: css.resumeStrip, role: "status", children: [_jsx("span", { className: css.resumeText, children: t('composer.interrupted', { count: interrupted.length, names: interrupted.join('、') }) }), _jsx("button", { type: "button", className: css.resumeAction, disabled: busy, onClick: () => { void continueAll(); }, children: t('composer.continueAll') })] }))] }), error !== null && _jsx("div", { className: css.error, role: "alert", children: error }), external && coordinator.childSessionId !== undefined && renderMemberInbox?.(coordinator.childSessionId), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.coordinator, style: { '--member-color': memberColor(coordinator?.name ?? 'dsh') }, children: [_jsx("span", { className: css.coordinatorDot, "aria-hidden": true }), _jsx("span", { className: css.coordinatorName, children: coordinator?.name ?? 'dsh' }), _jsxs("span", { children: ["\u00B7 ", t('coordinator.label'), " \u00B7 ", external ? coordinator.provider : 'DSH'] })] }), mention !== null && candidates.length > 0 && (_jsxs("ul", { className: css.menu, role: "listbox", "aria-label": t('composer.at.label'), children: [candidates.map((row, index) => (_jsxs("li", { children: [index === 0 && memberRows.length > 0 && others.length > 0 && _jsx("div", { className: css.menuGroup, children: t('composer.at.members') }), index === memberRows.length && (_jsx("div", { className: css.menuGroup, children: t(mention.query === '' ? 'composer.at.recent' : 'composer.at.sessions') })), _jsx("button", { type: "button", role: "option", "aria-selected": index === mention.index, className: index === mention.index ? css.itemActive : css.item, "data-kid": row.kind === 'roomMember' && row.nested === true ? '' : undefined, onMouseDown: (event) => {
384
488
  event.preventDefault();
385
489
  pick(row);
386
490
  }, children: row.kind === 'roomMember'
@@ -395,8 +499,8 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, openTasks, ta
395
499
  ? t('team.external.linked')
396
500
  : row.candidate.room === true
397
501
  ? t('composer.at.room')
398
- : row.candidate.createdAt === undefined ? t('composer.at.session') : t('composer.at.sessionAt', { time: shortTime(row.candidate.createdAt, Date.now()) }) })] }) })] }, row.kind === 'member' ? `m:${row.member.name}` : row.kind === 'session' ? `s:${row.candidate.sessionId}` : `r:${row.candidate.sessionId}:${row.member.name}`))), mention.query === '' && others.length > 0 && _jsx("li", { className: css.menuMore, children: t('composer.at.more') })] })), tag !== undefined && (_jsx("div", { className: css.tagRow, children: _jsxs("span", { className: css.tag, "data-new": tag.task === 'new' ? '' : undefined, children: [tag.label, _jsx("button", { type: "button", className: css.tagRemove, "aria-label": t('job.tagRemove'), title: t('job.tagRemove'), onClick: () => { taskTags?.set(sessionId, undefined); }, children: "\u2715" })] }) })), chips.length > 0 && (_jsx("div", { className: css.chipRow, children: chips.map((chip, k) => (_jsxs("span", { className: css.chip, children: [_jsxs("button", { type: "button", className: css.chipLabel, title: chip.label, onClick: () => { openReference?.(chip.source, chip.ref); }, children: [_jsx("span", { "aria-hidden": true, className: css.chipAt, children: "@" }), _jsx("span", { className: css.chipText, children: chip.label })] }), _jsx("button", { type: "button", className: css.tagRemove, "aria-label": t('composer.chipRemove', { label: chip.label }), title: t('composer.chipRemove', { label: chip.label }), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { removeChip(k); }, children: "\u2715" })] }, chip.occurrenceId))) })), _jsx("textarea", { ref: areaRef, className: css.input, rows: 2, value: value, placeholder: tag === undefined ? t('composer.placeholder', { name: coordinator?.name ?? 'dsh' }) : tag.task === 'new' ? t('job.tagPlaceholderNew') : t('job.tagPlaceholder', { label: tag.label }), onChange: onChange, onKeyDown: onKeyDown, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: () => { composing.current = false; justComposed.current = true; }, onKeyUp: () => { justComposed.current = false; }, onBlur: () => { composing.current = false; justComposed.current = false; } }), _jsxs("div", { className: css.row, children: [external && coordinator.childSessionId !== undefined && renderMemberConfiguration?.(coordinator.childSessionId), !external && modelDirectory !== undefined && (_jsx(RoomModelPicker, { directory: modelDirectory, onError: setError, t: t })), running && (_jsx("button", { type: "button", className: css.primary, "aria-label": t('composer.stop'), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { if (external)
502
+ : row.candidate.createdAt === undefined ? t('composer.at.session') : t('composer.at.sessionAt', { time: shortTime(row.candidate.createdAt, Date.now()) }) })] }) })] }, row.kind === 'member' ? `m:${row.member.name}` : row.kind === 'session' ? `s:${row.candidate.sessionId}` : `r:${row.candidate.sessionId}:${row.member.name}`))), mention.query === '' && others.length > 0 && _jsx("li", { className: css.menuMore, children: t('composer.at.more') })] })), tag !== undefined && (_jsx("div", { className: css.tagRow, children: _jsxs("span", { className: css.tag, "data-new": tag.task === 'new' ? '' : undefined, children: [tag.label, _jsx("button", { type: "button", className: css.tagRemove, "aria-label": t('job.tagRemove'), title: t('job.tagRemove'), onClick: () => { taskTags?.set(sessionId, undefined); }, children: "\u2715" })] }) })), chips.length > 0 && (_jsx("div", { className: css.chipRow, children: chips.map((chip, k) => (_jsxs("span", { className: css.chip, children: [_jsxs("button", { type: "button", className: css.chipLabel, title: chip.label, onClick: () => { openReference?.(chip.source, chip.ref); }, children: [_jsx("span", { "aria-hidden": true, className: css.chipAt, children: "@" }), _jsx("span", { className: css.chipText, children: chip.label })] }), _jsx("button", { type: "button", className: css.tagRemove, "aria-label": t('composer.chipRemove', { label: chip.label }), title: t('composer.chipRemove', { label: chip.label }), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { removeChip(k); }, children: "\u2715" })] }, chip.occurrenceId))) })), attachable && (_jsx(RoomAttachments, { attachments: drafts, face: attachmentFace, onRemove: removeAttachment, onRetry: (id) => { attachmentFace.retry(sessionId, id); }, t: t })), _jsx("textarea", { ref: areaRef, className: css.input, rows: 2, value: value, placeholder: tag === undefined ? t('composer.placeholder', { name: coordinator?.name ?? 'dsh' }) : tag.task === 'new' ? t('job.tagPlaceholderNew') : t('job.tagPlaceholder', { label: tag.label }), onChange: onChange, onKeyDown: onKeyDown, onPaste: onPaste, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: () => { composing.current = false; justComposed.current = true; }, onKeyUp: () => { justComposed.current = false; }, onBlur: () => { composing.current = false; justComposed.current = false; } }), _jsxs("div", { className: css.row, children: [attachable && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.attach, "aria-label": t('attach.add'), title: t('attach.add'), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { fileInputRef.current?.click(); }, children: _jsx(IconPaperclipOutlineMedium, { size: 16 }) }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, hidden: true, onChange: (event) => { addFiles([...event.target.files ?? []]); event.target.value = ''; } })] })), external && coordinator.childSessionId !== undefined && renderMemberConfiguration?.(coordinator.childSessionId), !external && modelDirectory !== undefined && (_jsx(RoomModelPicker, { directory: modelDirectory, onError: setError, t: t })), running && (_jsx("button", { type: "button", className: css.primary, "aria-label": t('composer.stop'), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { if (external)
399
503
  stopMember?.(coordinator.name);
400
504
  else
401
- stop(); }, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("rect", { x: "3", y: "3", width: "10", height: "10", rx: "3", fill: "currentColor" }) }) })), _jsx("button", { type: "button", className: css.primary, "aria-label": t('composer.send'), disabled: busy || draft.trim() === '', onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { void send(); }, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("path", { d: "M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z", fill: "currentColor" }) }) })] })] }), !external && useProjection !== undefined && _jsx(RoomStatsLine, { useProjection: useProjection, state: state, t: t })] }));
505
+ stop(); }, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("rect", { x: "3", y: "3", width: "10", height: "10", rx: "3", fill: "currentColor" }) }) })), _jsx("button", { type: "button", className: css.primary, "aria-label": t('composer.send'), disabled: busy || uploadsPending || (draft.trim() === '' && (!attachable || attachmentIds.length === 0)), onMouseDown: (event) => { event.preventDefault(); }, onClick: () => { void send(); }, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("path", { d: "M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z", fill: "currentColor" }) }) })] })] }), useProjection !== undefined && _jsx(RoomStatsLine, { useProjection: useProjection, state: state, membersOnly: external, t: t })] }));
402
506
  }
@@ -4,10 +4,18 @@
4
4
  * picking — one option (or several on a multi-pick question) plus an optional
5
5
  * 其他 free-text line — instead of a numbered list in the member's reply that
6
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).
7
+ * the card then folds to the given answers. When the call ended unanswered
8
+ * (a timeout, or a restart took the member's process) the card stays
9
+ * answerable — the answers then reach the member as a new message (已补答);
10
+ * only a consent gate (room_help's 允许/不允许) folds to 已过期 for good.
8
11
  */
9
12
  import { type ReactNode } from 'react';
10
13
  import type { RoomQuestionViewProps } from './slots.ts';
11
14
  /** The question card. */
12
- export declare function RoomQuestionView({ node, answerQuestion, t }: RoomQuestionViewProps): ReactNode;
15
+ export declare function RoomQuestionView({ node, answerQuestion, inline, t }: RoomQuestionViewProps & {
16
+ /** Inside the asker's round (its run card or reply) instead of a chat row of its own; `mid` = the split point was not found. */
17
+ readonly inline?: {
18
+ readonly mid?: boolean;
19
+ } | undefined;
20
+ }): ReactNode;
13
21
  //# sourceMappingURL=RoomQuestionView.d.ts.map
@@ -5,18 +5,24 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  * picking — one option (or several on a multi-pick question) plus an optional
6
6
  * 其他 free-text line — instead of a numbered list in the member's reply that
7
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).
8
+ * the card then folds to the given answers. When the call ended unanswered
9
+ * (a timeout, or a restart took the member's process) the card stays
10
+ * answerable — the answers then reach the member as a new message (已补答);
11
+ * only a consent gate (room_help's 允许/不允许) folds to 已过期 for good.
9
12
  */
10
13
  import { useState } from 'react';
11
14
  import { memberColor } from './member-color.js';
12
15
  import css from './RoomQuestionView.module.css';
13
16
  /** The question card. */
14
- export function RoomQuestionView({ node, answerQuestion, t }) {
17
+ export function RoomQuestionView({ node, answerQuestion, inline, t }) {
15
18
  const data = node.data;
16
19
  const [drafts, setDrafts] = useState({});
17
20
  const [busy, setBusy] = useState(false);
18
21
  const [error, setError] = useState(undefined);
19
22
  const pending = data.state === 'pending';
23
+ // An expired question still takes an answer; an expired consent gate does not.
24
+ const lateOpen = data.state === 'cancelled' && data.consent !== true;
25
+ const open = pending || lateOpen;
20
26
  const draftOf = (id) => drafts[id] ?? { selected: [], custom: '' };
21
27
  const setDraft = (id, next) => { setDrafts(current => ({ ...current, [id]: next })); };
22
28
  const answered = (id) => draftOf(id).selected.length > 0 || draftOf(id).custom.trim() !== '';
@@ -47,15 +53,22 @@ export function RoomQuestionView({ node, answerQuestion, t }) {
47
53
  const answer = data.answers?.find(entry => entry.id === id);
48
54
  return [...answer?.selected ?? [], ...answer?.custom === undefined ? [] : [answer.custom]].join(';');
49
55
  };
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) => {
56
+ const status = !pending && _jsx("span", { className: css.status, children: t(data.state === 'answered' ? (data.late === true ? 'question.answeredLate' : 'question.answered') : lateOpen ? 'question.lateOpen' : 'question.cancelled') });
57
+ const body = (_jsxs(_Fragment, { children: [data.questions.map((question) => {
51
58
  const multi = question.multiSelect === true;
52
59
  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) => {
60
+ return (_jsxs("fieldset", { className: css.question, disabled: !open || busy, children: [_jsxs("legend", { className: css.prompt, children: [question.header !== undefined && _jsx("span", { className: css.chip, children: question.header }), _jsx("span", { children: question.question }), open && multi && _jsx("span", { className: css.hint, children: t('question.multi') })] }), open ? (_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
61
  // A picked radio unpicks on a second click (the draft allows none).
55
62
  if (!multi && draft.selected.includes(option.label)) {
56
63
  event.preventDefault();
57
64
  pick(question.id, option.label, multi);
58
65
  }
59
66
  } }), _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') })] }))] }));
67
+ }), open && (_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') })] }))] }));
68
+ // Inside the round: no avatar of its own (the round's card has it); the
69
+ // head says who is waiting, or that it was asked here.
70
+ if (inline !== undefined) {
71
+ return (_jsxs("div", { className: css.inline, "data-state": lateOpen ? 'late' : data.state, "data-ask-id": data.questionId, children: [_jsxs("div", { className: css.inlineHead, children: [_jsxs("span", { children: [pending ? t('question.waiting', { member: data.member }) : t('question.asked'), inline.mid === true ? ` · ${t('question.midRound')}` : ''] }), status] }), body] }));
72
+ }
73
+ return (_jsxs("div", { className: css.row, "data-state": lateOpen ? 'late' : data.state, "data-ask-id": data.questionId, style: { '--member-color': memberColor(data.member) }, children: [_jsx("span", { className: css.avatar, "aria-hidden": true, children: data.member.slice(0, 1).toUpperCase() }), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.head, children: [_jsx("span", { className: css.title, children: t('question.title', { member: data.member }) }), status] }), body] })] }));
61
74
  }
@@ -13,5 +13,5 @@
13
13
  import { type ReactNode } from 'react';
14
14
  import type { RoomRunViewProps } from './slots.ts';
15
15
  /** The member run row. */
16
- export declare function RoomRunView({ node, sessionId, roomStore, openSession, cancelMember, renderMemberOutput, t }: RoomRunViewProps): ReactNode;
16
+ export declare function RoomRunView({ node, sessionId, roomStore, openSession, cancelMember, renderMemberOutput, openPath, answerQuestion, t }: RoomRunViewProps): ReactNode;
17
17
  //# sourceMappingURL=RoomRunView.d.ts.map
@@ -11,15 +11,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * avatar and light card, the row as the card's header, the live output
12
12
  * below — so the reply card that replaces it lands in the same place.
13
13
  */
14
- import { useEffect, useState, useSyncExternalStore } from 'react';
14
+ import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
15
15
  import { StateDot, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
16
16
  import { IconStopFillMedium } from './icons.js';
17
17
  import { formatDurationMs } from './format.js';
18
18
  import { memberColor } from './member-color.js';
19
+ import { pathMentions } from './path-mentions.js';
20
+ import { RoomQuestionView } from './RoomQuestionView.js';
21
+ import { askCardData } from './nodes.js';
19
22
  import css from './RoomRunView.module.css';
20
23
  import speech from './RoomSpeechView.module.css';
21
24
  /** The member run row. */
22
- export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMember, renderMemberOutput, t }) {
25
+ export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMember, renderMemberOutput, openPath, answerQuestion, t }) {
23
26
  const state = useSyncExternalStore(roomStore.subscribe, () => roomStore.getCached(sessionId));
24
27
  const recovered = state?.runs.find(run => run.member === node.data.member && run.startedAt === node.data.startedAt);
25
28
  const data = node.data.state === 'running' && recovered !== undefined ? { ...node.data, ...recovered } : node.data;
@@ -31,7 +34,14 @@ export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMem
31
34
  return () => { clearInterval(timer); };
32
35
  }, [data.state]);
33
36
  // The run events carry no delegation handle; the roster record does.
34
- const childSessionId = state?.members.find(entry => entry.name === data.member)?.childSessionId;
37
+ const rosterEntry = state?.members.find(entry => entry.name === data.member);
38
+ const childSessionId = rosterEntry?.childSessionId;
39
+ // Paths the member writes while it works open in the sidebar's file viewer.
40
+ const memberCwd = rosterEntry?.cwd;
41
+ // The question this round stopped on: answered right here, at the end of its live output.
42
+ const runId = recovered?.runId;
43
+ const waiting = runId === undefined ? [] : (state?.askLog ?? []).filter(ask => ask.runId === runId && ask.state === 'pending');
44
+ const mentions = useMemo(() => openPath === undefined ? undefined : pathMentions((path) => { openPath(sessionId, path); }, memberCwd), [openPath, sessionId, memberCwd]);
35
45
  // The Definition hides done/cancelled; guard the same states here.
36
46
  if (data.state === 'done' || data.state === 'cancelled')
37
47
  return null;
@@ -47,11 +57,11 @@ export function RoomRunView({ node, sessionId, roomStore, openSession, cancelMem
47
57
  jump();
48
58
  }
49
59
  };
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) => {
60
+ 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' : waiting.length > 0 ? 'warning' : 'ongoing' }), _jsx("span", { className: css.title, children: failed ? t('run.failed', { member: data.member }) : waiting.length > 0 ? t('run.waiting', { 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
61
  event.stopPropagation();
52
62
  void cancelMember(data.member);
53
63
  }, children: _jsx(IconStopFillMedium, {}) }) }))] }));
54
64
  if (failed)
55
65
  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)] })] }));
66
+ 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 && (mentions === undefined ? renderMemberOutput?.(childSessionId, data.startedAt) : renderMemberOutput?.(childSessionId, data.startedAt, { fileMentions: mentions })), waiting.map(ask => (_jsx(RoomQuestionView, { ...{ node: { data: askCardData(ask) }, answerQuestion, t }, inline: {} }, ask.id)))] })] }));
57
67
  }
@@ -17,7 +17,9 @@
17
17
  * roster/blackboard semantics) and TPS/TTFT (a CLI run has no token stream).
18
18
  */
19
19
  import { type ReactNode } from 'react';
20
+ import { type RoomSpeechData } from './nodes.ts';
20
21
  import type { RoomSpeechViewProps } from './slots.ts';
22
+ import type { RoomAsk } from '../types.ts';
21
23
  /**
22
24
  * The human's expand/collapse choices for one room's speeches, by speech seq.
23
25
  * A per-viewer convenience in browser storage: unreadable storage (private
@@ -28,5 +30,26 @@ export declare const foldChoices: {
28
30
  write(sessionId: string, seq: number, expanded: boolean): void;
29
31
  };
30
32
  /** The member speech row. */
31
- export declare function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, t }: RoomSpeechViewProps): ReactNode;
33
+ /** One piece of a reply: a stretch of its text, or a question the round asked there. */
34
+ type ReplyPart = {
35
+ readonly kind: 'text';
36
+ readonly from: number;
37
+ readonly text: string;
38
+ } | {
39
+ readonly kind: 'ask';
40
+ readonly ask: RoomAsk;
41
+ readonly mid: boolean;
42
+ };
43
+ /**
44
+ * Split a reply around the questions its round asked, in the order it said
45
+ * and asked them (offsets recorded when the reply landed; -1 = the split
46
+ * point was not found, so the question opens the reply marked mid-round).
47
+ * A question the log does not know (an older cache) is left out.
48
+ * @param data - the speech node data.
49
+ * @param askLog - the room's question log.
50
+ * @returns the parts, text and questions interleaved.
51
+ */
52
+ export declare function replyParts(data: RoomSpeechData, askLog: readonly RoomAsk[] | undefined): ReplyPart[];
53
+ export declare function RoomSpeechView({ node, sessionId, roomStore, openSession, openDetail, openPath, answerQuestion, t }: RoomSpeechViewProps): ReactNode;
54
+ export {};
32
55
  //# sourceMappingURL=RoomSpeechView.d.ts.map