@khorsheed/dsh-room 0.2.1 → 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 (114) hide show
  1. package/CHANGELOG.md +59 -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 +34057 -6483
  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 +2517 -363
  11. package/lib/typert.remote-client.d.ts +53 -9
  12. package/lib/typert.remote-client.js +1909 -280
  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 +228 -37
  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/preset-visibility.d.ts +12 -2
  68. package/lib/types/client/preset-visibility.js +54 -13
  69. package/lib/types/client/slots.d.ts +124 -51
  70. package/lib/types/client/task-tag.d.ts +32 -0
  71. package/lib/types/client/task-tag.js +33 -0
  72. package/lib/types/client/tasks.d.ts +50 -0
  73. package/lib/types/client/tasks.js +82 -0
  74. package/lib/types/client/team.d.ts +73 -0
  75. package/lib/types/client/team.js +187 -0
  76. package/lib/types/decisions.d.ts +49 -0
  77. package/lib/types/decisions.js +101 -0
  78. package/lib/types/deliveries.d.ts +17 -1
  79. package/lib/types/deliveries.js +82 -6
  80. package/lib/types/dispatch.d.ts +92 -4
  81. package/lib/types/dispatch.js +477 -109
  82. package/lib/types/help.d.ts +137 -0
  83. package/lib/types/help.js +216 -0
  84. package/lib/types/helps.d.ts +26 -0
  85. package/lib/types/helps.js +47 -0
  86. package/lib/types/index.d.ts +292 -52
  87. package/lib/types/index.js +1415 -199
  88. package/lib/types/jobs.d.ts +22 -0
  89. package/lib/types/jobs.js +77 -0
  90. package/lib/types/journal.d.ts +17 -27
  91. package/lib/types/journal.js +120 -43
  92. package/lib/types/messaging.d.ts +65 -0
  93. package/lib/types/messaging.js +160 -0
  94. package/lib/types/plan-service.js +7 -1
  95. package/lib/types/report-gate.d.ts +81 -0
  96. package/lib/types/report-gate.js +89 -0
  97. package/lib/types/tool-definition.d.ts +43 -20
  98. package/lib/types/tool-definition.js +145 -212
  99. package/lib/types/tool.d.ts +2 -2
  100. package/lib/types/tool.js +3 -2
  101. package/lib/types/types.d.ts +755 -78
  102. package/locale/zh.json +1 -1
  103. package/package.json +27 -29
  104. package/lib/invariant.js +0 -26
  105. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  106. package/lib/types/client/RoomActivityDock.js +0 -105
  107. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  108. package/lib/types/client/RoomDockCapsules.js +0 -277
  109. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  110. package/lib/types/client/RoomPlanTab.js +0 -13
  111. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  112. package/lib/types/client/RoomTaskLineView.js +0 -21
  113. package/lib/types/invariant.d.ts +0 -16
  114. package/lib/types/invariant.js +0 -24
@@ -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, 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,31 +1,45 @@
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 —
12
14
  * the text is dispatched verbatim, the picks ride the postMessage request's
13
15
  * `targets`). Hand-typed mid-sentence mentions without a menu pick stay
14
- * prose: they address nobody. A bare message (neither channel) is NOT room
15
- * business: it is released to the official submit
16
- * path — the takeover writes the text into the session's official input
17
- * machine (`inputActions.setDraft` + `inputActions.submit()`, the same entry
18
- * the InputBar's Enter key drives), so it becomes an ordinary main-agent
19
- * turn; sent while that turn runs, the machine's busy admission enqueues it
20
- * exactly as the official bar's does. A structured rejection (unknown
21
- * targets) shows as an inline error line.
16
+ * prose: they address nobody. EVERY send — bare text included — routes
17
+ * through room's own dispatch (the postMessage Remote; the host routes bare
18
+ * text to the current coordinator), never through `inputActions.submit()`:
19
+ * the machine's busy admission would enqueue behind a running coordinator
20
+ * and serialize what the dispatch engine deliberately runs in parallel. A
21
+ * structured rejection (unknown targets) shows as an inline error line.
22
+ *
23
+ * The DRAFT, though, lives in the session's official input machine: the
24
+ * chain occupant's standard session kit (`useInput`/`inputActions`) is this
25
+ * component's draft store, so the takeover and the display:none fallback
26
+ * share one draft — text typed during the first-pull election window
27
+ * carries over, drafts persist across navigation, and external draft
28
+ * writers (quote-anything's 引用到当前会话) land here with zero
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.
22
36
  *
23
37
  * Taking over the chain inherits the official bar's environment duties (the
24
38
  * member-channel rule, local-agent 84a2ed0): a pending interaction yields (the
25
39
  * selector declines while `owner.pendingInteraction` is set, so the priority-1
26
40
  * ApprovalPanel elects), and everything the hidden fallback tree carried is
