@khorsheed/dsh-room 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.en.md +29 -62
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +28 -61
  5. package/lib/client.js +13097 -4182
  6. package/lib/index.js +4045 -730
  7. package/lib/journal-B4FOGO6S.js +482 -0
  8. package/lib/journal-Rid9AAnD.js +485 -0
  9. package/lib/tool.js +112 -166
  10. package/lib/typert.host.js +2031 -374
  11. package/lib/typert.remote-client.d.ts +34 -10
  12. package/lib/typert.remote-client.js +1599 -210
  13. package/lib/types/agent-setup.d.ts +1 -1
  14. package/lib/types/client/InviteAgentAction.js +4 -16
  15. package/lib/types/client/InviteDialog.d.ts +28 -6
  16. package/lib/types/client/InviteDialog.js +76 -12
  17. package/lib/types/client/MemberCard.d.ts +5 -1
  18. package/lib/types/client/MemberCard.js +4 -4
  19. package/lib/types/client/MembersView.d.ts +1 -1
  20. package/lib/types/client/MembersView.js +58 -22
  21. package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
  22. package/lib/types/client/RemoveMemberConfirm.js +85 -0
  23. package/lib/types/client/RoomApprovalView.d.ts +11 -0
  24. package/lib/types/client/RoomApprovalView.js +28 -0
  25. package/lib/types/client/RoomComposer.d.ts +1 -1
  26. package/lib/types/client/RoomComposer.js +195 -29
  27. package/lib/types/client/RoomDecisionView.d.ts +22 -0
  28. package/lib/types/client/RoomDecisionView.js +35 -0
  29. package/lib/types/client/RoomHelpView.d.ts +17 -0
  30. package/lib/types/client/RoomHelpView.js +56 -0
  31. package/lib/types/client/RoomInboxView.d.ts +10 -0
  32. package/lib/types/client/RoomInboxView.js +14 -0
  33. package/lib/types/client/RoomQuestionView.d.ts +13 -0
  34. package/lib/types/client/RoomQuestionView.js +61 -0
  35. package/lib/types/client/RoomRelayView.js +1 -1
  36. package/lib/types/client/RoomRunView.d.ts +3 -1
  37. package/lib/types/client/RoomRunView.js +12 -5
  38. package/lib/types/client/RoomSettingsView.d.ts +19 -0
  39. package/lib/types/client/RoomSettingsView.js +30 -0
  40. package/lib/types/client/RoomSpeechView.d.ts +22 -8
  41. package/lib/types/client/RoomSpeechView.js +94 -11
  42. package/lib/types/client/RoomStatsLine.d.ts +26 -5
  43. package/lib/types/client/RoomStatsLine.js +53 -7
  44. package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
  45. package/lib/types/client/RoomTaskCapsule.js +37 -0
  46. package/lib/types/client/TasksTab.d.ts +41 -0
  47. package/lib/types/client/TasksTab.js +43 -0
  48. package/lib/types/client/TasksView.d.ts +44 -0
  49. package/lib/types/client/TasksView.js +149 -0
  50. package/lib/types/client/TeamView.d.ts +33 -0
  51. package/lib/types/client/TeamView.js +210 -0
  52. package/lib/types/client/at-menu.d.ts +127 -0
  53. package/lib/types/client/at-menu.js +173 -0
  54. package/lib/types/client/draft-chips.d.ts +67 -0
  55. package/lib/types/client/draft-chips.js +95 -0
  56. package/lib/types/client/executions.d.ts +0 -30
  57. package/lib/types/client/executions.js +4 -34
  58. package/lib/types/client/icons-local.d.ts +25 -0
  59. package/lib/types/client/icons-local.js +10 -0
  60. package/lib/types/client/icons.d.ts +2 -1
  61. package/lib/types/client/icons.js +1 -0
  62. package/lib/types/client/index.js +276 -72
  63. package/lib/types/client/locales.d.ts +482 -52
  64. package/lib/types/client/locales.js +490 -60
  65. package/lib/types/client/nodes.d.ts +112 -17
  66. package/lib/types/client/nodes.js +197 -26
  67. package/lib/types/client/slots.d.ts +117 -47
  68. package/lib/types/client/task-tag.d.ts +32 -0
  69. package/lib/types/client/task-tag.js +33 -0
  70. package/lib/types/client/tasks.d.ts +50 -0
  71. package/lib/types/client/tasks.js +82 -0
  72. package/lib/types/client/team.d.ts +73 -0
  73. package/lib/types/client/team.js +187 -0
  74. package/lib/types/decisions.d.ts +49 -0
  75. package/lib/types/decisions.js +101 -0
  76. package/lib/types/deliveries.d.ts +17 -1
  77. package/lib/types/deliveries.js +82 -6
  78. package/lib/types/dispatch.d.ts +92 -4
  79. package/lib/types/dispatch.js +477 -109
  80. package/lib/types/help.d.ts +137 -0
  81. package/lib/types/help.js +216 -0
  82. package/lib/types/helps.d.ts +26 -0
  83. package/lib/types/helps.js +47 -0
  84. package/lib/types/index.d.ts +292 -52
  85. package/lib/types/index.js +1415 -199
  86. package/lib/types/jobs.d.ts +22 -0
  87. package/lib/types/jobs.js +77 -0
  88. package/lib/types/journal.d.ts +17 -27
  89. package/lib/types/journal.js +120 -43
  90. package/lib/types/messaging.d.ts +65 -0
  91. package/lib/types/messaging.js +160 -0
  92. package/lib/types/plan-service.js +7 -1
  93. package/lib/types/report-gate.d.ts +81 -0
  94. package/lib/types/report-gate.js +89 -0
  95. package/lib/types/tool-definition.d.ts +43 -20
  96. package/lib/types/tool-definition.js +145 -212
  97. package/lib/types/tool.d.ts +2 -2
  98. package/lib/types/tool.js +3 -2
  99. package/lib/types/types.d.ts +755 -78
  100. package/locale/zh.json +1 -1
  101. package/package.json +3 -3
  102. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  103. package/lib/types/client/RoomActivityDock.js +0 -105
  104. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  105. package/lib/types/client/RoomDockCapsules.js +0 -277
  106. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  107. package/lib/types/client/RoomPlanTab.js +0 -13
  108. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  109. package/lib/types/client/RoomTaskLineView.js +0 -21
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The in-card removal confirmation: replaces the browser's native confirm
4
+ * with a layer anchored on the member's own card that states what removing
5
+ * THIS member will actually do — stop its running round, cancel its queued
6
+ * messages, tell whoever waits for its
7
+ * report — and then does exactly that (RoomService.removeMember). An idle
8
+ * member with nothing pending gets one sentence. The current coordinator
9
+ * cannot be removed: the layer says why and how instead of offering a
10
+ * button that would only fail. Esc and an outside click cancel; focus opens
11
+ * on 取消 so a stray Enter never removes anyone.
12
+ */
13
+ import { useEffect, useRef, useState } from 'react';
14
+ import css from './RemoveMemberConfirm.module.css';
15
+ /**
16
+ * Read a removal's consequences from the room state the members tab holds.
17
+ * @param state - the replayed room state.
18
+ * @param member - the member about to be removed.
19
+ * @returns the impact summary.
20
+ */
21
+ export function removalImpact(state, member) {
22
+ const run = state.runs.find(entry => entry.member === member.name && entry.state === 'running');
23
+ const id = member.id;
24
+ const open = (state.deliveries ?? []).filter(row => row.memberId === id && (row.status === 'queued' || row.status === 'running'));
25
+ const coordinator = state.coordinator === undefined
26
+ ? state.members.find(entry => entry.kind === 'main-agent')
27
+ : state.members.find(entry => entry.id === state.coordinator.memberId);
28
+ const waiting = new Set();
29
+ for (const row of open) {
30
+ const target = row.plan !== undefined ? coordinator : state.members.find(entry => entry.id !== undefined && entry.id === row.replyTo);
31
+ if (target !== undefined && target.name !== member.name)
32
+ waiting.add(target.name);
33
+ }
34
+ const current = open.find(row => row.status === 'running' && (run?.runId === undefined || row.id === run.runId));
35
+ return {
36
+ running: run === undefined ? undefined : (current?.text.trim().split('\n', 1)[0] ?? '').slice(0, 60),
37
+ queued: open.filter(row => row.status === 'queued').length,
38
+ waiting: [...waiting],
39
+ };
40
+ }
41
+ /** The confirmation layer. */
42
+ export function RemoveMemberConfirm({ member, impact, coordinator, onConfirm, onCancel, t, }) {
43
+ const ref = useRef(null);
44
+ const [busy, setBusy] = useState(false);
45
+ useEffect(() => {
46
+ const onKey = (event) => { if (event.key === 'Escape')
47
+ onCancel(); };
48
+ const onDown = (event) => {
49
+ if (ref.current !== null && !ref.current.contains(event.target))
50
+ onCancel();
51
+ };
52
+ document.addEventListener('keydown', onKey);
53
+ document.addEventListener('mousedown', onDown);
54
+ return () => {
55
+ document.removeEventListener('keydown', onKey);
56
+ document.removeEventListener('mousedown', onDown);
57
+ };
58
+ }, [onCancel]);
59
+ const title = t('remove.title', { member: member.name });
60
+ if (coordinator) {
61
+ return (_jsxs("div", { ref: ref, className: css.layer, role: "dialog", "aria-label": title, children: [_jsx("p", { className: css.title, children: title }), _jsx("p", { className: css.body, children: t('remove.coordinator') }), _jsx("div", { className: css.actions, children: _jsx("button", { type: "button", className: css.secondary, autoFocus: true, onClick: onCancel, children: t('remove.ok') }) })] }));
62
+ }
63
+ const lines = [];
64
+ if (impact.running !== undefined) {
65
+ lines.push(impact.running === '' ? t('remove.running') : t('remove.runningTask', { task: impact.running }));
66
+ }
67
+ if (impact.queued > 0)
68
+ lines.push(t('remove.queued', { count: impact.queued }));
69
+ if (impact.waiting.length > 0)
70
+ lines.push(t('remove.waiting', { members: impact.waiting.join('、') }));
71
+ const confirm = async () => {
72
+ if (busy)
73
+ return;
74
+ setBusy(true);
75
+ try {
76
+ await onConfirm();
77
+ }
78
+ finally {
79
+ setBusy(false);
80
+ }
81
+ };
82
+ return (_jsxs("div", { ref: ref, className: css.layer, role: "dialog", "aria-label": title, children: [_jsx("p", { className: css.title, children: title }), lines.length === 0
83
+ ? _jsx("p", { className: css.body, children: t('remove.idle') })
84
+ : _jsx("ul", { className: css.list, children: lines.map((line, index) => _jsx("li", { children: line }, index)) }), _jsx("p", { className: css.note, children: t('remove.keep') }), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.secondary, autoFocus: true, onClick: onCancel, children: t('remove.cancel') }), _jsx("button", { type: "button", className: css.danger, disabled: busy, onClick: () => { void confirm(); }, children: impact.running !== undefined ? t('remove.stopAndRemove') : t('remove.confirm') })] })] }));
85
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A member's tool approval ('room-approval' keyed renderer): one line in the
3
+ * room's conversation — 「⚠ coco 要你确认:调用 video_render」 with the reason
4
+ * — and [允许一次] [拒绝] while it waits. The answer folds in place through the
5
+ * journal (allowed / rejected / cancelled when the call ended first).
6
+ */
7
+ import { type ReactNode } from 'react';
8
+ import type { RoomApprovalViewProps } from './slots.ts';
9
+ /** The approval line. */
10
+ export declare function RoomApprovalView({ node, answerApproval, t }: RoomApprovalViewProps): ReactNode;
11
+ //# sourceMappingURL=RoomApprovalView.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A member's tool approval ('room-approval' keyed renderer): one line in the
4
+ * room's conversation — 「⚠ coco 要你确认:调用 video_render」 with the reason
5
+ * — and [允许一次] [拒绝] while it waits. The answer folds in place through the
6
+ * journal (allowed / rejected / cancelled when the call ended first).
7
+ */
8
+ import { useState } from 'react';
9
+ import css from './RoomRelayView.module.css';
10
+ /** The approval line. */
11
+ export function RoomApprovalView({ node, answerApproval, t }) {
12
+ const data = node.data;
13
+ const [busy, setBusy] = useState(false);
14
+ const [error, setError] = useState(undefined);
15
+ const answer = async (outcome) => {
16
+ if (busy)
17
+ return;
18
+ setBusy(true);
19
+ try {
20
+ setError(await answerApproval(data.approvalId, outcome));
21
+ }
22
+ finally {
23
+ setBusy(false);
24
+ }
25
+ };
26
+ const pending = data.state === 'pending';
27
+ return (_jsxs("div", { className: css.row, "data-state": pending ? 'pending' : 'resolved', children: [_jsxs("span", { className: css.text, children: [t('approval.line', { member: data.member, tool: data.tool }), data.reason === undefined ? '' : `(${data.reason})`, pending ? '' : ` ${t(`approval.state.${data.state}`)}`, error === undefined ? '' : ` ${error}`] }), pending && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.action, disabled: busy, onClick: () => { void answer('allowed-once'); }, children: t('approval.allow') }), _jsx("button", { type: "button", className: css.action, disabled: busy, onClick: () => { void answer('rejected'); }, children: t('approval.reject') })] }))] }));
28
+ }
@@ -17,5 +17,5 @@ import type { RoomComposerMatch, RoomComposerProps } from './slots.ts';
17
17
  */
