@itookit/dsht 0.3.0 → 0.3.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 (134) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +39 -23
  3. package/README.zh.md +40 -24
  4. package/dist/catalog/controller.d.ts +32 -0
  5. package/dist/catalog/controller.js +88 -0
  6. package/dist/catalog/index.d.ts +2 -0
  7. package/dist/catalog/index.js +2 -0
  8. package/dist/{cli.js → cli/index.js} +42 -29
  9. package/dist/controller/connection.d.ts +80 -0
  10. package/dist/controller/connection.js +190 -0
  11. package/dist/controller/controller.d.ts +269 -0
  12. package/dist/controller/controller.js +372 -0
  13. package/dist/controller/index.d.ts +5 -0
  14. package/dist/controller/index.js +3 -0
  15. package/dist/controller/memory-log.d.ts +35 -0
  16. package/dist/controller/memory-log.js +95 -0
  17. package/dist/cost/config.d.ts +17 -0
  18. package/dist/cost/config.js +68 -0
  19. package/dist/cost/controller.d.ts +41 -0
  20. package/dist/cost/controller.js +115 -0
  21. package/dist/cost/index.d.ts +10 -0
  22. package/dist/cost/index.js +8 -0
  23. package/dist/cost/ledger-files.d.ts +16 -0
  24. package/dist/cost/ledger-files.js +103 -0
  25. package/dist/cost/ledger.d.ts +78 -0
  26. package/dist/cost/ledger.js +146 -0
  27. package/dist/cost/pricing.d.ts +86 -0
  28. package/dist/cost/pricing.js +223 -0
  29. package/dist/cost/records.d.ts +17 -0
  30. package/dist/cost/records.js +79 -0
  31. package/dist/cost/scanner.d.ts +22 -0
  32. package/dist/cost/scanner.js +95 -0
  33. package/dist/cost/types.d.ts +85 -0
  34. package/dist/cost/types.js +7 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +2 -0
  37. package/dist/session/connection-view.d.ts +18 -0
  38. package/dist/session/connection-view.js +1 -0
  39. package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
  40. package/dist/session/controller.js +616 -0
  41. package/dist/session/export-html.d.ts +9 -0
  42. package/dist/session/export-html.js +39 -0
  43. package/dist/{export.d.ts → session/export.d.ts} +1 -1
  44. package/dist/{export.js → session/export.js} +6 -20
  45. package/dist/{history.d.ts → session/history.d.ts} +17 -0
  46. package/dist/{history.js → session/history.js} +163 -2
  47. package/dist/session/index.d.ts +17 -0
  48. package/dist/session/index.js +10 -0
  49. package/dist/session/markdown.d.ts +39 -0
  50. package/dist/session/markdown.js +255 -0
  51. package/dist/session/math.d.ts +11 -0
  52. package/dist/session/math.js +82 -0
  53. package/dist/session/navigation.d.ts +37 -0
  54. package/dist/session/navigation.js +84 -0
  55. package/dist/{references.js → session/references.js} +1 -1
  56. package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
  57. package/dist/{telemetry.js → session/telemetry.js} +1 -1
  58. package/dist/{transcript.d.ts → session/transcript.d.ts} +65 -1
  59. package/dist/{transcript.js → session/transcript.js} +141 -20
  60. package/dist/session/types.d.ts +18 -0
  61. package/dist/session/types.js +2 -0
  62. package/dist/state.d.ts +41 -0
  63. package/dist/state.js +9 -0
  64. package/dist/storage/directories.d.ts +14 -0
  65. package/dist/storage/directories.js +24 -0
  66. package/dist/storage/files.d.ts +51 -0
  67. package/dist/storage/files.js +152 -0
  68. package/dist/storage/heap-snapshot.d.ts +19 -0
  69. package/dist/storage/heap-snapshot.js +29 -0
  70. package/dist/storage/index.d.ts +4 -0
  71. package/dist/storage/index.js +4 -0
  72. package/dist/transport/auth.js +65 -0
  73. package/dist/transport/host.d.ts +13 -0
  74. package/dist/transport/host.js +1 -0
  75. package/dist/ui/app.d.ts +11 -0
  76. package/dist/ui/app.js +790 -0
  77. package/dist/ui/chat/header.d.ts +11 -0
  78. package/dist/ui/chat/header.js +14 -0
  79. package/dist/ui/chat/history-view.d.ts +12 -0
  80. package/dist/ui/chat/history-view.js +17 -0
  81. package/dist/ui/chat/status.d.ts +91 -0
  82. package/dist/ui/chat/status.js +386 -0
  83. package/dist/ui/chat/viewport.d.ts +17 -0
  84. package/dist/ui/chat/viewport.js +14 -0
  85. package/dist/ui/commands/parse.d.ts +99 -0
  86. package/dist/ui/commands/parse.js +126 -0
  87. package/dist/ui/commands/registry.d.ts +33 -0
  88. package/dist/ui/commands/registry.js +73 -0
  89. package/dist/ui/copy-mode.d.ts +4 -0
  90. package/dist/ui/copy-mode.js +6 -0
  91. package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
  92. package/dist/{cost-view.js → ui/dialogs/cost.js} +6 -6
  93. package/dist/ui/dialogs/index.d.ts +120 -0
  94. package/dist/ui/dialogs/index.js +113 -0
  95. package/dist/ui/dialogs/picker.d.ts +18 -0
  96. package/dist/ui/dialogs/picker.js +38 -0
  97. package/dist/ui/frozen.d.ts +8 -0
  98. package/dist/ui/frozen.js +7 -0
  99. package/dist/ui/input/references.d.ts +12 -0
  100. package/dist/ui/input/references.js +15 -0
  101. package/dist/ui/mount.d.ts +6 -0
  102. package/dist/ui/mount.js +11 -0
  103. package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
  104. package/package.json +19 -13
  105. package/dist/app.d.ts +0 -21
  106. package/dist/app.js +0 -805
  107. package/dist/auth.js +0 -108
  108. package/dist/controller.js +0 -961
  109. package/dist/cost.d.ts +0 -119
  110. package/dist/cost.js +0 -313
  111. package/dist/history-view.d.ts +0 -8
  112. package/dist/history-view.js +0 -12
  113. package/dist/navigation.d.ts +0 -11
  114. package/dist/navigation.js +0 -36
  115. package/dist/status.d.ts +0 -28
  116. package/dist/status.js +0 -157
  117. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  118. /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
  119. /package/dist/{memory.js → session/memory.js} +0 -0
  120. /package/dist/{references.d.ts → session/references.d.ts} +0 -0
  121. /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
  122. /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
  123. /package/dist/{client.js → transport/client.js} +0 -0
  124. /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
  125. /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
  126. /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
  127. /package/dist/{wire.js → transport/wire.js} +0 -0
  128. /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
  129. /package/dist/{input-history.js → ui/input/history.js} +0 -0
  130. /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
  131. /package/dist/{input.js → ui/input/input.js} +0 -0
  132. /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
  133. /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
  134. /package/dist/{theme.js → ui/theme/index.js} +0 -0