27
41
  * re-homed INTO this component — the main agent's turn Stop (the send circle
28
- * swaps while `running`), the dock capsules (RoomDockCapsules), the main
42
+ * swaps while `running`), the task capsule (RoomTaskCapsule), the main
29
43
  * agent's todo strip (RoomTodoStrip, the `todos` projection), the queued-
30
44
  * messages strip (RoomQueueStrip, the `inbox` projection — the 0.1.5 session
31
45
  * snapshot's queue on the old line), the session
@@ -34,18 +48,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
48
  * itself is single-owner and cannot be re-hosted, so the picker is our own
35
49
  * trigger on the same directory; a host without ui-model-selection gets no
36
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.
37
56
  */
38
57
  import { coordinatorMember } from '../journal.js';
39
- import { useRef, useState, useSyncExternalStore, } from 'react';
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';
40
61
  import { memberColor } from './member-color.js';
41
62
  import { RoomStatsLine } from './RoomStatsLine.js';
42
- import { RoomDockCapsules } from './RoomDockCapsules.js';
63
+ import { RoomTaskCapsule } from './RoomTaskCapsule.js';
43
64
  import { RoomTodoStrip } from './RoomTodoStrip.js';
44
- import { RoomActivityDock } from './RoomActivityDock.js';
45
65
  import { RoomQueueStrip } from './RoomQueueStrip.js';
46
- import { RoomRecoveryView } from './RoomRecoveryView.js';
47
66
  import { RoomModelPicker } from './RoomModelPicker.js';
67
+ import { IconChevronRightOutlineMedium } from './icons.js';
48
68
  import css from './RoomComposer.module.css';
69
+ const NO_TAGS = () => () => { };
49
70
  const EMPTY_LEGACY_QUEUE = [];
50
71
  const QUEUE_PREVIEW_CHARS = 200;
51
72
  /** The queue row's flat preview: attachment blocks dropped, the rest joined and whitespace-collapsed, 200 chars max (the official QueueDock's rule). */
@@ -90,10 +111,24 @@ export function selectRoomComposer(owner, isRoomCached) {
90
111
  const sessionId = owner.sessionId ?? owner.session?.sessionId;
91
112
  return sessionId !== undefined && isRoomCached(sessionId) ? { room: true } : null;
92
113
  }
114
+ const NO_CHIPS = [];
93
115
  /** The room composer takeover component. */
94
- 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, 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, }) {
95
117
  const state = useSyncExternalStore(roomStore.subscribe, () => roomStore.getCached(sessionId));
96
- const [draft, setDraft] = useState('');
118
+ const tag = useSyncExternalStore(taskTags?.subscribe ?? NO_TAGS, () => taskTags?.get(sessionId));
119
+ // The draft is the session's official input machine (the module header):
120
+ // shared with the hidden fallback bar, persistent across navigation, and
121
+ // writable by external drafters (quote) with zero room-specific code.
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;
97
132
  const [busy, setBusy] = useState(false);
98
133
  const [error, setError] = useState(null);
99
134
  const [mention, setMention] = useState(null);
@@ -101,10 +136,59 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
101
136
  `@name` sits (see the module header); cleared with the draft on send. */
102
137
  const [picked, setPicked] = useState([]);
103
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);
104
145
  const members = state?.members ?? [];
