@itookit/dsht 0.2.1 → 0.2.4

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.
package/dist/app.js CHANGED
@@ -1,47 +1,148 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Ink terminal interface: startup pickers, transcript, and slash-command composer. */
3
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
4
- import { Box, Text, useApp, useInput, useStdout } from 'ink';
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';
5
7
  import { useMouseWheel } from "./mouse.js";
6
8
  import { TextInput } from "./input.js";
7
- import { historyLayout, jumpTarget } from "./history.js";
9
+ import { InputHistory } from "./input-history.js";
10
+ import { historyLayout, releaseHistoryLayout } from "./history.js";
8
11
  import { toolLine } from "./transcript.js";
9
12
  import { activeReference, fileMention } from "./references.js";
10
13
  import { CostPanel } from "./cost-view.js";
11
14
  import { StatusBar } from "./status.js";
12
15
  import { navigationCommand, sessionLabel } from "./navigation.js";
13
16
  import { array, errorText, object, safeText, string } from "./wire.js";
14
- const COMMANDS = ['/ws', '/s', '/new', '/older', '/history', '/jump', '/search', '/ssearch', '/wsearch', '/cancel', '/steer', '/allow', '/deny', '/status', '/cost', '/help', '/quit'];
15
- const HELP = '/ws [name or ID] · /s [title or ID] · /s all · /new · /older · /history [text] · /jump <seq|first|last> · /search text · /ssearch text · /wsearch text · /cancel · /steer text · /allow · /deny · /status · /cost · /quit';
16
- /** A slash-command panel closes on the next command or after this long, whichever comes first. */
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. */
17
57
  const PANEL_LIFETIME_MS = 10_000;
18
- function Picker({ choices, enabled, canSelect }) {
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);
19
66
  const [selected, setSelected] = useState(0);
20
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]);
21
75
  useInput((_input, key) => {
22
- if (!canSelect())
76
+ if (!canSelect() || key.eventType === 'release')
23
77
  return;
24
78
  if (key.upArrow)
25
79
  setSelected(Math.max(0, current - 1));
26
80
  else if (key.downArrow)
27
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?.();
28
84
  else if (key.return)
29
85
  choices[current]?.action();
30
- }, { isActive: enabled });
31
- const start = Math.max(0, current - 7);
32
- return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start + 12).map((choice, index) => _jsxs(Text, { color: start + index === current ? 'cyan' : undefined, children: [start + index === current ? '❯ ' : ' ', safeText(choice.label)] }, choice.key)), _jsx(Text, { dimColor: true, children: "\u2191 \u2193 select \u00B7 Enter open \u00B7 Ctrl+C stop / exit" })] });
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"] })] });
33
89
  }
34
90
  /** The caller owns starting and stopping the controller around the Ink render lifetime. */
35
- export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
91
+ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = mocha }) {
36
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]);
37
96
  const { exit } = useApp();
38
97
  const { stdout } = useStdout();
98
+ const [copyMode, setCopyMode] = useState(false);
39
99
  const [input, updateInput] = useState('');
40
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]);
41
113
  const [cursor, setCursor] = useState(0);
42
114
  // Input callbacks may run before Ink refreshes the controlled field's listener.
43
- const setInput = (value) => { draft.current = value; updateInput(value); setCursor(value.length); };
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
+ });
44
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);
45
146
  const [historyQuery, setHistoryQuery] = useState();
46
147
  const [contentSearch, setContentSearch] = useState(false);
47
148
  const [searchResults, setSearchResults] = useState();
@@ -49,23 +150,33 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
49
150
  useEffect(() => () => historyAbort.current?.abort(), []);
50
151
  const [costExpanded, setCostExpanded] = useState(false);
51
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();
52
158
  const [help, setHelp] = useState(false);
53
159
  const [answers, setAnswers] = useState({});
160
+ const [optionState, setOptionState] = useState();
54
161
  const [referenceIndex, setReferenceIndex] = useState(0);
55
162
  const [dismissedReference, dismissReference] = useState();
56
163
  const [lookup, setLookup] = useState();