package/dist/app.js DELETED
@@ -1,805 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /** Ink terminal interface: startup pickers, transcript, and slash-command composer. */
3
- import { HistoryViewport } from "./history-view.js";
4
- import { mocha, ThemeContext, useTheme } from "./theme.js";
5
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, memo, createContext, useContext } from 'react';
6
- import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout } from 'ink';
7
- import { useMouseWheel } from "./mouse.js";
8
- import { TextInput } from "./input.js";
9
- import { InputHistory } from "./input-history.js";
10
- import { historyLayout, releaseHistoryLayout } from "./history.js";
11
- import { toolLine } from "./transcript.js";
12
- import { activeReference, fileMention } from "./references.js";
13
- import { CostPanel } from "./cost-view.js";
14
- import { StatusBar } from "./status.js";
15
- import { navigationCommand, sessionLabel } from "./navigation.js";
16
- import { array, errorText, object, safeText, string } from "./wire.js";
17
- /** Command discovery catalog shared by Tab completion and the `/help` panel. */
18
- export const COMMAND_HINTS = [
19
- { command: '/ws', usage: '[name or ID]', description: 'List/switch workspaces; --delete name removes registration' },
20
- { command: '/resume', usage: '[title or ID]', description: 'List/switch sessions; --delete ID archives with confirmation' },
21
- { command: '/model', usage: '[provider model [effort]]', description: 'Choose a model and reasoning effort for subsequent requests' },
22
- { command: '/new', description: 'Create a session in the selected workspace' },
23
- { command: '/copy', description: 'Freeze for native selection; Esc resumes (Ctrl+S shortcut)' },
24
- { command: '/latest', description: 'Return to the live conversation' },
25
- { command: '/older', description: 'Load earlier history' },
26
- { command: '/history', usage: '[text]', description: 'List your prompts, optionally filtered' },
27
- { command: '/search', usage: 'text', description: 'Search history page by page and open a match' },
28
- { command: '/ssearch', usage: 'text', description: 'Search sessions in the current workspace' },
29
- { command: '/wsearch', usage: 'text', description: 'Search sessions across all workspaces' },
30
- { command: '/compact', description: 'Compact older history while the session is idle' },
31
- { command: '/cancel', description: 'Cancel the active turn' },
32
- { command: '/queue', description: 'View and remove pending input' },
33
- { command: '/plan', usage: '[off|message]', description: 'Enter or leave host plan mode' },
34
- { command: '/goal', usage: '[action|objective]', description: 'View or manage the host task goal' },
35
- { command: '/permission', usage: '[preset]', description: 'View or switch the host permission preset' },
36
- { command: '/feedback', usage: 'text', description: 'Record feedback about the session' },
37
- { command: '/export', usage: '[local.zip]', description: 'Save the session log ZIP to a new local file' },
38
- { command: '/allow', description: 'Approve the pending request once' },
39
- { command: '/deny', description: 'Reject the pending request' },
40
- { command: '/status', description: 'Show full session status details' },
41
- { command: '/cost', description: 'Show cost estimates and refresh usage' },
42
- { command: '/think', usage: '[seq or live]', description: 'Inspect reasoning with user prompt summaries' },
43
- { command: '/help', description: 'Show this command list' },
44
- { command: '/quit', description: 'Exit dsht' },
45
- ];
46
- const COMMANDS = COMMAND_HINTS.map(hint => hint.command);
47
- /** Command column text per hint, aligned in the `/help` panel. */
48
- const COMMAND_LABELS = COMMAND_HINTS.map(hint => hint.usage === undefined ? hint.command : `${hint.command} ${hint.usage}`);
49
- /** Widest command column, so descriptions start on one column. */
50
- const COMMAND_LABEL_WIDTH = Math.max(...COMMAND_LABELS.map(label => label.length)) + 2;
51
- /** Longest common prefix of the candidate commands, so Tab can extend an ambiguous draft. */
52
- export function commonPrefix(values) {
53
- let prefix = values[0] ?? '';
54
- for (const value of values) {
55
- let index = 0;
56
- while (index < prefix.length && index < value.length && prefix[index] === value[index])
57
- index++;
58
- prefix = prefix.slice(0, index);
59
- }
60
- return prefix;
61
- }
62
- /** Transient notices expire; interactive panels remain open until dismissed. */
63
- const PANEL_LIFETIME_MS = 10_000;
64
- const CopyMode = createContext(false);
65
- /** Retain rendered children while frozen; local dialog interactions remain outside this wrapper. */
66
- const Frozen = memo(function Frozen({ children }) {
67
- return _jsx(_Fragment, { children: children });
68
- }, (previous, next) => previous.frozen && next.frozen && previous.identity === next.identity);
69
- function Picker({ choices, enabled, canSelect, pageSize = 12, hint }) {
70
- const theme = useTheme();
71
- const copyMode = useContext(CopyMode);
72
- const [selected, setSelected] = useState(0);
73
- const current = Math.min(selected, choices.length - 1);
74
- const { internal_eventEmitter } = useStdin();
75
- const rawKey = useRef('');
76
- useEffect(() => {
77
- const remember = (raw) => { rawKey.current = raw; };
78
- internal_eventEmitter.prependListener('input', remember);
79
- return () => { internal_eventEmitter.removeListener('input', remember); };
80
- }, [internal_eventEmitter]);
81
- useInput((_input, key) => {
82
- if (!canSelect() || key.eventType === 'release')
83
- return;
84
- if (key.upArrow)
85
- setSelected(Math.max(0, current - 1));
86
- else if (key.downArrow)
87
- setSelected(Math.min(choices.length - 1, current + 1));
88
- else if (_input === 'd' && !key.ctrl && !key.meta || key.delete && /^\x1b\[3(?:;\d+)?~$/.test(rawKey.current))
89
- choices[current]?.remove?.();
90
- else if (key.return)
91
- choices[current]?.action();
92
- }, { isActive: enabled && !copyMode });
93
- const start = Math.max(0, current - Math.max(0, pageSize - 1));
94
- return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start + pageSize).map((choice, index) => _jsxs(Text, { color: start + index === current ? theme.accent : undefined, children: [start + index === current ? '❯ ' : ' ', safeText(choice.label)] }, choice.key)), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
95
- }
96
- /** The caller owns starting and stopping the controller around the Ink render lifetime. */
97
- export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = mocha }) {
98
- const state = useSyncExternalStore(controller.subscribe, controller.snapshot);
99
- const presetId = controller.telemetry.view(state.sessionId).values.agentPreset;
100
- useEffect(() => { if (typeof presetId === 'string')
101
- controller.loadPresetNames(); }, [controller, state.online, presetId]);
102
- const { exit } = useApp();
103
- const { stdout } = useStdout();
104
- const [copyMode, setCopyMode] = useState(false);
105
- const [input, updateInput] = useState('');
106
- const draft = useRef('');
107
- const inputHistory = useRef(new InputHistory());
108
- useLayoutEffect(() => {
109
- const history = new InputHistory();
110
- inputHistory.current = history;
111
- if (!state.transcript.ready)
112
- return;
113
- const messages = state.transcript.messagesForWidth(Math.max(20, (stdout.columns ?? 100) - 2));
114
- // Seed once per loaded session, never scan historical messages on stream ticks or arrow presses.
115
- const prompts = messages.filter(message => message.role === 'You').slice(-200);
116
- for (const message of prompts)
117
- history.record(message.text.replace(/\r?\n/g, ' ').trim());
118
- }, [state.sessionId, state.transcript, state.transcript.ready]);
119
- const [cursor, setCursor] = useState(0);
120
- // Input callbacks may run before Ink refreshes the controlled field's listener.
121
- const setInput = (value, recalled = false) => {
122
- if (!recalled)
123
- inputHistory.current.reset();
124
- draft.current = value;
125
- updateInput(value);
126
- setCursor(value.length);
127
- };
128
- const [historyWindow, setHistoryWindow] = useState();
129
- const [historyLoading, setHistoryLoading] = useState();
130
- const [historyMatches, setHistoryMatches] = useState();
131
- const displayTranscript = historyWindow ?? state.transcript;
132
- const displayRef = useRef(displayTranscript);
133
- displayRef.current = displayTranscript;
134
- useEffect(() => () => { if (historyWindow) {
135
- releaseHistoryLayout(historyWindow);
136
- historyWindow.dispose();
137
- } }, [historyWindow]);
138
- const conversationBox = useRef(null);
139
- const [conversationRows, setConversationRows] = useState(20);
140
- useLayoutEffect(() => {
141
- if (conversationBox.current) {
142
- const height = Math.floor(measureElement(conversationBox.current).height);
143
- if (height !== conversationRows)
144
- setConversationRows(height);
145
- }
146
- });
147
- const [scroll, setScroll] = useState(0);
148
- const [liveReasoning, setLiveReasoning] = useState('row');
149
- const [removal, setRemoval] = useState();
150
- const [models, setModels] = useState();
151
- const [thoughtList, setThoughtList] = useState(false);
152
- const [queueOpen, setQueueOpen] = useState(false);
153
- const [historyQuery, setHistoryQuery] = useState();
154
- const [contentSearch, setContentSearch] = useState(false);
155
- const [searchResults, setSearchResults] = useState();
156
- const historyAbort = useRef(undefined);
157
- useEffect(() => () => historyAbort.current?.abort(), []);
158
- const [costExpanded, setCostExpanded] = useState(false);
159
- const [statusExpanded, setStatusExpanded] = useState(false);
160
- const [reasoningOverrides, setReasoningOverrides] = useState(new Set());
161
- const reasoning = 'row';
162
- useEffect(() => { setReasoningOverrides(new Set()); setModels(undefined); setThoughtList(false); setHistoryWindow(undefined); setHistoryMatches(undefined); }, [state.transcript]);
163
- useEffect(() => { setLiveReasoning('row'); }, [state.transcript, state.transcript.liveAttemptKey]);
164
- const [notice, setNotice] = useState();
165
- const [help, setHelp] = useState(false);
166
- const [helpPage, setHelpPage] = useState(0);
167
- const helpPageSize = Math.max(1, (stdout.rows ?? 30) - 12);
168
- const helpPages = Math.ceil(COMMAND_HINTS.length / helpPageSize);
169
- const currentHelpPage = Math.min(helpPage, helpPages - 1);
170
- const [answers, setAnswers] = useState({});
171
- const [optionState, setOptionState] = useState();
172
- const [referenceIndex, setReferenceIndex] = useState(0);
173
- const [dismissedReference, dismissReference] = useState();
174
- const [lookup, setLookup] = useState();
175
- // Interactive panels remain stable while read; only transient notices expire.
176
- useEffect(() => {
177
- if (copyMode)
178
- return;
179
- if (notice === undefined)
180
- return;
181
- const timer = setTimeout(() => setNotice(undefined), panelLifetimeMs);
182
- return () => clearTimeout(timer);
183
- }, [notice, panelLifetimeMs, copyMode]);
184
- const pending = state.pending[0];
185
- const queued = controller.telemetry.pending(state.sessionId).filter(item => item.placement !== 'context');
186
- useEffect(() => { setQueueOpen(false); }, [state.sessionId, pending?.eventId]);
187
- const token = state.screen === 'chat' && state.online && !state.busy && !pending
188
- && !input.startsWith('/') && dismissedReference !== input && cursor === input.length
189
- ? activeReference(input) : undefined;
190
- const referenceOpen = token !== undefined;
191
- const dialogOpen = !!(queueOpen || removal || models || thoughtList || historyQuery !== undefined || searchResults || costExpanded || statusExpanded || help || pending || referenceOpen || state.screen !== 'chat');
192
- const displayPaused = copyMode || dialogOpen;
193
- const matches = referenceOpen && lookup?.draft === input && lookup.sessionId === state.sessionId ? lookup : undefined;
194
- useEffect(() => {
195
- if (!referenceOpen)
196
- return;
197
- const abort = new AbortController();
198
- setLookup(undefined);
199
- setReferenceIndex(0);
200
- void controller.references(activeReference(input).query, abort.signal).then(items => {
201
- if (!abort.signal.aborted)
202
- setLookup({ draft: input, sessionId: state.sessionId, items });
203
- }, error => {
204
- if (!abort.signal.aborted)
205
- setLookup({ draft: input, sessionId: state.sessionId, items: [], error: errorText(error) });
206
- });
207
- return () => abort.abort();
208
- }, [controller, input, state.sessionId, referenceOpen]);
209
- const pickReference = () => {
210
- const candidate = matches?.items[referenceIndex];
211
- if (!candidate || !token || draft.current !== input)
212
- return;
213
- const mention = fileMention(candidate, token.quoted);
214
- setInput(input.slice(0, -token.prefix.length) + mention + (candidate.kind === 'file' ? ' ' : ''));
215
- };
216
- /** Complete the leading slash command; an ambiguous draft extends to the shared prefix. */
217
- const completeCommand = () => {
218
- if (!input.startsWith('/') || input.includes(' '))
219
- return;
220
- const matches = COMMANDS.filter(command => command.startsWith(input));
221
- const only = matches.length === 1 ? matches[0] : undefined;
222
- if (only !== undefined) {
223
- setInput(`${only} `);
224
- return;
225
- }
226
- const prefix = commonPrefix(matches);
227
- if (prefix.length > input.length)
228
- setInput(prefix);
229
- };
230
- const questions = pending?.event === 'user-questions/request' ? array(object(pending.request).questions).map(object) : [];
231
- const eventId = pending ? string(pending.eventId) : '';
232
- const answered = answers[eventId] ?? [];
233
- const question = questions[answered.length];
234
- const optionKey = `${eventId}:${answered.length}`;
235
- const options = question ? array(question.options ?? []).map(object) : [];
236
- const choiceState = optionState?.key === optionKey ? optionState : { key: optionKey, cursor: 0, selected: [], custom: false };
237
- const optionCursor = Math.min(choiceState.cursor, options.length);
238
- // Reserve the header, composer and question instructions; each choice may have a description.
239
- const optionPageSize = Math.max(1, Math.min(6, Math.floor(((stdout.rows ?? 30) - 16) / 2)));
240
- const optionStart = Math.max(0, optionCursor - optionPageSize + 1);
241
- const questionKeysActive = !!question && options.length > 0 && !choiceState.custom && !copyMode
242
- && !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded;
243
- const answerQuestion = async (selected, custom) => {
244
- if (controller.state.pending[0]?.eventId !== eventId)
245
- throw new Error('The pending question has changed');
246
- const answer = { id: string(question.id), selected, ...(custom ? { custom } : {}) };
247
- const next = [...answered, answer];
248
- if (next.length === questions.length) {
249
- await controller.answer({ answers: next });
250
- setAnswers(previous => { const rest = { ...previous }; delete rest[eventId]; return rest; });
251
- }
252
- else
253
- setAnswers(previous => ({ ...previous, [eventId]: next }));
254
- setOptionState(undefined);
255
- };
256
- const operate = (fn) => { void controller.perform(fn); };
257
- const requestRemoval = async (kind, query) => {
258
- const target = await controller.removalTarget(kind, query);
259
- if (target.kind === 'session' && target.empty)
260
- await controller.removeTarget(target);
261
- else
262
- setRemoval(target);
263
- };
264
- useInput((_value, key) => {
265
- if (key.eventType === 'release')
266
- return;
267
- if (copyMode) {
268
- if (key.escape || key.ctrl && (_value === 's' || _value === 'c'))
269
- setCopyMode(false);
270
- return;
271
- }
272
- if (key.ctrl && _value === 's') {
273
- setCopyMode(true);
274
- return;
275
- }
276
- if ((key.escape || key.ctrl && _value === 'c') && historyAbort.current) {
277
- historyAbort.current.abort();
278
- return;
279
- }
280
- if ((key.escape || key.ctrl && _value === 'c') && controller.state.pending.length) {
281
- if (key.ctrl && draft.current)
282
- setInput('');
283
- if (key.escape) {
284
- setOptionState({ ...choiceState, custom: false });
285
- setRemoval(undefined);
286
- setModels(undefined);
287
- setThoughtList(false);
288
- setSearchResults(undefined);
289
- setHistoryQuery(undefined);
290
- setHistoryMatches(undefined);
291
- setHelp(false);
292
- setCostExpanded(false);
293
- setStatusExpanded(false);
294
- }
295
- return;
296
- }
297
- if (questionKeysActive && !draft.current && !controller.state.busy && !key.ctrl && !key.meta) {
298
- const digit = /^[1-9]$/.test(_value) ? Number(_value) - 1 : -1;
299
- if (key.upArrow || key.downArrow) {
300
- setOptionState({ ...choiceState, cursor: Math.max(0, Math.min(options.length, optionCursor + (key.upArrow ? -1 : 1))) });
301
- return;
302
- }
303
- if (digit >= 0 && digit <= options.length || _value === ' ' && question.multiSelect === true && optionCursor < options.length) {
304
- const index = digit >= 0 ? digit : optionCursor;
305
- const label = index < options.length ? string(options[index].label) : undefined;
306
- const selected = question.multiSelect === true && label
307
- ? choiceState.selected.includes(label) ? choiceState.selected.filter(item => item !== label) : [...choiceState.selected, label]
308
- : choiceState.selected;
309
- setOptionState({ ...choiceState, cursor: index, selected });
310
- return;
311
- }
312
- if (key.return) {
313
- if (optionCursor === options.length) {
314
- setOptionState({ ...choiceState, custom: true });
315
- return;
316
- }
317
- const selected = question.multiSelect === true ? choiceState.selected : [string(options[optionCursor].label)];
318
- if (!selected.length) {
319
- setNotice('Select at least one option with Space or a number');
320
- return;
321
- }
322
- operate(() => answerQuestion(selected));
323
- return;
324
- }
325
- }
326
- if (help && (key.pageUp || key.pageDown)) {
327
- setHelpPage(Math.max(0, Math.min(helpPages - 1, currentHelpPage + (key.pageUp ? -1 : 1))));
328
- return;
329
- }
330
- if (key.pageUp || key.pageDown) {
331
- scrollHistory(key.pageUp ? 10 : -10);
332
- return;
333
- }
334
- if (key.escape && queueOpen) {
335
- setQueueOpen(false);
336
- return;
337
- }
338
- if (key.escape && removal) {
339
- setRemoval(undefined);
340
- return;
341
- }
342
- if (key.escape && models) {
343
- setModels(undefined);
344
- return;
345
- }
346
- if (key.escape && thoughtList) {
347
- setThoughtList(false);
348
- if (controller.running)
349
- void controller.interrupt(true);
350
- return;
351
- }
352
- if (key.escape && searchResults) {
353
- setSearchResults(undefined);
354
- if (controller.running)
355
- void controller.interrupt(true);
356
- return;
357
- }
358
- if (key.escape && historyQuery !== undefined) {
359
- setHistoryQuery(undefined);
360
- setHistoryMatches(undefined);
361
- if (controller.running)
362
- void controller.interrupt(true);
363
- return;
364
- }
365
- if (key.ctrl && _value === 'c') {
366
- // A draft clears first, exactly like a shell prompt; an empty draft still stops or exits.
367
- if (input !== '') {
368
- setInput('');
369
- return;
370
- }
371
- void controller.interrupt().then(shouldExit => { if (shouldExit)
372
- exit(); });
373
- return;
374
- }
375
- if (referenceOpen) {
376
- if (key.escape) {
377
- dismissReference(input);
378
- if (controller.running)
379
- void controller.interrupt(true);
380
- }
381
- else if (key.tab)
382
- pickReference();
383
- else if (key.upArrow)
384
- setReferenceIndex(value => Math.max(0, value - 1));
385
- else if (key.downArrow)
386
- setReferenceIndex(value => Math.max(0, Math.min((matches?.items.length ?? 1) - 1, value + 1)));
387
- return;
388
- }
389
- const recallPrevious = key.upArrow || key.ctrl && _value === 'p';
390
- const recallNext = key.downArrow || key.ctrl && _value === 'n';
391
- if ((recallPrevious || recallNext) && state.online && !controller.state.busy && !pending
392
- && !queueOpen && !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded
393
- && (state.screen === 'chat' || draft.current !== '' || key.ctrl)) {
394
- setInput(inputHistory.current.move(recallPrevious ? -1 : 1, draft.current), true);
395
- return;
396
- }
397
- if (key.tab) {
398
- completeCommand();
399
- return;
400
- }
401
- if (key.escape && (help || costExpanded || statusExpanded || notice !== undefined)) {
402
- setHelp(false);
403
- setCostExpanded(false);
404
- setStatusExpanded(false);
405
- setNotice(undefined);
406
- if (controller.running)
407
- void controller.interrupt(true);
408
- return;
409
- }
410
- if (key.escape && state.screen === 'chat') {
411
- void controller.interrupt(true);
412
- }
413
- });
414
- const submit = async (raw) => {
415
- if (referenceOpen) {
416
- pickReference();
417
- return;
418
- }
419
- if (copyMode)
420
- return;
421
- const value = raw.trim();
422
- if (!value)
423
- return;
424
- if (!pending && !/^\/feedback(?:\s|$)/.test(value))
425
- inputHistory.current.record(value);
426
- if (value === '/copy') {
427
- setInput('');
428
- setCopyMode(true);
429
- return;
430
- }
431
- setRemoval(undefined);
432
- if (value !== '/queue')
433
- setQueueOpen(false);
434
- // Each panel belongs to the command that opened it, so any other command closes it.
435
- if (!/^\/model(?: |$)/.test(value))
436
- setModels(undefined);
437
- if (value !== '/help')
438
- setHelp(false);
439
- if (value !== '/cost')
440
- setCostExpanded(false);
441
- if (value !== '/status')
442
- setStatusExpanded(false);
443
- if (!/^\/think(?: |$)/.test(value))
444
- setThoughtList(false);
445
- if (value === '/quit') {
446
- exit();
447
- return;
448
- }
449
- if (value === '/cost') {
450
- setCostExpanded(value => !value);
451
- setInput('');
452
- if (!costExpanded)
453
- void controller.perform(() => historyOperation(signal => controller.refreshCosts(signal)));
454
- return;
455
- }
456
- if (value === '/status') {
457
- setStatusExpanded(value => !value);
458
- setInput('');
459
- return;
460
- }
461
- if (value === '/help') {
462
- setHelp(value => !value);
463
- setHelpPage(0);
464
- setInput('');
465
- return;
466
- }
467
- const accepted = await controller.perform(async () => {
468
- const navigation = navigationCommand(value);
469
- if (navigation && /^--(?:delete|archive)(?:\s|$)/.test(navigation.query ?? '')) {
470
- const query = navigation.query.replace(/^--(?:delete|archive)\s*/, '');
471
- if (!query)
472
- throw new Error('Specify the name or ID to remove');
473
- await requestRemoval(navigation.kind, query);
474
- }
475
- else if (navigation) {
476
- setHistoryQuery(undefined);
477
- setSearchResults(undefined);
478
- if (navigation.kind === 'workspace')
479
- await controller.switchWorkspace(navigation.query);
480
- else
481
- await controller.switchSession(navigation.query);
482
- setScroll(0);
483
- }
484
- else if (state.screen === 'path')
485
- await controller.createWorkspace(value);
486
- else if (value === '/latest') {
487
- setHistoryWindow(undefined);
488
- setHistoryMatches(undefined);
489
- setHistoryQuery(undefined);
490
- setSearchResults(undefined);
491
- setReasoningOverrides(new Set());
492
- setScroll(0);
493
- controller.pinHistory(false);
494
- }
495
- else if (/^\/model(?: |$)/.test(value)) {
496
- if (state.screen !== 'chat')
497
- throw new Error('Select a session first');
498
- const args = value.split(/\s+/).slice(1);
499
- if (!args.length) {
500
- setHistoryQuery(undefined);
501
- setSearchResults(undefined);
502
- setModels({ catalog: await controller.modelCatalog() });
503
- }
504
- else {
505
- if (args.length < 2 || args.length > 3)
506
- throw new Error('Use /model [provider model [effort]]');
507
- await controller.selectModel(args[0], args[1], args[2]);
508
- setModels(undefined);
509
- }
510
- }
511
- else if (value === '/queue') {
512
- if (state.screen !== 'chat')
513
- throw new Error('Select a session first');
514
- if (pending)
515
- throw new Error('Answer the pending question or approval first');
516
- setQueueOpen(true);
517
- }
518
- else if (value === '/new')
519
- await controller.createSession();
520
- else if (value === '/history' || value.startsWith('/history ')) {
521
- if (state.screen !== 'chat')
522
- throw new Error('Select a session first');
523
- setSearchResults(undefined);
524
- setContentSearch(false);
525
- setHistoryQuery(value.slice(8).trim());
526
- }
527
- else if (/^\/(?:search|ssearch|wsearch)(?: |$)/.test(value)) {
528
- const [command, ...words] = value.split(' ');
529
- const query = words.join(' ').trim();
530
- if (!query)
531
- throw new Error(`Use ${command} <text>`);
532
- await historyOperation(async (signal) => {
533
- if (command === '/search') {
534
- setHistoryMatches(await controller.searchHistory(query, signal));
535
- setSearchResults(undefined);
536
- setContentSearch(true);
537
- setHistoryQuery(query);
538
- }
539
- else {
540
- const result = await controller.searchSessions(query, command === '/ssearch', signal);
541
- setHistoryQuery(undefined);
542
- setSearchResults({ query, ...result });
543
- }
544
- });
545
- }
546
- else if (/^\/think(?: |$)/.test(value)) {
547
- if (state.screen !== 'chat')
548
- throw new Error('Select a session first');
549
- const target = value.slice(6).trim();
550
- if (target === 'live') {
551
- setLiveReasoning(value => value === 'row' ? 'full' : 'row');
552
- setThoughtList(false);
553
- setScroll(0);
554
- }
555
- else if (target) {
556
- const seq = Number(target);
557
- if (!Number.isSafeInteger(seq) || !displayTranscript.thoughts.some(entry => entry.seq === seq))
558
- throw new Error('Use /think <message sequence> for a loaded reasoning block');
559
- const next = new Set(reasoningOverrides);
560
- if (next.delete(seq)) {
561
- setReasoningOverrides(next);
562
- return;
563
- }
564
- next.add(seq);
565
- setReasoningOverrides(next);
566
- await jumpHistory(seq, next);
567
- }
568
- else {
569
- setHistoryQuery(undefined);
570
- setSearchResults(undefined);
571
- setThoughtList(true);
572
- }
573
- }
574
- else if (value === '/older') {
575
- await controller.older(undefined, displayTranscript);
576
- setScroll(value => value + 10);
577
- }
578
- else if (/^\/compact(?: |$)/.test(value)) {
579
- if (state.screen !== 'chat')
580
- throw new Error('Select a session first');
581
- if (value !== '/compact')
582
- throw new Error('Use /compact (no arguments)');
583
- setNotice(undefined);
584
- await historyOperation(async (signal) => { setNotice(await controller.command('/compact', signal)); }, 'Compacting history…');
585
- }
586
- else if (value === '/cancel')
587
- await controller.cancelTurn();
588
- else if (value === '/allow')
589
- await controller.approve(true);
590
- else if (value === '/deny')
591
- await controller.approve(false);
592
- else if (/^\/(?:plan|goal|permission|feedback)(?:\s|$)/.test(value)) {
593
- if (state.screen !== 'chat')
594
- throw new Error('Select a session first');
595
- if (pending)
596
- throw new Error('Answer the pending question or approval first');
597
- setNotice(undefined);
598
- await historyOperation(async (signal) => { setNotice(await controller.command(value, signal)); }, 'Running command…');
599
- }
600
- else if (/^\/export(?:\s|$)/.test(value)) {
601
- if (state.screen !== 'chat')
602
- throw new Error('Select a session first');
603
- const destination = value.slice(7).trim().replace(/^(["'])(.*)\1$/, '$2');
604
- await historyOperation(async (signal) => { setNotice(`Saved session log: ${await controller.exportLog(destination || undefined, signal)}`); }, 'Exporting session log…');
605
- }
606
- else if (question)
607
- await answerQuestion(question.multiSelect === true ? choiceState.selected : [], value);
608
- else if (pending)
609
- throw new Error('Answer the approval with /allow or /deny');
610
- else if (value.startsWith('/'))
611
- throw new Error('Unknown command. Use /help.');
612
- else if (state.screen !== 'chat')
613
- throw new Error('Choose a session or type /ws or /resume');
614
- else {
615
- await controller.prompt(value);
616
- setHistoryWindow(undefined);
617
- setScroll(0);
618
- }
619
- });
620
- if (accepted)
621
- setInput('');
622
- };
623
- const choices = state.screen === 'workspaces' ? [
624
- ...state.workspaces.map(workspace => ({ key: string(workspace.workspaceId),
625
- label: `${string(workspace.title)} ${string(workspace.path)}`,
626
- remove: () => setRemoval({ kind: 'workspace', id: string(workspace.workspaceId), name: string(workspace.title), path: string(workspace.path) }),
627
- action: () => controller.pickWorkspace(string(workspace.workspaceId)) })),
628
- { key: '@all', label: 'All sessions', action: () => operate(() => controller.switchSession('all')) },
629
- { key: '@new', label: '+ Add workspace (host directory)', action: () => controller.enterPath() },
630
- ] : [
631
- ...(state.workspaceId && !state.showAllSessions ? [{ key: '@new', label: '+ New session', action: () => operate(() => controller.createSession()) }] : []),
632
- ...controller.visibleSessions.map(session => ({ key: string(session.sessionId),
633
- label: `${session.running ? '● ' : ''}${sessionLabel(session)} ${session.sessionId}`,
634
- remove: () => operate(() => requestRemoval('session', string(session.sessionId))),
635
- action: () => { setScroll(0); operate(() => controller.selectSession(string(session.sessionId))); } })),
636
- { key: '@back', label: '← Workspaces', action: () => operate(() => controller.showPicker('workspaces')) },
637
- ];
638
- const width = Math.max(10, (stdout.columns ?? 80) - 2);
639
- const layout = useMemo(() => historyLayout(displayTranscript, width, reasoning, reasoningOverrides, liveReasoning), [displayTranscript, displayTranscript.version, width, reasoning, reasoningOverrides, liveReasoning]);
640
- const { length, first } = layout;
641
- const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
642
- const showHistoryHint = dialogOpen || displayTranscript.hasMore || !!historyWindow;
643
- const pageSize = Math.max(1, conversationRows - (showHistoryHint ? 1 : 0));
644
- const previousView = useRef({ transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning });
645
- const previous = previousView.current;
646
- const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
647
- const adjustedScroll = previous.session !== state.transcript ? 0
648
- : scroll > 0 && previous.transcript === displayTranscript && !prepended && previous.folds === reasoningOverrides && previous.liveReasoning === liveReasoning ? Math.max(0, scroll + length - previous.count) : scroll;
649
- const maxScroll = Math.max(0, length - pageSize);
650
- const position = Math.min(adjustedScroll, maxScroll);
651
- useLayoutEffect(() => {
652
- previousView.current = { transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning };
653
- if (position !== scroll)
654
- setScroll(position);
655
- }, [state.transcript, displayTranscript, length, position, scroll, reasoningOverrides, liveReasoning]);
656
- useLayoutEffect(() => {
657
- controller.pinHistory(!historyWindow && (position > 0 || thoughtList || historyQuery !== undefined && !contentSearch));
658
- }, [controller, historyWindow, position, thoughtList, historyQuery, contentSearch, state.transcript]);
659
- useEffect(() => {
660
- const first = displayTranscript.beforeSeq;
661
- if (first !== undefined)
662
- setReasoningOverrides(previous => {
663
- const next = new Set([...previous].filter(seq => seq >= first));
664
- return next.size === previous.size ? previous : next;
665
- });
666
- }, [displayTranscript, displayTranscript.memoryRevision]);
667
- const scrollPosition = useRef(position);
668
- scrollPosition.current = position;
669
- const loadingPage = useRef(false);
670
- const scrollIntent = useRef(0);
671
- const mounted = useRef(true);
672
- useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
673
- function scrollHistory(delta) {
674
- if (copyMode || state.screen !== 'chat')
675
- return;
676
- const intent = ++scrollIntent.current;
677
- const next = Math.max(0, Math.min(maxScroll, scrollPosition.current + delta));
678
- scrollPosition.current = next;
679
- setScroll(next);
680
- if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !displayTranscript.ready || !displayTranscript.hasMore)
681
- return;
682
- loadingPage.current = true;
683
- const transcript = displayTranscript;
684
- void controller.perform(() => historyOperation(signal => controller.older(signal, transcript))).then(accepted => {
685
- if (accepted && mounted.current && displayRef.current === transcript && scrollIntent.current === intent) {
686
- setScroll(value => value + delta);
687
- }
688
- }).finally(() => { loadingPage.current = false; });
689
- }
690
- async function historyOperation(operation, label = 'Loading history…') {
691
- const abort = new AbortController();
692
- historyAbort.current = abort;
693
- setHistoryLoading(label);
694
- try {
695
- await operation(abort.signal);
696
- }
697
- finally {
698
- if (historyAbort.current === abort) {
699
- historyAbort.current = undefined;
700
- setHistoryLoading(undefined);
701
- }
702
- }
703
- }
704
- async function openSearchSession(sessionId, query) {
705
- await historyOperation(async (signal) => {
706
- await controller.selectSession(sessionId);
707
- await controller.waitForHistory(signal);
708
- setHistoryMatches(await controller.searchHistory(query, signal));
709
- setSearchResults(undefined);
710
- setContentSearch(true);
711
- setHistoryQuery(query);
712
- });
713
- }
714
- async function jumpHistory(target, folds = reasoningOverrides) {
715
- if (state.screen !== 'chat')
716
- throw new Error('Select a session first');
717
- ++scrollIntent.current;
718
- const abort = new AbortController();
719
- historyAbort.current = abort;
720
- try {
721
- const transcript = displayTranscript.messages.some(message => message.seq === target) ? displayTranscript
722
- : state.transcript.messages.some(message => message.seq === target) ? state.transcript
723
- : await controller.historyAt(target, abort.signal);
724
- abort.signal.throwIfAborted();
725
- const current = historyLayout(transcript, width, reasoning, folds, liveReasoning);
726
- const row = current.offsets.get(target);
727
- if (row === undefined)
728
- throw new Error('No visible message at this sequence; use /history to choose a record');
729
- setHistoryWindow(transcript === state.transcript ? undefined : transcript);
730
- setHistoryQuery(undefined);
731
- setHistoryMatches(undefined);
732
- setThoughtList(false);
733
- setScroll(Math.max(0, current.length - pageSize - row));
734
- }
735
- finally {
736
- if (historyAbort.current === abort)
737
- historyAbort.current = undefined;
738
- }
739
- }
740
- useMouseWheel(direction => scrollHistory(direction * 3), !copyMode && state.screen === 'chat', () => { if (!dialogOpen)
741
- setCopyMode(true); });
742
- const trailingGap = dialogOpen && length > 0 && layout.viewport(length - 1, length)[0]?.text === '' ? 1 : 0;
743
- const end = Math.max(pageSize, length - position - trailingGap);
744
- const visible = useMemo(() => layout.viewport(Math.max(0, end - pageSize), end), [layout, end, pageSize]);
745
- const liveThought = thoughtList && !historyWindow ? state.transcript.liveParts(width).find(part => part.kind === 'reasoning') : undefined;
746
- const thoughtEntries = thoughtList ? displayTranscript.thoughts : undefined;
747
- const thoughtChoices = useMemo(() => [...(thoughtEntries ?? [])].reverse().map(entry => ({
748
- key: String(entry.seq), label: `${toolLine(`#${entry.seq} User · ${entry.prompt}`, width - 2)}\n ${toolLine(`◇ ${entry.preview}`, width - 4)}`,
749
- action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq); setReasoningOverrides(next); await jumpHistory(entry.seq, next); }),
750
- })), [thoughtEntries, width, pageSize, reasoningOverrides, liveReasoning, displayTranscript]);
751
- const thoughtOptions = useMemo(() => [
752
- ...(liveThought ? [{ key: 'live', label: `${toolLine(`Now · User · ${state.transcript.latestPrompt}`, width - 2)}\n ${toolLine(liveThought.text, width - 4)}`,
753
- action: () => { setLiveReasoning('full'); setThoughtList(false); const expanded = historyLayout(state.transcript, width, reasoning, reasoningOverrides, 'full'); setScroll(Math.max(0, expanded.length - pageSize - expanded.liveOffset)); } }] : []),
754
- ...thoughtChoices,
755
- ...(displayTranscript.hasMore ? [{ key: 'older', label: '↑ Load older reasoning', action: () => operate(() => historyOperation(signal => controller.older(signal, displayTranscript))) }] : []),
756
- { key: 'close', label: '← Back to conversation', action: () => setThoughtList(false) },
757
- ], [liveThought, thoughtChoices, displayTranscript, displayTranscript.hasMore, width, pageSize, reasoningOverrides]);
758
- const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
759
- const workspaceName = workspace ? string(workspace.title) || string(workspace.path) : state.workspaceId;
760
- const headerTitle = controller.sessionName
761
- ? [controller.sessionName, width >= 60 ? workspaceName : undefined].filter(Boolean).join(' · ')
762
- : workspaceName || 'All workspaces';
763
- return _jsx(ThemeContext.Provider, { value: theme, children: _jsx(CopyMode.Provider, { value: copyMode, children: _jsx(Frozen, { frozen: copyMode, identity: `${width}:${stdout.rows}`, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, height: Math.max(1, (stdout.rows ?? 30) - 1), overflowY: "hidden", children: [copyMode && _jsx(Text, { color: theme.accent, children: "Copy mode \u00B7 drag to select \u00B7 Esc / Ctrl+S resumes" }), _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}`, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { width: width, children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: toolLine(headerTitle, width) }) }), width >= 60 && controller.sessionMode && _jsx(Box, { flexShrink: 0, marginLeft: 2, children: _jsx(Text, { color: theme.accent, children: toolLine(controller.sessionMode, Math.min(24, Math.floor(width / 3))) }) })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(width) })] }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [historyLoading && _jsxs(Text, { dimColor: true, children: [historyLoading, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: displayPaused, identity: state.sessionId ?? "", children: statusNotice && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(state.status) }) }), state.error && _jsx(Text, { color: theme.colors.error, children: state.error }), state.screen === 'chat' && _jsxs(Box, { ref: conversationBox, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", marginY: dialogOpen ? 0 : 1, children: [visible.length ? _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, children: _jsx(HistoryViewport, { rows: visible }) }) : _jsx(Text, { color: theme.colors.muted, children: "Start a conversation with the host agent." }), showHistoryHint && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: dialogOpen ? 'Wheel/PgUp/PgDn · Scroll history' : historyWindow ? 'Earlier history · /latest returns to live conversation' : 'Scroll up or /older to load earlier history' })] })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: safeText(notice) }), _jsxs(Box, { borderStyle: "round", borderColor: pending ? theme.colors.context : state.online ? theme.accent : theme.border, paddingX: 1, flexDirection: "column", flexShrink: 1, minHeight: 3, children: [_jsx(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: queueOpen && !pending ? _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pending input \u00B7 Esc close" }), !queued.length && _jsx(Text, { dimColor: true, children: state.controlError ? 'Host queue unavailable' : 'No pending input' }), _jsx(Picker, { choices: queued.map(item => ({
764
- key: item.id, label: toolLine(item.text, width - 6),
765
- action: () => operate(() => controller.removeQueued(item.id)),
766
- remove: () => operate(() => controller.removeQueued(item.id)),
767
- })), pageSize: Math.max(1, Math.min(6, (stdout.rows ?? 30) - 12)), hint: "\u2191 \u2193 select \u00B7 Enter / d / Delete remove \u00B7 Esc close", enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online && !controller.state.pending.length })] }) : removal ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.context, children: removal.kind === 'workspace' ? 'Remove workspace registration?' : 'Archive session?' }), _jsx(Text, { wrap: "truncate-end", children: safeText(removal.name) }), _jsxs(Text, { wrap: "truncate-end", children: ["ID: ", safeText(removal.id)] }), removal.path && _jsxs(Text, { wrap: "truncate-end", children: ["Path: ", safeText(removal.path)] }), _jsx(Text, { children: removal.kind === 'workspace' ? 'Removes the workspace from the list. Directory and sessions are kept.' : 'Hides the session from lists. History is kept; /resume ID can reopen it.' }), _jsx(Text, { dimColor: true, children: "Running tasks continue. Esc cancels this dialog." }), _jsx(Picker, { choices: [
768
- { key: 'cancel', label: 'Cancel', action: () => setRemoval(undefined) },
769
- { key: 'confirm', label: removal.kind === 'workspace' ? 'Remove workspace' : 'Archive session',
770
- action: () => operate(async () => { await controller.removeTarget(removal); setRemoval(undefined); }) },
771
- ], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, `${removal.kind}:${removal.id}`)] }) : models ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: models.model ? 'Choose reasoning effort' : 'Choose model' }), _jsx(Text, { dimColor: true, children: "Applies to subsequent requests; host also saves the default." }), array(models.catalog.failures).map(object).map(failure => _jsx(Text, { color: theme.colors.error, children: safeText(`${failure.name}: ${failure.message}`) }, string(failure.id))), _jsx(Picker, { pageSize: Math.max(1, Math.min(8, (stdout.rows ?? 30) - 15)), choices: models.model ? [
772
- { key: 'default', label: `Default effort${object(models.model.reasoning).defaultEffort ? ` · ${string(object(models.model.reasoning).defaultEffort)}` : ''}`,
773
- action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id)); setModels(undefined); }) },
774
- ...array(object(models.model.reasoning).efforts).map(object).map(effort => ({ key: string(effort.id), label: toolLine(`${effort.name} (${effort.id})${effort.description ? ` · ${effort.description}` : ''}`, width - 2),
775
- action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id), string(effort.id)); setModels(undefined); }) })),
776
- { key: 'back', label: '← Models', action: () => setModels({ catalog: models.catalog }) },
777
- ] : [
778
- ...array(models.catalog.groups).map(object).flatMap(group => array(group.models).map(object).map(model => ({
779
- key: `${group.id}:${model.id}`, label: toolLine(`${group.name} · ${model.name} (${model.id})`, width - 2),
780
- action: () => {
781
- if (array(object(model.reasoning ?? { efforts: [] }).efforts).length)
782
- setModels({ catalog: models.catalog, provider: string(group.id), model });
783
- else
784
- operate(async () => { await controller.selectModel(string(group.id), string(model.id)); setModels(undefined); });
785
- },
786
- }))),
787
- { key: 'close', label: '← Back to conversation', action: () => setModels(undefined) },
788
- ], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, models.model ? `${models.provider}:${models.model.id}` : 'models')] }) : searchResults ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Session search \u00B7 ", safeText(searchResults.query)] }), searchResults.hasMore && _jsx(Text, { color: theme.colors.context, children: "Host returned only the first 20 global matches; workspace results may be incomplete. Refine your query." }), !searchResults.items.length && _jsx(Text, { children: "No sessions in the returned results" }), _jsx(Picker, { choices: [...searchResults.items.map(item => ({ key: string(item.sessionId),
789
- label: toolLine(`${item.sessionId} · ${item.snippet}`, width - 2),
790
- action: () => operate(() => openSearchSession(string(item.sessionId), searchResults.query)),
791
- })), { key: 'close', label: '← Back', action: () => setSearchResults(undefined) }], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy })] }) : state.screen === 'workspaces' || state.screen === 'sessions' ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: state.screen === 'workspaces' ? 'Choose workspace' : state.showAllSessions ? 'Choose session · All workspaces' : 'Choose session' }), _jsx(Picker, { choices: choices, enabled: state.online && !state.busy && !input, canSelect: () => !draft.current && controller.state.online && !controller.state.busy }, `${state.screen}:${state.workspaceId ?? ''}`)] }) : _jsxs(_Fragment, { children: [thoughtList && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.reasoning, children: "Reasoning history \u00B7 User prompts \u00B7 Esc close" }), !thoughtEntries?.length && !liveThought && _jsx(Text, { dimColor: true, children: "No reasoning in loaded history" }), _jsx(Picker, { choices: thoughtOptions, pageSize: Math.max(1, Math.min(6, Math.floor(((stdout.rows ?? 30) - 14) / 2))), enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `thoughts:${state.sessionId}`)] }), state.screen === 'chat' && historyQuery !== undefined && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: [contentSearch ? 'Search · session history' : 'History · loaded records', " \u00B7 Esc close"] }), contentSearch && historyMatches?.truncated && _jsx(Text, { color: theme.colors.context, children: "Showing the first 200 matches \u00B7 Refine your search" }), _jsx(Picker, { choices: [
792
- ...(contentSearch && historyMatches ? historyMatches.items.map(message => ({
793
- key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.preview}`, width - 2),
794
- action: () => operate(() => jumpHistory(message.seq)),
795
- })) : layout.messages.filter(message => message.role === 'You' && `${message.seq} ${message.text}`.toLowerCase().includes(historyQuery.toLowerCase())).map(message => ({
796
- key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.text}`, width - 2),
797
- action: () => operate(() => jumpHistory(message.seq)),
798
- }))),
799
- { key: 'close', label: '← Back to conversation', action: () => { setHistoryQuery(undefined); setHistoryMatches(undefined); } },
800
- ], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `history:${historyQuery}`)] }), pending && _jsxs(Box, { flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, color: theme.colors.context, children: question ? `Question ${answered.length + 1}/${questions.length}${question.header ? ` · ${safeText(string(question.header))}` : ''}` : 'Approval required' }), _jsx(Text, { children: safeText(question ? string(question.question) : JSON.stringify(pending.request, null, 2)) }), question?.detail && _jsx(Text, { children: safeText(string(question.detail)) }), options.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [[...options, { label: 'Other answer — type below' }].map((option, index) => ({ option, index }))
801
- .slice(optionStart, optionStart + optionPageSize).map(({ option, index }) => _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { color: index === optionCursor ? theme.accent : undefined, wrap: "truncate-end", children: [index === optionCursor ? '❯ ' : ' ', index + 1, ". ", question?.multiSelect === true && index < options.length ? choiceState.selected.includes(string(option.label)) ? '[x] ' : '[ ] ' : '', safeText(string(option.label))] }), option.description && _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [' ', safeText(string(option.description))] })] }, index)), _jsx(Text, { dimColor: true, children: choiceState.custom ? 'Type your answer below · Esc returns to options' : question?.multiSelect === true
802
- ? '↑ ↓ move · Space / 1–9 toggle · Enter confirm' : '↑ ↓ / 1–9 select · Enter confirm' })] }), _jsx(Text, { dimColor: true, children: question ? 'Text answers supported · Ctrl+C clears · /cancel stops' : '/allow approves once · /deny rejects · /cancel stops' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { dimColor: true, children: ["Waiting: ", queued.length, " \u00B7 /queue to remove"] }), queued.slice(0, 2).map(item => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [item.placement === 'steering' ? '↳ ' : '· ', safeText(toolLine(item.text, width - 6))] }, item.id))] }), _jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: theme.accent, children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(draft.current); }, reservedKeys: queueOpen && !pending ? ['d'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : undefined, focus: state.online && !state.busy && !copyMode, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' })] }), referenceOpen && _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Host files \u00B7 \u2191 \u2193 select \u00B7 Tab/Enter insert \u00B7 Esc close" }), !matches ? _jsx(Text, { dimColor: true, children: "Searching\u2026" }) : matches.error ? _jsx(Text, { color: theme.colors.error, children: matches.error })
803
- : matches.items.length === 0 ? _jsx(Text, { dimColor: true, children: "No matching host files" })
804
- : matches.items.slice(Math.max(0, referenceIndex - 5), Math.max(0, referenceIndex - 5) + 6).map((item, index) => _jsxs(Text, { color: index + Math.max(0, referenceIndex - 5) === referenceIndex ? theme.accent : undefined, children: [index + Math.max(0, referenceIndex - 5) === referenceIndex ? '❯ ' : ' ', item.path, item.kind === 'directory' ? '/' : ''] }, `${item.kind}:${item.path}`))] })] }), input.startsWith('/') && !input.includes(' ') && _jsx(Text, { dimColor: true, children: COMMANDS.filter(command => command.startsWith(input)).join(' ') }), help && _jsxs(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [COMMAND_HINTS.slice(currentHelpPage * helpPageSize, (currentHelpPage + 1) * helpPageSize).map((hint, index) => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: theme.accent, children: COMMAND_LABELS[currentHelpPage * helpPageSize + index].padEnd(COMMAND_LABEL_WIDTH) }), hint.description] }, hint.command)), _jsxs(Text, { dimColor: true, children: ["Help ", currentHelpPage + 1, "/", helpPages, " \u00B7 PgUp/PgDn pages \u00B7 Esc close"] }), _jsx(Text, { dimColor: true, children: "Enter send \u00B7 Tab complete \u00B7 Esc cancel \u00B7 Wheel/PgUp/PgDn scroll \u00B7 Ctrl+C clear / stop / exit" }), _jsx(Text, { dimColor: true, children: "History: \u2191/\u2193 or Ctrl+P/N recall \u00B7 Editing: Ctrl+A/E start/end \u00B7 Ctrl+K/U kill right/left \u00B7 Ctrl+W kill word \u00B7 Ctrl+Y restore" })] }), costExpanded && _jsx(CostPanel, { controller: controller }), _jsx(Frozen, { frozen: displayPaused || position > 0, identity: `${width}:${state.sessionId}:${statusExpanded}`, children: _jsx(StatusBar, { controller: controller, width: width, expanded: statusExpanded, revision: state.version, paused: displayPaused || position > 0 }) })] })] }) }) }) });
805
- }