105
- const candidates = mention === null
106
- ? []
107
- : 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];
108
192
  // The inherited environment state: the room's own main-agent turn flag
109
193
  // (drives the Send/Stop swap) and the still-queued inbox rows (the official
110
194
  // queue dock's data, re-homed as a read-only strip). alpha.2 moved the
@@ -124,25 +208,99 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
124
208
  .filter(row => row.placement === 'queued')
125
209
  .map(row => ({ id: String(row.id), preview: row.preview }))
126
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
+ }));
127
220
  const resize = (area) => {
128
221
  area.style.height = 'auto';
129
222
  area.style.height = `${area.scrollHeight}px`;
130
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]);
229
+ // External draft writes (a quote insertion, the election window's
230
+ // carry-over) never fire onChange — keep the height in sync with the draft.
231
+ useEffect(() => {
232
+ if (areaRef.current !== null)
233
+ resize(areaRef.current);
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
+ };
131
255
  const onChange = (event) => {
132
- const value = event.target.value;
133
- setDraft(value);
256
+ const next = event.target.value;
257
+ writeText(next);
134
258
  resize(event.target);
135
- const active = detectMention(value, event.target.selectionStart ?? value.length);
259
+ const active = detectMention(next, event.target.selectionStart ?? next.length);
136
260
  setMention(active === null ? null : { ...active, index: 0 });
137
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
+ };
138
292
  const applyMention = (name) => {
139
293
  if (mention === null)
140
294
  return;
141
- const caret = areaRef.current?.selectionStart ?? draft.length;
142
- const next = `${draft.slice(0, mention.start)}@${name} ${draft.slice(caret)}`;
143
- 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);
144
298
  setMention(null);
145
299
  setPicked(picked.includes(name) ? picked : [...picked, name]);
300
+ // The machine tracks no caret: restore it behind the spliced pick once
301
+ // the store round-trip has re-rendered the textarea.
302
+ const restored = mention.start + name.length + 2;
303
+ requestAnimationFrame(() => areaRef.current?.setSelectionRange(restored, restored));
146
304
  };
147
305
  const send = async () => {
148
306
  const text = draft.trim();
@@ -153,16 +311,23 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
153
311
  try {
154
312
  // Two-argument form while nothing was menu-picked: the injected face's
155
313
  // third parameter is the pick channel only.
156
- const outcome = picked.length > 0
157
- ? await submit(sessionId, text, picked)
158
- : 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);
159
319
  if (!outcome.ok) {
160
320
  setError(outcome.message);
161
321
  return;
162
322
  }
163
- setDraft('');
323
+ inputActions.setDraft('');
324
+ // Persist the cleared draft when the host's kit exposes the flush (the
325
+ // send bypasses the machine's own submit, which would do it).
326
+ const flushDraft = inputActions.persistDraft;
327
+ flushDraft?.();
164
328
  setMention(null);
165
329
  setPicked([]);
330
+ taskTags?.set(sessionId, undefined);
166
331
  }
167
332
  catch {
168
333
  setError(t('composer.error.generic'));
@@ -172,6 +337,11 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
172
337
  }
173
338
  };
174
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;
175
345
  if (mention !== null) {
176
346
  if (event.key === 'Escape') {
177
347
  event.preventDefault();
@@ -189,9 +359,18 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
189
359
  setMention({ ...mention, index: (mention.index + candidates.length - 1) % candidates.length });
190
360
  return;
191
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
+ }
192
371
  if (event.key === 'Enter' || event.key === 'Tab') {
193
372
  event.preventDefault();
194
- applyMention(candidates[mention.index].name);
373
+ pick(active);
195
374
  return;
196
375
  }
197
376
  }
@@ -201,11 +380,23 @@ export function RoomComposer({ sessionId, roomStore, submit, stop, addTask, clos
201
380
  void send();
202
381
  }
203
382
  };
204
- 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) => {
205
- event.preventDefault();
206
- applyMention(member.name);
207
- }, 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)
208
399
  stopMember?.(coordinator.name);
209
400
  else
210
- 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 })] }));
211
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