18
18
  export declare function selectRoomComposer(owner: ComposerChainProps, isRoomCached: (sessionId: SessionId) => boolean): RoomComposerMatch | null;
19
19
  /** The room composer takeover component. */
20
- export declare function RoomComposer({ sessionId, roomStore, submit, stop, addTask, closeTask, setGoal, planCommand, openPlanSession, openPlan, stopExecution, backgroundSessions, activeChildren, stopChild, reconcileDelivery, roomCwd, invite, listProviders, listNames, browseDirectory, modelSurface, renderHarnessModelPicker, roomChrome, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, useSession, useProjection, useInput, inputActions, t, }: RoomComposerProps): ReactNode;
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;
21
21
  //# sourceMappingURL=RoomComposer.d.ts.map
@@ -1,11 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Room composer: the `conversation.composer` chain takeover for room
4
4
  * sessions. Replaces the official input bar while the selector holds (the
5
5
  * official bar is display:none underneath), so it owns the whole interaction:
6
6
  * a textarea (Enter sends, Shift+Enter newlines), a send button, and the
7
- * @-completion menu listing ONLY existing roster members (pure addressing —
8
- * invitation lives in the members tab). Addressing is the union of two
7
+ * @ menu in two groups: this room's members (pure addressing — invitation
8
+ * lives in the members tab) and other sessions (the host's session-reference
9
+ * candidates; a pick inserts the host's own session chip, the mention the
10
+ * room's agent asks for help through — see at-menu.ts). Addressing is the union of two
9
11
  * channels: leading `@name` tokens (the host's parseMentions grammar,
10
12
  * stripped from the dispatched text) and the mention menu's picks (a pick
11
13
  * records explicit addressing wherever the `@name` sits in the sentence —
@@ -24,16 +26,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
26
  * share one draft — text typed during the first-pull election window
25
27
  * carries over, drafts persist across navigation, and external draft
26
28
  * writers (quote-anything's 引用到当前会话) land here with zero
27
- * room-specific code. A reference chip in the machine projects to its
28
- * clipboard text in this plain textarea, and editing flattens chips back
29
- * to text (`setDraft` semantics).
29
+ * room-specific code. Reference chips in the machine (a quote, any trigger
30
+ * source's pick) render as their own row above the textarea — label, click
31
+ * to preview through the chip's source, × to remove — and the textarea holds
32
+ * only the typed text around them; edits map back as detect-span
33
+ * replacements, so typing never flattens a chip (draft-chips.ts). A host
34
+ * without the machine's chip view or `insertText` keeps the old plain form:
35
+ * chips project to their clipboard text and editing flattens them.
30
36
  *
31
37
  * Taking over the chain inherits the official bar's environment duties (the
32
38
  * member-channel rule, local-agent 84a2ed0): a pending interaction yields (the
33
39
  * selector declines while `owner.pendingInteraction` is set, so the priority-1
34
40
  * ApprovalPanel elects), and everything the hidden fallback tree carried is
35
41
  * re-homed INTO this component — the main agent's turn Stop (the send circle
36
- * swaps while `running`), the dock capsules (RoomDockCapsules), the main
42
+ * swaps while `running`), the task capsule (RoomTaskCapsule), the main
37
43
  * agent's todo strip (RoomTodoStrip, the `todos` projection), the queued-