57
- // A slash-command panel is temporary: the next command or its lifetime closes it.
164
+ // Interactive panels remain stable while read; only transient notices expire.
58
165
  useEffect(() => {
59
- if (!help && !costExpanded && !statusExpanded)
166
+ if (copyMode)
167
+ return;
168
+ if (notice === undefined)
60
169
  return;
61
- const timer = setTimeout(() => { setHelp(false); setCostExpanded(false); setStatusExpanded(false); }, panelLifetimeMs);
170
+ const timer = setTimeout(() => setNotice(undefined), panelLifetimeMs);
62
171
  return () => clearTimeout(timer);
63
- }, [help, costExpanded, statusExpanded, panelLifetimeMs]);
172
+ }, [notice, panelLifetimeMs, copyMode]);
64
173
  const pending = state.pending[0];
65
174
  const token = state.screen === 'chat' && state.online && !state.busy && !pending
66
175
  && (!input.startsWith('/') || input.startsWith('/steer ')) && dismissedReference !== input && cursor === input.length
67
176
  ? activeReference(input) : undefined;
68
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;
69
180
  const matches = referenceOpen && lookup?.draft === input && lookup.sessionId === state.sessionId ? lookup : undefined;
70
181
  useEffect(() => {
71
182
  if (!referenceOpen)
@@ -89,16 +200,130 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
89
200
  const mention = fileMention(candidate, token.quoted);
90
201
  setInput(input.slice(0, -token.prefix.length) + mention + (candidate.kind === 'file' ? ' ' : ''));
91
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
+ };
92
217
  const questions = pending?.event === 'user-questions/request' ? array(object(pending.request).questions).map(object) : [];
93
218
  const eventId = pending ? string(pending.eventId) : '';
94
219
  const answered = answers[eventId] ?? [];
95
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
+ };
96
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
+ };
97
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
+ }
98
263
  if ((key.escape || key.ctrl && _value === 'c') && historyAbort.current) {
99
264
  historyAbort.current.abort();
100
265
  return;
101
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
+ }
102
327
  if (key.escape && searchResults) {
103
328
  setSearchResults(undefined);
104
329
  if (controller.running)
@@ -107,11 +332,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
107
332
  }
108
333
  if (key.escape && historyQuery !== undefined) {
109
334
  setHistoryQuery(undefined);
335
+ setHistoryMatches(undefined);
110
336
  if (controller.running)
111
337
  void controller.interrupt(true);
112
338
  return;
113
339
  }
114
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
+ }
115
346
  void controller.interrupt().then(shouldExit => { if (shouldExit)
116
347
  exit(); });
117
348
  return;
@@ -130,6 +361,27 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
130
361
  setReferenceIndex(value => Math.max(0, Math.min((matches?.items.length ?? 1) - 1, value + 1)));
131
362
  return;
132
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
+ }
133
385
  if (key.escape && state.screen === 'chat') {
134
386
  void controller.interrupt(true);
135
387
  }
@@ -143,16 +395,30 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
143
395
  pickReference();
144
396
  return;
145
397
  }
398
+ if (copyMode)
399
+ return;
146
400
  const value = raw.trim();
147
401
  if (!value)
148
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);
149
411
  // Each panel belongs to the command that opened it, so any other command closes it.
412
+ if (!/^\/model(?: |$)/.test(value))
413
+ setModels(undefined);
150
414
  if (value !== '/help')
151
415
  setHelp(false);
152
416
  if (value !== '/cost')
153
417
  setCostExpanded(false);
154
418
  if (value !== '/status')
155
419
  setStatusExpanded(false);
420
+ if (!/^\/think(?: |$)/.test(value))
421
+ setThoughtList(false);
156
422
  if (value === '/quit') {
157
423
  exit();
158
424
  return;
@@ -176,7 +442,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
176
442
  }
