@itookit/dsht 0.2.4 → 0.3.2

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