38
44
  * messages strip (RoomQueueStrip, the `inbox` projection — the 0.1.5 session
39
45
  * snapshot's queue on the old line), the session
@@ -42,18 +48,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
42
48
  * itself is single-owner and cannot be re-hosted, so the picker is our own
43
49
  * trigger on the same directory; a host without ui-model-selection gets no
44
50
  * picker).
51
+ *
52
+ * The task page's 「追问」 / 「+ 新任务」 put a removable chip on the card
53
+ * (「任务 N」 / 「新任务」, the shared RoomTaskTags store) and focus the
54
+ * textarea; the next send carries it as postMessage's `task` and clears it.
55
+ * Removing the chip leaves the filing to dsh.
45
56
  */
46
57
  import { coordinatorMember } from '../journal.js';
47
58
  import { useEffect, useRef, useState, useSyncExternalStore, } from 'react';
59
+ import { chipDetectIndex, draftSegments, freeText, planFreeEdit } from './draft-chips.js';
60
+ import { detectOffset, memberMention, otherRows, rankByMatch, sessionMention, sessionTitle, shortTime } from './at-menu.js';
48
61
  import { memberColor } from './member-color.js';
49
62
  import { RoomStatsLine } from './RoomStatsLine.js';
50
- import { RoomDockCapsules } from './RoomDockCapsules.js';
63
+ import { RoomTaskCapsule } from './RoomTaskCapsule.js';
51
64
  import { RoomTodoStrip } from './RoomTodoStrip.js';