177
443
  const accepted = await controller.perform(async () => {
178
444
  const navigation = navigationCommand(value);
179
- if (navigation) {
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) {
180
452
  setHistoryQuery(undefined);
181
453
  setSearchResults(undefined);
182
454
  if (navigation.kind === 'workspace')
@@ -187,6 +459,31 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
187
459
  }
188
460
  else if (state.screen === 'path')
189
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
+ }
190
487
  else if (value === '/new')
191
488
  await controller.createSession();
192
489
  else if (value === '/history' || value.startsWith('/history ')) {
@@ -203,7 +500,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
203
500
  throw new Error(`Use ${command} <text>`);
204
501
  await historyOperation(async (signal) => {
205
502
  if (command === '/search') {
206
- await controller.historyThrough('first', signal);
503
+ setHistoryMatches(await controller.searchHistory(query, signal));
207
504
  setSearchResults(undefined);
208
505
  setContentSearch(true);
209
506
  setHistoryQuery(query);
@@ -215,10 +512,36 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
215
512
  }
216
513
  });
217
514
  }
218
- else if (value === '/jump' || value.startsWith('/jump '))
219
- await jumpHistory(jumpTarget(value.slice(5).trim()));
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
+ }
220
543
  else if (value === '/older') {
221
- await controller.older();
544
+ await controller.older(undefined, displayTranscript);
222
545
  setScroll(value => value + 10);
223
546
  }
224
547
  else if (value === '/cancel')
@@ -229,21 +552,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
229
552
  await controller.approve(false);
230
553
  else if (value.startsWith('/steer '))
231
554
  await controller.prompt(value.slice(7), 'steer');
232
- else if (question) {
233
- const answer = { id: string(question.id), selected: [], custom: value };
234
- const next = [...answered, answer];
235
- if (next.length === questions.length)
236
- await controller.answer({ answers: next });
237
- setAnswers(previous => ({ ...previous, [eventId]: next }));
238
- }
555
+ else if (question)
556
+ await answerQuestion(question.multiSelect === true ? choiceState.selected : [], value);
239
557
  else if (pending)
240
558
  throw new Error('Answer the approval with /allow or /deny');
241
559
  else if (value.startsWith('/'))
242
560
  throw new Error('Unknown command. Use /help.');
243
561
  else if (state.screen !== 'chat')
244
- throw new Error('Choose a session or type /ws or /s');
562
+ throw new Error('Choose a session or type /ws or /resume');
245
563
  else {
246
564
  await controller.prompt(value);
565
+ setHistoryWindow(undefined);
247
566
  setScroll(0);
248
567
  }
249
568
  });
@@ -253,6 +572,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
253
572
  const choices = state.screen === 'workspaces' ? [
254
573
  ...state.workspaces.map(workspace => ({ key: string(workspace.workspaceId),
255
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) }),
256
576
  action: () => controller.pickWorkspace(string(workspace.workspaceId)) })),
257
577
  { key: '@all', label: 'All sessions', action: () => operate(() => controller.switchSession('all')) },
258
578
  { key: '@new', label: '+ Add workspace (host directory)', action: () => controller.enterPath() },
@@ -260,25 +580,38 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
260
580
  ...(state.workspaceId && !state.showAllSessions ? [{ key: '@new', label: '+ New session', action: () => operate(() => controller.createSession()) }] : []),
261
581
  ...controller.visibleSessions.map(session => ({ key: string(session.sessionId),
262
582
  label: `${session.running ? '● ' : ''}${sessionLabel(session)} ${session.sessionId}`,
583
+ remove: () => operate(() => requestRemoval('session', string(session.sessionId))),
263
584
  action: () => { setScroll(0); operate(() => controller.selectSession(string(session.sessionId))); } })),
264
585
  { key: '@back', label: '← Workspaces', action: () => operate(() => controller.showPicker('workspaces')) },
265
586
  ];
266
587
  const width = Math.max(10, (stdout.columns ?? 80) - 2);
267
- const layout = useMemo(() => historyLayout(state.transcript, width), [state.transcript, state.transcript.version, width]);
268
- const { lines, first } = layout;
269
- const pageSize = Math.max(5, (stdout.rows ?? 30) - (pending ? 17 : 12) - (statusExpanded ? 9 : 0));
270
- const previousView = useRef({ transcript: state.transcript, count: lines.length, first });
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 });
271
593
  const previous = previousView.current;
272
594
  const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
273
- const adjustedScroll = previous.transcript !== state.transcript ? 0
274
- : scroll > 0 && !prepended ? Math.max(0, scroll + lines.length - previous.count) : scroll;
275
- const maxScroll = Math.max(0, lines.length - pageSize);
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);
276
598
  const position = Math.min(adjustedScroll, maxScroll);
277
599
  useLayoutEffect(() => {
278
- previousView.current = { transcript: state.transcript, count: lines.length, first };
600
+ previousView.current = { transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning };
279
601
  if (position !== scroll)
280
602
  setScroll(position);
281
- }, [state.transcript, lines, position, scroll]);
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]);
282
615
  const scrollPosition = useRef(position);
283
616
  scrollPosition.current = position;
284
617
  const loadingPage = useRef(false);
@@ -286,18 +619,18 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
286
619
  const mounted = useRef(true);
287
620
  useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
288
621
  function scrollHistory(delta) {
289
- if (state.screen !== 'chat' || historyQuery !== undefined || searchResults !== undefined)
622
+ if (displayPaused || state.screen !== 'chat' || historyQuery !== undefined || searchResults !== undefined || thoughtList)
290
623
  return;
291
624
  const intent = ++scrollIntent.current;
292
625
  const next = Math.max(0, Math.min(maxScroll, scrollPosition.current + delta));
293
626
  scrollPosition.current = next;
294
627
  setScroll(next);
295
- if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !state.transcript.ready || !state.transcript.hasMore)
628
+ if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !displayTranscript.ready || !displayTranscript.hasMore)
296
629
  return;
297
630
  loadingPage.current = true;
298
- const transcript = state.transcript;
299
- void controller.perform(() => historyOperation(signal => controller.older(signal))).then(accepted => {
300
- if (accepted && mounted.current && controller.state.transcript === transcript && scrollIntent.current === intent) {
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) {
301
634
  setScroll(value => value + delta);
302
635
  }
303
636
  }).finally(() => { loadingPage.current = false; });
@@ -305,65 +638,109 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
305
638
  async function historyOperation(operation) {
306
639
  const abort = new AbortController();
307
640
  historyAbort.current = abort;
641
+ setHistoryLoading(true);
308
642
  try {
309
643
  await operation(abort.signal);
310
644
  }
311
645
  finally {
312
- if (historyAbort.current === abort)
646
+ if (historyAbort.current === abort) {
313
647
  historyAbort.current = undefined;
648
+ setHistoryLoading(false);
649
+ }
314
650
  }
315
651
  }
316
652
  async function openSearchSession(sessionId, query) {
317
653
  await historyOperation(async (signal) => {
318
654
  await controller.selectSession(sessionId);
319
655
  await controller.waitForHistory(signal);
320
- await controller.historyThrough('first', signal);
656
+ setHistoryMatches(await controller.searchHistory(query, signal));
321
657
  setSearchResults(undefined);
322
658
  setContentSearch(true);
323
659
  setHistoryQuery(query);
324
660
  });
325
661
  }
326
- async function jumpHistory(target) {
662
+ async function jumpHistory(target, folds = reasoningOverrides) {
327
663
  if (state.screen !== 'chat')
328
664
  throw new Error('Select a session first');
329
665
  ++scrollIntent.current;
330
- if (target === 'last') {
331
- setHistoryQuery(undefined);
332
- setScroll(0);
333
- return;
334
- }
335
666
  const abort = new AbortController();
336
667
  historyAbort.current = abort;
337
668
  try {
338
- await controller.historyThrough(target, abort.signal);
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);
339
672
  abort.signal.throwIfAborted();
340
- const current = historyLayout(controller.state.transcript, width);
341
- const seq = target === 'first' ? current.messages[0]?.seq : target;
342
- const row = seq === undefined ? undefined : current.offsets.get(seq);
673
+ const current = historyLayout(transcript, width, reasoning, folds, liveReasoning);
674
+ const row = current.offsets.get(target);
343
675
  if (row === undefined)
344
676
  throw new Error('No visible message at this sequence; use /history to choose a record');
677
+ setHistoryWindow(transcript === state.transcript ? undefined : transcript);
345
678
  setHistoryQuery(undefined);
346
- setScroll(Math.max(0, current.lines.length - pageSize - row));
679
+ setHistoryMatches(undefined);
680
+ setThoughtList(false);
681
+ setScroll(Math.max(0, current.length - pageSize - row));
347
682
  }
348
683
  finally {
349
684
  if (historyAbort.current === abort)
350
685
  historyAbort.current = undefined;
351
686
  }
352
687
  }