52
- import { RoomActivityDock } from './RoomActivityDock.js';
53
65
  import { RoomQueueStrip } from './RoomQueueStrip.js';
54
- import { RoomRecoveryView } from './RoomRecoveryView.js';
55
66
  import { RoomModelPicker } from './RoomModelPicker.js';
67
+ import { IconChevronRightOutlineMedium } from './icons.js';
56
68
  import css from './RoomComposer.module.css';
69
+ const NO_TAGS = () => () => { };
57
70
  const EMPTY_LEGACY_QUEUE = [];
58
71
  const QUEUE_PREVIEW_CHARS = 200;
59
72
  /** The queue row's flat preview: attachment blocks dropped, the rest joined and whitespace-collapsed, 200 chars max (the official QueueDock's rule). */
@@ -98,13 +111,24 @@ export function selectRoomComposer(owner, isRoomCached) {
98
111
  const sessionId = owner.sessionId ?? owner.session?.sessionId;
99
112
  return sessionId !== undefined && isRoomCached(sessionId) ? { room: true } : null;
100
113
  }
114
+ const NO_CHIPS = [];
101
115
  /** The room composer takeover component. */
102
- export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, closeTask, setGoal, planCommand, openPlanSession, openPlan, stopExecution, backgroundSessions, activeChildren, stopChild, reconcileDelivery, roomCwd, invite, listProviders, listNames, browseDirectory, modelSurface, renderHarnessModelPicker, roomChrome, modelDirectory, renderMemberConfiguration, renderMemberInbox, stopMember, useSession, useProjection, useInput, inputActions, t, }) {
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, }) {
103
117
  const state = useSyncExternalStore(roomStore.subscribe, () => roomStore.getCached(sessionId));
118
+ const tag = useSyncExternalStore(taskTags?.subscribe ?? NO_TAGS, () => taskTags?.get(sessionId));
104
119
  // The draft is the session's official input machine (the module header):
105
120
  // shared with the hidden fallback bar, persistent across navigation, and
106
121
  // writable by external drafters (quote) with zero room-specific code.
107
122
  const draft = useInput(input => input.draft);
123
+ // The machine's chips (absent before the occurrence view shipped) and the
124
+ // span-guarded text edit (absent on older faces): both present = chip mode.
125
+ const occurrences = useInput(input => input.occurrences);
126
+ const chipEdit = inputActions;
127
+ const chipMode = occurrences !== undefined && occurrences.length > 0
128
+ && chipEdit.insertText !== undefined && chipEdit.captureInsertion !== undefined;
129
+ const chips = chipMode ? occurrences : NO_CHIPS;
130
+ // What the textarea shows: the typed text around the chips.
131
+ const value = chipMode ? freeText(draft, chips) : draft;
108
132
  const [busy, setBusy] = useState(false);
109
133
  const [error, setError] = useState(null);
110
134
  const [mention, setMention] = useState(null);
@@ -112,10 +136,59 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
112
136
  `@name` sits (see the module header); cleared with the draft on send. */
113
137
  const [picked, setPicked] = useState([]);
114
138
  const areaRef = useRef(null);
139
+ // IME composition (the official ui-primitives observeComposition rule):
140
+ // Enter while composing, or the one that closes a composition (Safari
141
+ // fires compositionend before its keydown), commits the candidate — it
142
+ // never sends. keyCode 229 covers engines without isComposing.
143
+ const composing = useRef(false);
144
+ const justComposed = useRef(false);
115
145
  const members = state?.members ?? [];
116
- const candidates = mention === null
117
- ? []
118
- : members.filter(member => member.name.startsWith(mention.query));
146
+ // The other-sessions group: the host's candidates for the open query (the
147
+ // recent list plus the query's own lookup, merged). A room starts folded;
148
+ // its arrow (or →) shows its members, and a typed query matches members by name.
149
+ const [sessionPool, setSessionPool] = useState([]);
150
+ const [expanded, setExpanded] = useState(new Set());
151
+ const toggleRoom = (id, open) => {
152
+ setExpanded((current) => {
153
+ const next = new Set(current);
154
+ if (open ?? !next.has(id))
155
+ next.add(id);
156
+ else
157
+ next.delete(id);
158
+ return next;
159
+ });
160
+ };
161
+ const query = mention?.query;
162
+ // The injected face is rebuilt on every render (and the room re-renders
163
+ // every second while anything runs): read it through a ref so only a new
164
+ // query cancels the lookup — keyed on the function, no lookup ever finished.
165
+ const listRef = useRef(listSessions);
166
+ listRef.current = listSessions;
167
+ const canList = listSessions !== undefined;
168
+ useEffect(() => {
169
+ const list = listRef.current;
170
+ if (query === undefined || list === undefined)
171
+ return;
172
+ const abort = new AbortController();
173
+ const timer = setTimeout(() => {
174
+ list(query, abort.signal).then((found) => { if (!abort.signal.aborted)
175
+ setSessionPool(found); }).catch(() => { });
176
+ }, query === '' ? 0 : 120);
177
+ return () => { clearTimeout(timer); abort.abort(); };
178
+ }, [query, canList]);
179
+ // Outside sessions kept on the member list sit in the members group.
180
+ const linked = state?.linked ?? [];
181
+ const excluded = new Set([
182
+ sessionId, ...linked.map(entry => entry.sessionId),
183
+ ...members.flatMap(member => member.childSessionId === undefined ? [] : [member.childSessionId]),
184
+ ]);
185
+ const memberRows = mention === null ? [] : [
186
+ ...rankByMatch(mention.query, members, member => member.name).map(member => ({ kind: 'member', member })),
187
+ ...rankByMatch(mention.query, linked, entry => entry.title)
188
+ .map(entry => ({ kind: 'session', candidate: { sessionId: entry.sessionId, label: entry.title, mention: sessionMention(entry.sessionId, entry.title), linked: true } })),
189
+ ];
190
+ const others = mention === null ? [] : otherRows(mention.query, sessionPool, excluded, expanded);
191
+ const candidates = [...memberRows, ...others];
119
192
  // The inherited environment state: the room's own main-agent turn flag
120
193
  // (drives the Send/Stop swap) and the still-queued inbox rows (the official
121
194
  // queue dock's data, re-homed as a read-only strip). alpha.2 moved the
@@ -135,29 +208,93 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
135
208
  .filter(row => row.placement === 'queued')
136
209
  .map(row => ({ id: String(row.id), preview: row.preview }))
137
210
  : nextTurn.map(row => ({ id: String(row.id), preview: previewOf(row.content) }));
211
+ // The coordinator's own room deliveries: a round a restart interrupted
212
+ // blocks the ones after it until the human reconciles or stops its task —
213
+ // say so, instead of a silent queue.
214
+ const coordinatorRows = (state?.deliveries ?? []).filter(row => row.memberId === coordinator?.id && (row.status === 'queued' || row.status === 'uncertain'));
215
+ const blocked = coordinatorRows.some(row => row.status === 'uncertain');
216
+ const coordinatorQueue = coordinatorRows.map(row => ({
217
+ id: row.id,
218
+ preview: row.status === 'uncertain' ? t('job.queueUncertain', { text: row.text }) : blocked ? t('job.queueBlocked', { text: row.text }) : row.text,
219
+ }));
138
220
  const resize = (area) => {
139
221
  area.style.height = 'auto';
140
222
  area.style.height = `${area.scrollHeight}px`;
141
223
  };
224
+ // A new tag (the task page's 追问 / 新任务) hands the textarea focus.
225
+ useEffect(() => {
226
+ if (tag !== undefined)
227
+ areaRef.current?.focus();
228
+ }, [tag?.revision]);
142
229
  // External draft writes (a quote insertion, the election window's
143
230
  // carry-over) never fire onChange — keep the height in sync with the draft.
144
231
  useEffect(() => {
145
232
  if (areaRef.current !== null)
146
233
  resize(areaRef.current);
147
234
  }, [draft]);
235
+ // Write the textarea's text back: chip mode maps the change onto the
236
+ // machine as span edits (each against the then-current revision, last
237
+ // first), so the chips survive; otherwise the whole draft is replaced.
238
+ const writeText = (next) => {
239
+ if (!chipMode) {
240
+ inputActions.setDraft(next);
241
+ return;
242
+ }
243
+ for (const edit of planFreeEdit(draft, chips, next)) {
244
+ const draftRev = chipEdit.captureInsertion().draftRev;
245
+ if (!chipEdit.insertText(edit.text, { start: edit.start, end: edit.end, draftRev }))
246
+ return;
247
+ }
248
+ };
249
+ // Remove one chip: delete its single detect placeholder.
250
+ const removeChip = (k) => {
251
+ const start = chipDetectIndex(draft, chips, k);
252
+ chipEdit.insertText?.('', { start, end: start + 1, draftRev: chipEdit.captureInsertion().draftRev });
253
+ areaRef.current?.focus();
254
+ };
148
255
  const onChange = (event) => {
149
- const value = event.target.value;
150
- inputActions.setDraft(value);
256
+ const next = event.target.value;
257
+ writeText(next);
151
258
  resize(event.target);
152
- const active = detectMention(value, event.target.selectionStart ?? value.length);
259
+ const active = detectMention(next, event.target.selectionStart ?? next.length);
153
260
  setMention(active === null ? null : { ...active, index: 0 });
154
261
  };
262
+ // A session pick: the host's own session chip replaces the typed `@query`
263
+ // (detect coordinates: the chips before it each fold to one placeholder);
264
+ // a host without the insertion seam gets the mention as text.
265
+ const applySession = (candidate) => {
266
+ setExpanded(new Set());
267
+ if (mention === null)
268
+ return;
269
+ const caret = areaRef.current?.selectionStart ?? value.length;
270
+ setMention(null);
271
+ if (insertSessionReference !== undefined && chipEdit.captureInsertion !== undefined) {
272
+ const segments = chipMode ? draftSegments(draft, chips) : [value];
273
+ const span = { start: detectOffset(segments, mention.start), end: detectOffset(segments, caret), draftRev: chipEdit.captureInsertion().draftRev };
274
+ if (insertSessionReference(candidate, span)) {
275
+ requestAnimationFrame(() => areaRef.current?.focus());
276
+ return;
277
+ }
278
+ }
279
+ writeText(`${value.slice(0, mention.start)}${candidate.mention} ${value.slice(caret)}`);
280
+ };
281
+ const pick = (row) => {
282
+ if (row.kind === 'member')
283
+ applyMention(row.member.name);
284
+ else if (row.kind === 'session')
285
+ applySession(row.candidate);
286
+ else {
287
+ // A room's member: the room's own chip under a 「成员(房间)」 label.
288
+ const named = memberMention(row.candidate, row.member);
289
+ applySession({ ...row.candidate, mention: named.mention, displayTitle: named.label });
290
+ }
291
+ };
155
292
  const applyMention = (name) => {
156
293
  if (mention === null)
157
294
  return;
158
- const caret = areaRef.current?.selectionStart ?? draft.length;
159
- const next = `${draft.slice(0, mention.start)}@${name} ${draft.slice(caret)}`;
160
- inputActions.setDraft(next);
295
+ const caret = areaRef.current?.selectionStart ?? value.length;
296
+ const next = `${value.slice(0, mention.start)}@${name} ${value.slice(caret)}`;
297
+ writeText(next);
161
298
  setMention(null);
162
299
  setPicked(picked.includes(name) ? picked : [...picked, name]);
163
300
  // The machine tracks no caret: restore it behind the spliced pick once
@@ -174,9 +311,11 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
174
311
  try {
175
312
  // Two-argument form while nothing was menu-picked: the injected face's
176
313
  // third parameter is the pick channel only.
177
- const outcome = picked.length > 0
178
- ? await submit(sessionId, text, picked)
179
- : await submit(sessionId, text);
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);
180
319
  if (!outcome.ok) {
181
320
  setError(outcome.message);
182
321
  return;
@@ -188,6 +327,7 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
188
327
  flushDraft?.();
189
328
  setMention(null);
190
329
  setPicked([]);
330
+ taskTags?.set(sessionId, undefined);
191
331
  }
192
332
  catch {
193
333
  setError(t('composer.error.generic'));
@@ -197,6 +337,11 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
197
337
  }
198
338
  };
199
339
  const onKeyDown = (event) => {
340
+ // oxlint-disable-next-line typescript/no-deprecated -- IME 229 covers engines without isComposing.
341
+ const ime = composing.current || justComposed.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
342
+ justComposed.current = false;
343
+ if (ime)
344
+ return;
200
345
  if (mention !== null) {
201
346
  if (event.key === 'Escape') {
202
347
  event.preventDefault();
@@ -214,9 +359,18 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
214
359
  setMention({ ...mention, index: (mention.index + candidates.length - 1) % candidates.length });
215
360
  return;
216
361
  }
362
+ const active = candidates[mention.index];
363
+ if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft') && active.kind !== 'member' && (active.kind === 'roomMember' || active.candidate.room === true)) {
364
+ event.preventDefault();
365
+ toggleRoom(active.candidate.sessionId, event.key === 'ArrowRight');
366
+ if (event.key === 'ArrowLeft' && active.kind === 'roomMember') {
367
+ setMention({ ...mention, index: candidates.findIndex(row => row.kind === 'session' && row.candidate.sessionId === active.candidate.sessionId) });
368
+ }
369
+ return;
370
+ }
217
371
  if (event.key === 'Enter' || event.key === 'Tab') {
218
372
  event.preventDefault();
219
- applyMention(candidates[mention.index].name);
373
+ pick(active);
220
374
  return;
221
375
  }
222
376
  }
@@ -226,11 +380,23 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
226
380
  void send();
227
381
  }