353
- useMouseWheel(direction => scrollHistory(direction * 3));
354
- const end = Math.max(pageSize, lines.length - position);
355
- const visible = lines.slice(Math.max(0, end - pageSize), end);
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]);
356
704
  const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
357
- return _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "DeepSeek Harness \u00B7 HTTP TUI" }), _jsxs(Text, { dimColor: true, children: [controller.base, " \u00B7 ", state.status, state.busy ? ' · Working…' : ''] }), _jsx(Text, { wrap: "truncate-end", children: toolLine(`${workspace ? string(workspace.title) : 'All workspaces'}${state.sessionId ? ` / ${controller.sessionName}` : ''}`, Math.max(1, width - 4)) })] }), state.error && _jsx(Text, { color: "red", children: state.error }), searchResults ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Session search \u00B7 ", safeText(searchResults.query)] }), searchResults.hasMore && _jsx(Text, { color: "yellow", 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),
358
- label: toolLine(`${item.sessionId} · ${item.snippet}`, width - 2),
359
- action: () => operate(() => openSearchSession(string(item.sessionId), searchResults.query)),
360
- })), { 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 && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: visible.length ? visible.join('\n') : 'Start a conversation with the host agent.' }), state.transcript.hasMore && _jsx(Text, { dimColor: true, children: "Scroll up or /older to load earlier history" })] }), 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"] }), _jsx(Picker, { choices: [
361
- ...layout.messages.filter(message => (contentSearch ? message.text : `${message.seq} ${message.role} ${message.text}`).toLowerCase().includes(historyQuery.toLowerCase())).map(message => ({
362
- key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.text}`, width - 2),
363
- action: () => operate(() => jumpHistory(message.seq)),
364
- })),
365
- { key: 'close', label: '← Back to conversation', action: () => setHistoryQuery(undefined) },
366
- ], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `history:${historyQuery}`)] }), pending && _jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "yellow", children: question ? 'Question' : '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)) }), question?.options && _jsx(Text, { children: array(question.options).map(option => string(object(option).label)).join(' · ') }), _jsx(Text, { dimColor: true, children: question ? 'Type your answer below' : '/allow approves once · /deny rejects' })] })] }), _jsxs(Box, { borderStyle: "round", borderColor: state.online ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(draft.current); }, focus: state.online && !state.busy, 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: "red", children: matches.error })
367
- : matches.items.length === 0 ? _jsx(Text, { dimColor: true, children: "No matching host files" })
368
- : 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 ? 'cyan' : undefined, children: [index + Math.max(0, referenceIndex - 5) === referenceIndex ? '❯ ' : ' ', item.path, item.kind === 'directory' ? '/' : ''] }, `${item.kind}:${item.path}`))] }), _jsx(Text, { dimColor: true, children: "Enter send \u00B7 Esc cancel \u00B7 Wheel/PgUp/PgDn scroll \u00B7 Ctrl+C stop / exit" }), input.startsWith('/') && !input.includes(' ') && _jsx(Text, { dimColor: true, children: COMMANDS.filter(command => command.startsWith(input)).join(' ') }), help && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: HELP }), _jsx(Text, { dimColor: true, children: "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(StatusBar, { controller: controller, expanded: statusExpanded, revision: state.version })] });
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 }) })] })] }) }) }) });
369
746
  }