228
382
  };
229
- return (_jsxs("div", { className: css.root, children: [_jsxs("div", { className: css.dock, children: [!external && useProjection !== undefined && _jsx(RoomTodoStrip, { useProjection: useProjection, t: t }), (state === undefined || (state.plan === undefined && (state.executions?.length ?? 0) === 0)) && _jsx(RoomDockCapsules, { sessionId: sessionId, roomStore: roomStore, addTask: addTask, closeTask: closeTask, setGoal: setGoal, formalPlans: planCommand !== undefined, roomCwd: roomCwd, invite: invite, listProviders: listProviders, listNames: listNames, browseDirectory: browseDirectory, modelSurface: modelSurface, renderHarnessModelPicker: renderHarnessModelPicker, roomChrome: roomChrome, t: t }), state !== undefined && reconcileDelivery !== undefined && _jsx(RoomRecoveryView, { deliveries: state.deliveries ?? [], members: state.members, reconcile: reconcileDelivery, openSession: openPlanSession, t: t }), _jsx(RoomQueueStrip, { items: [...external ? [] : queued, ...(state?.deliveries ?? []).filter(row => row.memberId === coordinator?.id && (row.status === 'queued' || row.status === 'uncertain')).map(row => ({ id: row.id, preview: row.status === 'uncertain' ? `${row.error ?? 'Outcome unknown'}: ${row.text}` : row.text }))], t: t }), state !== undefined && _jsx(RoomActivityDock, { sessionId: sessionId, state: state, backgroundSessions: backgroundSessions, activeChildren: activeChildren, stopChild: stopChild, openSession: openPlanSession, stopExecution: stopExecution, planCommand: planCommand, openPlan: openPlan, stopMember: stopMember, t: t }, sessionId)] }), 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, children: [t('coordinator.label'), " \u00B7 ", coordinator?.name ?? 'dsh', external ? ` · ${coordinator.provider}` : ' · DSH'] }), mention !== null && candidates.length > 0 && (_jsx("ul", { className: css.menu, role: "listbox", "aria-label": "members", children: candidates.map((member, index) => (_jsx("li", { children: _jsxs("button", { type: "button", role: "option", "aria-selected": index === mention.index, className: index === mention.index ? css.itemActive : css.item, onMouseDown: (event) => {
230
- event.preventDefault();
231
- applyMention(member.name);
232
- }, children: [_jsx("span", { className: css.dot, style: { background: memberColor(member.name) }, "aria-hidden": true }), _jsx("span", { children: member.name }), _jsx("span", { className: css.hint, children: member.kind === 'main-agent' ? t('member.kind.main') : member.provider ?? '' })] }) }, member.name))) })), _jsx("textarea", { ref: areaRef, className: css.input, rows: 2, value: draft, placeholder: t('composer.placeholder'), onChange: onChange, onKeyDown: onKeyDown }), _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)
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) => {
384
+ event.preventDefault();
385
+ pick(row);
386
+ }, children: row.kind === 'roomMember'
387
+ ? _jsxs(_Fragment, { children: [_jsx("span", { className: css.dot, style: { background: memberColor(row.member.name) }, "aria-hidden": true }), _jsxs("span", { className: css.itemText, children: [row.member.name, row.nested !== true && _jsxs("span", { className: css.itemContext, children: [" \u00B7 ", sessionTitle(row.candidate)] })] }), _jsx("span", { className: css.hint, children: row.member.kind === 'main-agent' ? t('member.kind.main') : row.member.provider ?? '' })] })
388
+ : row.kind === 'member'
389
+ ? _jsxs(_Fragment, { children: [_jsx("span", { className: css.dot, style: { background: memberColor(row.member.name) }, "aria-hidden": true }), _jsx("span", { className: css.itemText, children: row.member.name }), _jsx("span", { className: css.hint, children: row.member.kind === 'main-agent' ? t('member.kind.main') : row.member.provider ?? '' })] })
390
+ : _jsxs(_Fragment, { children: [_jsx("span", { className: css.sessionMark, "aria-hidden": true, children: "@" }), _jsx("span", { className: css.itemText, children: sessionTitle(row.candidate) }), row.candidate.room === true && (row.candidate.members?.length ?? 0) > 0 && (_jsx("span", { role: "button", tabIndex: -1, className: css.expand, "data-open": expanded.has(row.candidate.sessionId) ? '' : undefined, "aria-label": t(expanded.has(row.candidate.sessionId) ? 'composer.at.collapse' : 'composer.at.expand'), onMouseDown: (event) => {
391
+ event.preventDefault();
392
+ event.stopPropagation();
393
+ toggleRoom(row.candidate.sessionId);
394
+ }, children: _jsx(IconChevronRightOutlineMedium, { size: 14 }) })), _jsx("span", { className: css.hint, children: row.candidate.linked === true
395
+ ? t('team.external.linked')
396
+ : row.candidate.room === true
397
+ ? 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)
233
399
  stopMember?.(coordinator.name);
234
400
  else
235
- 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, t: t })] }));
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 })] }));
236
402
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One decision the room made on its own ('room-decision' keyed renderer): a
3
+ * single dim line in the room's conversation — the scenario's outcome, the
4
+ * rule that decided or the model's probability and latency, and 查看判断 for a
5
+ * decision that called the decision model (it opens Context Lens at that
6
+ * request). The row reads like the relay / approval status lines.
7
+ */
8
+ import type { ReactNode } from 'react';
9
+ import type { RoomDecisionEvent } from '../types.ts';
10
+ import type { RoomDecisionViewProps } from './slots.ts';
11
+ type Translate = RoomDecisionViewProps['t'];
12
+ /**
13
+ * The line's text for one decision.
14
+ * @param decision - the recorded decision.
15
+ * @param t - the room translate.
16
+ * @returns the outcome sentence.
17
+ */
18
+ export declare function decisionLine(decision: RoomDecisionEvent, t: Translate): string;
19
+ /** The decision row. */
20
+ export declare function RoomDecisionView({ node, openDecision, t }: RoomDecisionViewProps): ReactNode;
21
+ export {};
22
+ //# sourceMappingURL=RoomDecisionView.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import css from './RoomRelayView.module.css';
3
+ /**
4
+ * The line's text for one decision.
5
+ * @param decision - the recorded decision.
6
+ * @param t - the room translate.
7
+ * @returns the outcome sentence.
8
+ */
9
+ export function decisionLine(decision, t) {
10
+ const failed = decision.call?.failure !== undefined;
11
+ if (decision.scenario === 'task-filing') {
12
+ if (decision.rule === 'unavailable')
13
+ return t(failed ? 'decision.filing.failed' : 'decision.filing.unavailable');
14
+ return decision.about.task === undefined
15
+ ? t('decision.filing.new')
16
+ : t('decision.filing.task', { number: decision.about.task.number, title: decision.about.task.title });
17
+ }
18
+ const from = decision.about.from ?? '';
19
+ const to = decision.about.to ?? '';
20
+ if (decision.rule === 'unavailable')
21
+ return t(failed ? 'decision.report.failed' : 'decision.report.unavailable', { from, to });
22
+ if (decision.rule !== undefined)
23
+ return t('decision.report.rule', { from, to, rule: t(`decision.rule.${decision.rule}`) });
24
+ return t(decision.outcome === 'later' ? 'decision.report.later' : 'decision.report.now', { from, to });
25
+ }
26
+ /** The decision row. */
27
+ export function RoomDecisionView({ node, openDecision, t }) {
28
+ const decision = node.data.decision;
29
+ const call = decision.call;
30
+ const meta = [
31
+ ...decision.probability === undefined ? [] : [decision.probability.toFixed(2)],
32
+ ...call?.latencyMs === undefined ? [] : [`${(call.latencyMs / 1000).toFixed(1)}s`],
33
+ ].join(' · ');
34
+ return (_jsxs("div", { className: css.row, "data-decision": decision.scenario, children: [_jsx("span", { "aria-hidden": true, children: call?.failure !== undefined ? '⚠' : '🧭' }), _jsx("span", { className: css.text, children: decisionLine(decision, t) }), meta !== '' && _jsx("span", { children: meta }), call !== undefined && openDecision !== undefined && (_jsx("button", { type: "button", className: css.action, onClick: () => { openDecision(decision.id); }, children: t('decision.open') }))] }));
35
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cross-session help, on both ends.
3
+ *
4
+ * - `room-help` (the asking room): one line per thread — who asked which
5
+ * session, where it stands — that opens to the request and every message
6
+ * on the thread (the helper's questions, its reply with the handoff, the
7
+ * asker's follow-ups), plus a jump into the helper's session.
8
+ * - `room-help-request` (the helper's session): the request the room queued
9
+ * there, as a card naming the asking room and member, with a jump back.
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import type { RoomHelpRequestViewProps, RoomHelpViewProps } from './slots.ts';
13
+ /** The thread row in the asking room. */
14
+ export declare function RoomHelpView({ node, sessionId, roomStore, openSession, linkSession, dismissNudge, t }: RoomHelpViewProps): ReactNode;
15
+ /** The request card in the helper's session. */
16
+ export declare function RoomHelpRequestView({ node, openSession, t }: RoomHelpRequestViewProps): ReactNode;
17
+ //# sourceMappingURL=RoomHelpView.d.ts.map