@agi-fans/dsh-tui 0.14.0 → 0.16.0
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/lib/CHANGELOG.md +77 -1
- package/lib/chrome/diff-render.js +12 -0
- package/lib/chrome/diff-render.js.map +1 -1
- package/lib/chrome/goal-bar.d.ts +13 -0
- package/lib/chrome/goal-bar.js +45 -0
- package/lib/chrome/goal-bar.js.map +1 -0
- package/lib/chrome/grapheme.d.ts +15 -0
- package/lib/chrome/grapheme.js +49 -0
- package/lib/chrome/grapheme.js.map +1 -0
- package/lib/chrome/main-screen-renderer.js +21 -5
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +101 -30
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/renderer.d.ts +13 -61
- package/lib/chrome/renderer.js +17 -181
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-config.d.ts +9 -4
- package/lib/chrome/status-config.js +13 -4
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -1
- package/lib/chrome/status-line.js +42 -10
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +9 -2
- package/lib/chrome/theme.js +60 -12
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-renderers.d.ts +2 -0
- package/lib/chrome/tool-renderers.js +149 -2
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/chrome/welcome-tips.js +1 -0
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/chrome/width.d.ts +19 -1
- package/lib/chrome/width.js +135 -41
- package/lib/chrome/width.js.map +1 -1
- package/lib/commands/diff.d.ts +32 -0
- package/lib/commands/diff.js +116 -0
- package/lib/commands/diff.js.map +1 -0
- package/lib/commands/jobs.d.ts +14 -0
- package/lib/commands/jobs.js +82 -0
- package/lib/commands/jobs.js.map +1 -0
- package/lib/commands/model.d.ts +25 -0
- package/lib/commands/model.js +168 -4
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/permission.d.ts +10 -1
- package/lib/commands/permission.js +23 -14
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/session.js +75 -1
- package/lib/commands/session.js.map +1 -1
- package/lib/definition.d.ts +38 -4
- package/lib/definition.js.map +1 -1
- package/lib/input/editor.d.ts +2 -0
- package/lib/input/editor.js +19 -9
- package/lib/input/editor.js.map +1 -1
- package/lib/input/keybindings-config.d.ts +1 -1
- package/lib/input/keybindings-config.js +9 -0
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/runtime/agent-profile.d.ts +11 -0
- package/lib/runtime/agent-profile.js +20 -1
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +15 -5
- package/lib/runtime/provider-local.js +392 -98
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +33 -8
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/tool-presentation.js +13 -2
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/interaction-adapter.d.ts +10 -0
- package/lib/session/interaction-adapter.js +12 -1
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/job-notice.d.ts +28 -0
- package/lib/session/job-notice.js +34 -0
- package/lib/session/job-notice.js.map +1 -0
- package/lib/session/live-attempt-tracker.d.ts +43 -0
- package/lib/session/live-attempt-tracker.js +58 -0
- package/lib/session/live-attempt-tracker.js.map +1 -0
- package/lib/session/project-context.d.ts +2 -0
- package/lib/session/project-context.js +22 -11
- package/lib/session/project-context.js.map +1 -1
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +10 -4
- package/lib/session/session-controller.js +293 -65
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +31 -1
- package/lib/session/subagent-roster.js +79 -19
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/tui-settings.js +1 -0
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/agent-hub.d.ts +3 -0
- package/lib/views/agent-hub.js +40 -4
- package/lib/views/agent-hub.js.map +1 -1
- package/lib/views/copy-selector.d.ts +3 -0
- package/lib/views/copy-selector.js +26 -6
- package/lib/views/copy-selector.js.map +1 -1
- package/lib/views/event-views.d.ts +59 -1
- package/lib/views/event-views.js +245 -73
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/history-search.d.ts +3 -0
- package/lib/views/history-search.js +40 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +39 -0
- package/lib/views/hotkey-format.js +42 -0
- package/lib/views/hotkey-format.js.map +1 -0
- package/lib/views/hotkeys.d.ts +3 -0
- package/lib/views/hotkeys.js +44 -7
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +3 -0
- package/lib/views/prompt-selector.js +46 -7
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +3 -0
- package/lib/views/settings-list.js +36 -5
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/stream-time.d.ts +14 -0
- package/lib/views/stream-time.js +32 -0
- package/lib/views/stream-time.js.map +1 -0
- package/lib/views/streaming-reveal.js +56 -8
- package/lib/views/streaming-reveal.js.map +1 -1
- package/lib/views/trajectory.d.ts +57 -1
- package/lib/views/trajectory.js +452 -60
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-search.d.ts +45 -0
- package/lib/views/transcript-search.js +105 -0
- package/lib/views/transcript-search.js.map +1 -0
- package/package.json +41 -30
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { TerminalNotificationController, terminalNotificationSequence, terminalProgressSequence, } from "./terminal-notifications.js";
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
|
-
import { join } from 'node:path';
|
|
15
|
+
import { join, sep } from 'node:path';
|
|
16
16
|
import { createInterface } from 'node:readline';
|
|
17
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
17
18
|
import { TUI_SERVICE, } from "../definition.js";
|
|
18
19
|
import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
|
|
19
20
|
import { applyPathCompletion, defaultPathSource, editorLineAt, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "../views/path-complete.js";
|
|
@@ -25,20 +26,22 @@ import { buildCopyTargets, extractCopyTarget, parseCopyKind } from "../views/cop
|
|
|
25
26
|
import { applyHistorySearchEvent, createHistorySearch, } from "../views/history-search.js";
|
|
26
27
|
import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
|
|
27
28
|
import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
|
|
28
|
-
import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, } from "../views/trajectory.js";
|
|
29
|
+
import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryListMetrics, } from "../views/trajectory.js";
|
|
29
30
|
import { applyAgentHubEvent, createAgentHub, updateAgentHub, } from "../views/agent-hub.js";
|
|
30
|
-
import { applyEvent, blockLines, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
|
|
31
|
+
import { applyEvent, applyStreamChunk, blockLines, blockSearchText, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
|
|
32
|
+
import { applyTranscriptSearchEvent, createTranscriptSearch, searchBlockIndexes, } from "../views/transcript-search.js";
|
|
31
33
|
import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
|
|
32
34
|
import { flushPending, parseKeys } from "../input/keys.js";
|
|
33
35
|
import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
|
|
34
|
-
import { createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
|
|
36
|
+
import { colorDisabledByEnv, createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
|
|
37
|
+
import { renderTool } from "../chrome/tool-renderers.js";
|
|
35
38
|
import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "../session/tui-settings.js";
|
|
36
39
|
import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
37
40
|
import { HistoryStore } from "../views/history-store.js";
|
|
38
41
|
import { loadKeybindings } from "../input/keybindings-config.js";
|
|
39
42
|
import { editExternally } from "../input/external-editor.js";
|
|
40
43
|
import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "../views/prompt-selector.js";
|
|
41
|
-
import { resolveProjectContext } from "../session/project-context.js";
|
|
44
|
+
import { refreshProjectContext, resolveProjectContext } from "../session/project-context.js";
|
|
42
45
|
import { pickWelcomeTips } from "../chrome/welcome-tips.js";
|
|
43
46
|
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
|
|
44
47
|
import { imageMarker, imagePathCandidates, probeImageDimensions, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
@@ -51,10 +54,24 @@ function shortenPath(cwd) {
|
|
|
51
54
|
const home = homedir();
|
|
52
55
|
if (cwd === home)
|
|
53
56
|
return '~';
|
|
54
|
-
|
|
57
|
+
// Windows separates with `\`, so the home prefix check follows the host.
|
|
58
|
+
if (cwd.startsWith(home + sep))
|
|
55
59
|
return '~' + cwd.slice(home.length);
|
|
56
60
|
return cwd;
|
|
57
61
|
}
|
|
62
|
+
/** Maximum code points written into the OSC 2 window title. */
|
|
63
|
+
const WINDOW_TITLE_MAX = 120;
|
|
64
|
+
/**
|
|
65
|
+
* Make a session title safe for an OSC 2 write: control characters are
|
|
66
|
+
* replaced so a title cannot terminate or inject the escape sequence, and the
|
|
67
|
+
* payload is bounded without splitting a surrogate pair.
|
|
68
|
+
*/
|
|
69
|
+
function windowTitleText(title) {
|
|
70
|
+
if (title === undefined)
|
|
71
|
+
return '';
|
|
72
|
+
const cleaned = title.replace(/[\u0000-\u001f\u007f-\u009f]/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
73
|
+
return [...cleaned].slice(0, WINDOW_TITLE_MAX).join('');
|
|
74
|
+
}
|
|
58
75
|
export const name = 'omdsh-tui';
|
|
59
76
|
/**
|
|
60
77
|
* Local terminal presentation service.
|
|
@@ -65,6 +82,18 @@ function detectTerminalProfile() {
|
|
|
65
82
|
}
|
|
66
83
|
return process.platform === 'win32' ? 'conpty' : 'direct';
|
|
67
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Decide whether SGR is emitted. An explicit preference — plugin config or a
|
|
87
|
+
* value the user picked in `/settings` — always wins, so a non-empty `NO_COLOR`
|
|
88
|
+
* can never block someone who asked for color. Without a preference, a
|
|
89
|
+
* non-empty `NO_COLOR` or `FORCE_COLOR=0` suppresses color and the output
|
|
90
|
+
* stream's tty-ness decides.
|
|
91
|
+
*/
|
|
92
|
+
function resolveColors(preference, isTty) {
|
|
93
|
+
if (preference !== undefined)
|
|
94
|
+
return preference;
|
|
95
|
+
return isTty && !colorDisabledByEnv();
|
|
96
|
+
}
|
|
68
97
|
export class LocalTui {
|
|
69
98
|
#deferInitialRender = false;
|
|
70
99
|
#terminalProfile;
|
|
@@ -86,6 +115,7 @@ export class LocalTui {
|
|
|
86
115
|
#draft = '';
|
|
87
116
|
#ac = null;
|
|
88
117
|
#search = null;
|
|
118
|
+
#transcriptSearch = null;
|
|
89
119
|
#settings = null;
|
|
90
120
|
#copySelector = null;
|
|
91
121
|
#trajectory = null;
|
|
@@ -151,9 +181,18 @@ export class LocalTui {
|
|
|
151
181
|
#tools = [];
|
|
152
182
|
#runtimeCommands = [];
|
|
153
183
|
#prompt = null;
|
|
184
|
+
/**
|
|
185
|
+
* The overlay surface displaced by an in-flight prompt: while a human
|
|
186
|
+
* prompt owns the keyboard, its screen must not compete with a stale
|
|
187
|
+
* trajectory/hub surface that accepts invisible confirmations. Restored
|
|
188
|
+
* when the prompt settles.
|
|
189
|
+
*/
|
|
190
|
+
#promptDisplaced = null;
|
|
154
191
|
#recentSessions = [];
|
|
155
192
|
#welcomeTips;
|
|
156
193
|
#sessionId;
|
|
194
|
+
#sessionTitle;
|
|
195
|
+
#writtenWindowTitle;
|
|
157
196
|
#sessionStats;
|
|
158
197
|
#sessionControls;
|
|
159
198
|
#loopStatus;
|
|
@@ -161,7 +200,12 @@ export class LocalTui {
|
|
|
161
200
|
#subagentLauncherFocused = false;
|
|
162
201
|
#inspected;
|
|
163
202
|
#promptDocument;
|
|
164
|
-
|
|
203
|
+
/**
|
|
204
|
+
* 24-bit switch for the current color preference, re-derived whenever that
|
|
205
|
+
* preference changes so a session that starts colorless can still enable
|
|
206
|
+
* hexadecimal palettes without a restart.
|
|
207
|
+
*/
|
|
208
|
+
#trueColor = false;
|
|
165
209
|
#copy;
|
|
166
210
|
#readClipboard;
|
|
167
211
|
#readClipboardImage;
|
|
@@ -186,7 +230,8 @@ export class LocalTui {
|
|
|
186
230
|
/**
|
|
187
231
|
* @param term - terminal surface (injectable for tests).
|
|
188
232
|
* @param model - model label for the status line.
|
|
189
|
-
* @param colors - SGR styling switch
|
|
233
|
+
* @param colors - SGR styling switch; `undefined` derives one from the
|
|
234
|
+
* output stream's tty-ness and the environment.
|
|
190
235
|
* @param themeName - shipped palette.
|
|
191
236
|
* @param copy - clipboard writer (defaults to the platform tool).
|
|
192
237
|
* @param paths - cwd/home/listing used by `@` and path autocomplete.
|
|
@@ -194,7 +239,7 @@ export class LocalTui {
|
|
|
194
239
|
constructor(term, model, colors, themeName = 'dark', copy = copyToClipboard, paths = {}) {
|
|
195
240
|
this.#term = term;
|
|
196
241
|
this.#model = model;
|
|
197
|
-
this.#colors = colors;
|
|
242
|
+
this.#colors = resolveColors(colors, term.output.isTTY === true);
|
|
198
243
|
this.#themeName = themeName;
|
|
199
244
|
this.#copy = copy;
|
|
200
245
|
this.#readClipboard = paths.readClipboard ?? readFromClipboard;
|
|
@@ -219,7 +264,7 @@ export class LocalTui {
|
|
|
219
264
|
this.#terminalProfile = paths.terminalProfile ?? detectTerminalProfile();
|
|
220
265
|
this.#resizeDebounceMs = Math.max(0, paths.resizeDebounceMs ?? 120);
|
|
221
266
|
this.#streamRenderMs = Math.max(0, paths.streamRenderMs ?? 8);
|
|
222
|
-
this.#
|
|
267
|
+
this.#syncTrueColor();
|
|
223
268
|
this.#tty = term.input.isTTY === true;
|
|
224
269
|
this.#pwd = shortenPath(project.root);
|
|
225
270
|
this.#branch = project.gitLabel;
|
|
@@ -257,6 +302,15 @@ export class LocalTui {
|
|
|
257
302
|
}
|
|
258
303
|
this.#render();
|
|
259
304
|
}
|
|
305
|
+
/** Refresh Git workspace metadata after a turn; the footer shows realtime branch/dirty state. */
|
|
306
|
+
#refreshProjectContext() {
|
|
307
|
+
const branch = refreshProjectContext(this.#cwd).gitLabel;
|
|
308
|
+
if (branch === this.#branch)
|
|
309
|
+
return;
|
|
310
|
+
this.#branch = branch;
|
|
311
|
+
if (this.#tty)
|
|
312
|
+
this.#render();
|
|
313
|
+
}
|
|
260
314
|
event(event, presentation) {
|
|
261
315
|
this.#state = applyEvent(this.#state, event, presentation);
|
|
262
316
|
this.#emitNotification(this.#notifications.event({
|
|
@@ -266,22 +320,37 @@ export class LocalTui {
|
|
|
266
320
|
}));
|
|
267
321
|
if (this.#trajectory !== null)
|
|
268
322
|
this.#trajectory = appendTrajectoryEvent(this.#trajectory, event);
|
|
269
|
-
|
|
323
|
+
if (event.type === 'turn/end')
|
|
324
|
+
this.#refreshProjectContext();
|
|
325
|
+
this.#syncStreamingReveal(undefined);
|
|
326
|
+
this.#syncTick();
|
|
327
|
+
if (this.#tty) {
|
|
328
|
+
this.#render();
|
|
329
|
+
}
|
|
330
|
+
else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
|
|
331
|
+
this.#printPlain();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Fold one live `agent/assistant-stream` chunk into the transcript and
|
|
336
|
+
* schedule the streaming reveal. Durable settlement follows on the session
|
|
337
|
+
* log as `assistant/message` or `assistant/attempt`.
|
|
338
|
+
*/
|
|
339
|
+
streamDelta(delta) {
|
|
340
|
+
this.#state = applyStreamChunk(this.#state, delta);
|
|
341
|
+
const smoothStream = this.#syncStreamingReveal(delta);
|
|
270
342
|
this.#syncTick();
|
|
271
343
|
if (this.#tty) {
|
|
272
344
|
if (smoothStream) {
|
|
273
345
|
// The 30 fps reveal clock owns presentation for textual deltas.
|
|
274
346
|
}
|
|
275
|
-
else if (
|
|
347
|
+
else if (this.#streamRenderMs > 0) {
|
|
276
348
|
this.#scheduleStreamRender();
|
|
277
349
|
}
|
|
278
350
|
else {
|
|
279
351
|
this.#render();
|
|
280
352
|
}
|
|
281
353
|
}
|
|
282
|
-
else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
|
|
283
|
-
this.#printPlain();
|
|
284
|
-
}
|
|
285
354
|
}
|
|
286
355
|
setStatus(status) {
|
|
287
356
|
if (this.#state.status === 'compacting')
|
|
@@ -304,6 +373,13 @@ export class LocalTui {
|
|
|
304
373
|
this.#render();
|
|
305
374
|
}
|
|
306
375
|
setSubagents(roster) {
|
|
376
|
+
const previous = this.#subagents?.agents ?? [];
|
|
377
|
+
const incoming = roster?.agents ?? [];
|
|
378
|
+
const statusChanged = previous.length !== incoming.length || incoming.some((agent, index) => {
|
|
379
|
+
const before = previous[index];
|
|
380
|
+
return before === undefined || before.id !== agent.id || before.label !== agent.label
|
|
381
|
+
|| before.phase !== agent.phase || before.depth !== agent.depth || before.mode !== agent.mode;
|
|
382
|
+
});
|
|
307
383
|
this.#subagents = roster === undefined
|
|
308
384
|
? undefined
|
|
309
385
|
: { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })) };
|
|
@@ -328,11 +404,17 @@ export class LocalTui {
|
|
|
328
404
|
};
|
|
329
405
|
}
|
|
330
406
|
}
|
|
407
|
+
if (!statusChanged && this.#agentHub === null)
|
|
408
|
+
return;
|
|
331
409
|
this.#syncTick();
|
|
332
410
|
if (this.#tty)
|
|
333
|
-
this.#
|
|
411
|
+
this.#scheduleStreamRender();
|
|
334
412
|
}
|
|
335
413
|
setInspectedSubagent(inspected) {
|
|
414
|
+
if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
|
|
415
|
+
&& this.#inspected?.phase === inspected?.phase && this.#inspected?.mode === inspected?.mode
|
|
416
|
+
&& this.#inspected?.writable === inspected?.writable)
|
|
417
|
+
return;
|
|
336
418
|
this.#inspected = inspected === undefined ? undefined : { ...inspected };
|
|
337
419
|
if (inspected !== undefined) {
|
|
338
420
|
this.#inspectEscapeFenceUntil = 0;
|
|
@@ -358,9 +440,9 @@ export class LocalTui {
|
|
|
358
440
|
openTrajectory(events) {
|
|
359
441
|
if (!this.#tty)
|
|
360
442
|
return false;
|
|
443
|
+
this.#finishPrompt(null);
|
|
361
444
|
this.#trajectory = createTrajectory(events);
|
|
362
445
|
this.#agentHub = null;
|
|
363
|
-
this.#prompt = null;
|
|
364
446
|
this.#settings = null;
|
|
365
447
|
this.#copySelector = null;
|
|
366
448
|
this.#search = null;
|
|
@@ -383,6 +465,31 @@ export class LocalTui {
|
|
|
383
465
|
setImageValidator(validate) {
|
|
384
466
|
this.#validateImageDraft = validate;
|
|
385
467
|
}
|
|
468
|
+
toolCallContext(callId) {
|
|
469
|
+
const block = this.#state.blocks.findLast(candidate => candidate.kind === 'tool' && String(candidate.callId) === callId);
|
|
470
|
+
if (block?.kind !== 'tool')
|
|
471
|
+
return undefined;
|
|
472
|
+
// Reuse the card renderer so a decision explains the call the same way the
|
|
473
|
+
// transcript already does. The approval prompt renders its detail on one
|
|
474
|
+
// line, so the parts are joined here rather than handed over as rows.
|
|
475
|
+
const view = renderTool({
|
|
476
|
+
name: block.name,
|
|
477
|
+
arguments: block.args,
|
|
478
|
+
output: block.output,
|
|
479
|
+
status: block.status,
|
|
480
|
+
expanded: false,
|
|
481
|
+
...(block.presentation === undefined ? {} : { presentation: block.presentation }),
|
|
482
|
+
});
|
|
483
|
+
const detail = view.summary === undefined || view.summary === ''
|
|
484
|
+
? view.input.join(' ').replace(/\s+/gu, ' ').trim()
|
|
485
|
+
: '';
|
|
486
|
+
// A card with a summary already says what the call is. Without one the raw
|
|
487
|
+
// argument text is all there is, and its pretty-printed form would reduce to
|
|
488
|
+
// a lone brace on a single row, so the whole body is folded into one line.
|
|
489
|
+
return [view.title, view.summary, detail === '' ? undefined : detail]
|
|
490
|
+
.filter((part) => part !== undefined && part !== '')
|
|
491
|
+
.join(' · ');
|
|
492
|
+
}
|
|
386
493
|
notice(text, options = {}) {
|
|
387
494
|
const block = {
|
|
388
495
|
kind: 'notice',
|
|
@@ -411,6 +518,7 @@ export class LocalTui {
|
|
|
411
518
|
this.#emitNotification(this.#notifications.humanPrompt());
|
|
412
519
|
this.#editor.setText('');
|
|
413
520
|
this.#ac = null;
|
|
521
|
+
const displaced = this.#displaceSurface();
|
|
414
522
|
return new Promise((resolve) => {
|
|
415
523
|
const selected = Math.max(0, request.options?.findIndex(option => (option.value ?? option.label) === request.initialValue) ?? 0);
|
|
416
524
|
const pending = { request, selected, checked: new Set(), resolve };
|
|
@@ -420,6 +528,7 @@ export class LocalTui {
|
|
|
420
528
|
pending.offAbort = () => { request.signal?.removeEventListener('abort', onAbort); };
|
|
421
529
|
}
|
|
422
530
|
this.#prompt = pending;
|
|
531
|
+
this.#promptDisplaced = displaced;
|
|
423
532
|
if (this.#tty) {
|
|
424
533
|
this.#render();
|
|
425
534
|
}
|
|
@@ -454,6 +563,8 @@ export class LocalTui {
|
|
|
454
563
|
}
|
|
455
564
|
setSession(info) {
|
|
456
565
|
this.#sessionId = info.id;
|
|
566
|
+
const title = info.title?.trim();
|
|
567
|
+
this.#sessionTitle = title === undefined || title === '' ? undefined : title;
|
|
457
568
|
this.#recentSessions = info.recent.map((session) => ({ ...session }));
|
|
458
569
|
this.#sessionStats = info.stats === undefined ? undefined : { ...info.stats };
|
|
459
570
|
this.#sessionControls = info.controls === undefined
|
|
@@ -464,6 +575,7 @@ export class LocalTui {
|
|
|
464
575
|
};
|
|
465
576
|
if (!this.#tty)
|
|
466
577
|
return;
|
|
578
|
+
this.#syncWindowTitle();
|
|
467
579
|
if (this.#reveal !== undefined && this.#motion !== 'off')
|
|
468
580
|
return;
|
|
469
581
|
if (this.#streamRenderMs > 0 && this.#busy())
|
|
@@ -471,11 +583,31 @@ export class LocalTui {
|
|
|
471
583
|
else
|
|
472
584
|
this.#render();
|
|
473
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* Mirror the folded session title into the terminal's own window/tab title.
|
|
588
|
+
* Control characters are stripped so a title can never inject an escape
|
|
589
|
+
* sequence, and the payload is bounded to keep tab strips readable.
|
|
590
|
+
*/
|
|
591
|
+
#syncWindowTitle() {
|
|
592
|
+
const title = windowTitleText(this.#sessionTitle);
|
|
593
|
+
if (title === this.#writtenWindowTitle)
|
|
594
|
+
return;
|
|
595
|
+
this.#writtenWindowTitle = title;
|
|
596
|
+
this.#term.output.write(`\x1b]2;${title}\x07`);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Re-derive the 24-bit switch from the active color preference. Every path
|
|
600
|
+
* that changes `#colors` calls this so `/settings` takes effect immediately.
|
|
601
|
+
*/
|
|
602
|
+
#syncTrueColor() {
|
|
603
|
+
this.#trueColor = this.#colors && detectTrueColor();
|
|
604
|
+
}
|
|
474
605
|
/** Apply prefs loaded from the settings document (does not persist). */
|
|
475
606
|
applyStoredPrefs(prefs) {
|
|
476
607
|
const previousMotion = this.#motion;
|
|
477
608
|
this.#themeName = prefs.theme;
|
|
478
609
|
this.#colors = prefs.colors;
|
|
610
|
+
this.#syncTrueColor();
|
|
479
611
|
this.#motion = prefs.motion ?? 'full';
|
|
480
612
|
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
481
613
|
this.#expandTools = prefs.expandTools;
|
|
@@ -523,10 +655,10 @@ export class LocalTui {
|
|
|
523
655
|
this.#render();
|
|
524
656
|
};
|
|
525
657
|
}
|
|
526
|
-
readInput() {
|
|
658
|
+
readInput(signal) {
|
|
527
659
|
if (this.#pending !== null)
|
|
528
660
|
return Promise.reject(new Error('omdsh-tui: input read already in flight'));
|
|
529
|
-
if (this.#disposed)
|
|
661
|
+
if (this.#disposed || signal?.aborted === true)
|
|
530
662
|
return Promise.resolve(null);
|
|
531
663
|
// A Ctrl-D pressed while the previous turn was still settling lands here
|
|
532
664
|
// (no pending readline existed to resolve); honor it now.
|
|
@@ -535,7 +667,7 @@ export class LocalTui {
|
|
|
535
667
|
return Promise.resolve(null);
|
|
536
668
|
}
|
|
537
669
|
if (!this.#tty)
|
|
538
|
-
return this.#readlinePlain();
|
|
670
|
+
return this.#readlinePlain(signal);
|
|
539
671
|
// Lines submitted while a turn was still running were queued instead of
|
|
540
672
|
// dropped; serve the oldest before waiting for fresh input.
|
|
541
673
|
const queued = this.#queuedSubmissions.shift();
|
|
@@ -544,7 +676,18 @@ export class LocalTui {
|
|
|
544
676
|
return Promise.resolve(queued);
|
|
545
677
|
}
|
|
546
678
|
return new Promise((resolve) => {
|
|
547
|
-
|
|
679
|
+
const pending = { resolve, signal: signal ?? null };
|
|
680
|
+
this.#pending = pending;
|
|
681
|
+
if (signal !== undefined) {
|
|
682
|
+
const onAbort = () => {
|
|
683
|
+
if (this.#pending !== pending)
|
|
684
|
+
return;
|
|
685
|
+
this.#pending = null;
|
|
686
|
+
resolve(null);
|
|
687
|
+
};
|
|
688
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
689
|
+
pending.offAbort = () => { signal.removeEventListener('abort', onAbort); };
|
|
690
|
+
}
|
|
548
691
|
});
|
|
549
692
|
}
|
|
550
693
|
/** Compatibility helper for provider-level text editing tests and embedders. */
|
|
@@ -651,6 +794,11 @@ export class LocalTui {
|
|
|
651
794
|
// prompt does not overwrite the transcript. Disable bracketed paste
|
|
652
795
|
// and restore the cursor.
|
|
653
796
|
this.#renderer.finish();
|
|
797
|
+
// Clear the session title we owned so the shell's own title returns.
|
|
798
|
+
if (this.#writtenWindowTitle !== undefined && this.#writtenWindowTitle !== '') {
|
|
799
|
+
this.#term.output.write('\x1b]2;\x07');
|
|
800
|
+
this.#writtenWindowTitle = undefined;
|
|
801
|
+
}
|
|
654
802
|
this.#term.output.write('\x1b[?2004l\x1b[?25h\r\n');
|
|
655
803
|
if (this.#resumeHintRequested && this.#sessionId !== undefined) {
|
|
656
804
|
this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
|
|
@@ -669,15 +817,23 @@ export class LocalTui {
|
|
|
669
817
|
this.#lineReader?.close();
|
|
670
818
|
this.#offAgentBehaviorWatch?.();
|
|
671
819
|
this.#offAgentBehaviorWatch = undefined;
|
|
672
|
-
this.#
|
|
673
|
-
this.#pending = null;
|
|
820
|
+
this.#settlePending(null);
|
|
674
821
|
this.#finishPrompt(null);
|
|
675
822
|
}
|
|
823
|
+
/** Settle one pending read, detaching its abort listener. */
|
|
824
|
+
#settlePending(submission) {
|
|
825
|
+
const pending = this.#pending;
|
|
826
|
+
if (pending === null)
|
|
827
|
+
return;
|
|
828
|
+
pending.offAbort?.();
|
|
829
|
+
this.#pending = null;
|
|
830
|
+
pending.resolve(submission);
|
|
831
|
+
}
|
|
676
832
|
/** Re-render the current frame (resize reflow). */
|
|
677
833
|
refresh() {
|
|
678
834
|
this.#render();
|
|
679
835
|
}
|
|
680
|
-
#readlinePlain() {
|
|
836
|
+
#readlinePlain(signal) {
|
|
681
837
|
return new Promise((resolve) => {
|
|
682
838
|
if (this.#lineReader === null) {
|
|
683
839
|
this.#lineReader = createInterface({ input: this.#term.input });
|
|
@@ -689,13 +845,26 @@ export class LocalTui {
|
|
|
689
845
|
this.#plainResolve(null);
|
|
690
846
|
});
|
|
691
847
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
848
|
+
const pending = { resolve, signal: signal ?? null };
|
|
849
|
+
this.#plainPending = pending;
|
|
850
|
+
if (signal !== undefined) {
|
|
851
|
+
const onAbort = () => {
|
|
852
|
+
if (this.#plainPending !== pending)
|
|
853
|
+
return;
|
|
854
|
+
this.#plainPending = null;
|
|
855
|
+
resolve(null);
|
|
856
|
+
};
|
|
857
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
858
|
+
pending.offAbort = () => { signal.removeEventListener('abort', onAbort); };
|
|
695
859
|
}
|
|
696
|
-
this.#
|
|
860
|
+
this.#pumpPlain();
|
|
697
861
|
});
|
|
698
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* Queue one readline delivery, then drain. A single stream chunk can carry
|
|
865
|
+
* several lines; every line is buffered until the runner asks for the next
|
|
866
|
+
* read, and EOF waits for the queue before closing the reader.
|
|
867
|
+
*/
|
|
699
868
|
#plainResolve(line) {
|
|
700
869
|
if (this.#prompt !== null && line !== null) {
|
|
701
870
|
const value = line.trim();
|
|
@@ -717,9 +886,27 @@ export class LocalTui {
|
|
|
717
886
|
}
|
|
718
887
|
if (this.#prompt !== null)
|
|
719
888
|
this.#finishPrompt(null);
|
|
889
|
+
if (line !== null)
|
|
890
|
+
this.#plainQueue.push(line);
|
|
891
|
+
this.#pumpPlain();
|
|
892
|
+
}
|
|
893
|
+
/** Resolve the pending plain read from the queue, or close it after EOF drained. */
|
|
894
|
+
#pumpPlain() {
|
|
720
895
|
const pending = this.#plainPending;
|
|
721
|
-
|
|
722
|
-
|
|
896
|
+
if (pending === null)
|
|
897
|
+
return;
|
|
898
|
+
const line = this.#plainQueue.shift();
|
|
899
|
+
if (line !== undefined) {
|
|
900
|
+
pending.offAbort?.();
|
|
901
|
+
this.#plainPending = null;
|
|
902
|
+
pending.resolve({ text: line, images: [] });
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (this.#plainClosed) {
|
|
906
|
+
pending.offAbort?.();
|
|
907
|
+
this.#plainPending = null;
|
|
908
|
+
pending.resolve(null);
|
|
909
|
+
}
|
|
723
910
|
}
|
|
724
911
|
/** Print plain-mode blocks that settled since the last flush. */
|
|
725
912
|
#printPlain() {
|
|
@@ -761,9 +948,9 @@ export class LocalTui {
|
|
|
761
948
|
this.#tick = null;
|
|
762
949
|
}
|
|
763
950
|
}
|
|
764
|
-
#syncStreamingReveal(
|
|
765
|
-
const textualDelta =
|
|
766
|
-
&& (
|
|
951
|
+
#syncStreamingReveal(delta) {
|
|
952
|
+
const textualDelta = delta !== undefined
|
|
953
|
+
&& (delta.chunk.type === 'text-delta' || delta.chunk.type === 'reasoning-delta');
|
|
767
954
|
const key = streamingAssistantKey(this.#state);
|
|
768
955
|
if (!textualDelta || this.#motion === 'off' || key === undefined) {
|
|
769
956
|
this.#stopRevealTick();
|
|
@@ -868,6 +1055,7 @@ export class LocalTui {
|
|
|
868
1055
|
colors: this.#colors,
|
|
869
1056
|
pwd: this.#pwd,
|
|
870
1057
|
...(this.#branch !== undefined ? { branch: this.#branch } : {}),
|
|
1058
|
+
...(this.#sessionTitle === undefined ? {} : { sessionTitle: this.#sessionTitle }),
|
|
871
1059
|
version: APP_VERSION,
|
|
872
1060
|
appName: APP_NAME,
|
|
873
1061
|
spinnerFrame: this.#spinner,
|
|
@@ -876,6 +1064,14 @@ export class LocalTui {
|
|
|
876
1064
|
themeName: this.#themeName,
|
|
877
1065
|
scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
|
|
878
1066
|
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock }),
|
|
1067
|
+
...(this.#transcriptSearch === null ? {} : {
|
|
1068
|
+
transcriptSearch: {
|
|
1069
|
+
query: this.#transcriptSearch.query,
|
|
1070
|
+
matches: searchBlockIndexes(this.#state.blocks.map(blockSearchText), this.#transcriptSearch.query),
|
|
1071
|
+
focus: this.#transcriptSearch.focus,
|
|
1072
|
+
editing: this.#transcriptSearch.editing,
|
|
1073
|
+
},
|
|
1074
|
+
}),
|
|
879
1075
|
toolsExpanded: this.#toolsExpanded,
|
|
880
1076
|
expandedTools: this.#expandedToolCalls,
|
|
881
1077
|
commands: this.#commands(),
|
|
@@ -946,8 +1142,13 @@ export class LocalTui {
|
|
|
946
1142
|
this.#follow = true;
|
|
947
1143
|
this.#scrollStart = this.#maxStart;
|
|
948
1144
|
}
|
|
1145
|
+
#utf8 = new StringDecoder('utf8');
|
|
1146
|
+
#plainQueue = [];
|
|
949
1147
|
#onData(chunk) {
|
|
950
|
-
|
|
1148
|
+
// Decode bytes across the whole stream so a multi-byte UTF-8 character
|
|
1149
|
+
// split between data events is never corrupted into replacement chars.
|
|
1150
|
+
const text = this.#utf8.write(chunk);
|
|
1151
|
+
const { events, rest } = parseKeys(this.#pendingKeys + text);
|
|
951
1152
|
this.#pendingKeys = rest;
|
|
952
1153
|
if (this.#escapeTimer !== null) {
|
|
953
1154
|
clearTimeout(this.#escapeTimer);
|
|
@@ -1211,7 +1412,9 @@ export class LocalTui {
|
|
|
1211
1412
|
if (this.#handlePrompt(event))
|
|
1212
1413
|
return;
|
|
1213
1414
|
if (this.#trajectory !== null) {
|
|
1214
|
-
const
|
|
1415
|
+
const { pageSize } = trajectoryListMetrics(this.#trajectory, this.#term.height());
|
|
1416
|
+
const { pageLines } = trajectoryDetailMetrics(this.#trajectory, this.#term.height());
|
|
1417
|
+
const command = applyTrajectoryEvent(this.#trajectory, event, { pageSize, detailPageLines: pageLines });
|
|
1215
1418
|
if ('close' in command)
|
|
1216
1419
|
this.#trajectory = null;
|
|
1217
1420
|
else
|
|
@@ -1232,12 +1435,14 @@ export class LocalTui {
|
|
|
1232
1435
|
this.#render();
|
|
1233
1436
|
return;
|
|
1234
1437
|
}
|
|
1438
|
+
if (this.#transcriptSearch !== null) {
|
|
1439
|
+
this.#applyTranscriptSearch(event);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1235
1442
|
if (event.type === 'key') {
|
|
1236
|
-
const action = this.#
|
|
1237
|
-
if (action !== undefined)
|
|
1238
|
-
this.#runAction(action);
|
|
1443
|
+
const action = this.#boundAction(event.id);
|
|
1444
|
+
if (action !== undefined && this.#runAction(action))
|
|
1239
1445
|
return;
|
|
1240
|
-
}
|
|
1241
1446
|
}
|
|
1242
1447
|
if (this.#inspected !== undefined && this.#inspected.writable !== true && event.type === 'text'
|
|
1243
1448
|
&& this.#prompt === null && this.#settings === null && this.#copySelector === null && this.#search === null) {
|
|
@@ -1293,14 +1498,6 @@ export class LocalTui {
|
|
|
1293
1498
|
this.#applyCopySelector(event);
|
|
1294
1499
|
return;
|
|
1295
1500
|
}
|
|
1296
|
-
if (event.type === 'key' && event.id === 'ctrl+r') {
|
|
1297
|
-
if (this.#images.length > 0)
|
|
1298
|
-
return;
|
|
1299
|
-
this.#search = createHistorySearch(this.#history);
|
|
1300
|
-
this.#ac = null;
|
|
1301
|
-
this.#render();
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
1501
|
if (this.#search !== null) {
|
|
1305
1502
|
this.#applySearch(event);
|
|
1306
1503
|
return;
|
|
@@ -1344,44 +1541,6 @@ export class LocalTui {
|
|
|
1344
1541
|
if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
|
|
1345
1542
|
&& this.#removeImageAtCursor(event.id))
|
|
1346
1543
|
return;
|
|
1347
|
-
if (event.type === 'key') {
|
|
1348
|
-
if (event.id === 'pageUp') {
|
|
1349
|
-
this.#scrollBy(-this.#pageSize());
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
if (event.id === 'pageDown') {
|
|
1353
|
-
this.#scrollBy(this.#pageSize());
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
if (event.id === 'shift+up') {
|
|
1357
|
-
this.#scrollBy(-TRANSCRIPT_FAST_SCROLL);
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
if (event.id === 'shift+down') {
|
|
1361
|
-
this.#scrollBy(TRANSCRIPT_FAST_SCROLL);
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (event.id === 'ctrl+o') {
|
|
1365
|
-
const last = this.#state.blocks.at(-1);
|
|
1366
|
-
const tool = last?.kind === 'toolCatalog'
|
|
1367
|
-
? undefined
|
|
1368
|
-
: this.#state.blocks.findLast(block => block.kind === 'tool');
|
|
1369
|
-
if (last?.kind === 'toolCatalog') {
|
|
1370
|
-
this.#toolsExpanded = !this.#toolsExpanded;
|
|
1371
|
-
}
|
|
1372
|
-
else if (tool?.kind === 'tool') {
|
|
1373
|
-
if (this.#expandedToolCalls.has(tool.callId))
|
|
1374
|
-
this.#expandedToolCalls.delete(tool.callId);
|
|
1375
|
-
else
|
|
1376
|
-
this.#expandedToolCalls.add(tool.callId);
|
|
1377
|
-
}
|
|
1378
|
-
else {
|
|
1379
|
-
this.#toolsExpanded = !this.#toolsExpanded;
|
|
1380
|
-
}
|
|
1381
|
-
this.#render();
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
1544
|
this.#applyCommand(this.#editor.handle(event));
|
|
1386
1545
|
}
|
|
1387
1546
|
#handlePrompt(event) {
|
|
@@ -1651,6 +1810,7 @@ export class LocalTui {
|
|
|
1651
1810
|
const expandChanged = prefs.expandTools !== this.#expandTools;
|
|
1652
1811
|
this.#themeName = prefs.theme;
|
|
1653
1812
|
this.#colors = prefs.colors;
|
|
1813
|
+
this.#syncTrueColor();
|
|
1654
1814
|
const previousMotion = this.#motion;
|
|
1655
1815
|
this.#motion = prefs.motion ?? 'full';
|
|
1656
1816
|
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
@@ -2053,6 +2213,7 @@ export class LocalTui {
|
|
|
2053
2213
|
this.#resumeHintRequested = true;
|
|
2054
2214
|
if (this.#pending !== null) {
|
|
2055
2215
|
const pending = this.#pending;
|
|
2216
|
+
pending.offAbort?.();
|
|
2056
2217
|
this.#pending = null;
|
|
2057
2218
|
pending.resolve(null);
|
|
2058
2219
|
}
|
|
@@ -2102,6 +2263,7 @@ export class LocalTui {
|
|
|
2102
2263
|
}
|
|
2103
2264
|
const pending = this.#pending;
|
|
2104
2265
|
if (pending !== null) {
|
|
2266
|
+
pending.offAbort?.();
|
|
2105
2267
|
this.#pending = null;
|
|
2106
2268
|
pending.resolve({ text: submittedText, images });
|
|
2107
2269
|
if (queueEditNewer !== null)
|
|
@@ -2163,7 +2325,10 @@ export class LocalTui {
|
|
|
2163
2325
|
return;
|
|
2164
2326
|
}
|
|
2165
2327
|
if (command.name === 'clear') {
|
|
2166
|
-
|
|
2328
|
+
// Presentation-only reset: the agent may still own an active turn with
|
|
2329
|
+
// live status, todos, and queued inbox state. Clearing those would make
|
|
2330
|
+
// the next Ctrl-C (or follow-up) behave as if the session had finished.
|
|
2331
|
+
this.#state = { ...this.#state, blocks: [] };
|
|
2167
2332
|
this.#followTail();
|
|
2168
2333
|
this.#renderer.startEpoch();
|
|
2169
2334
|
this.#render();
|
|
@@ -2191,6 +2356,7 @@ export class LocalTui {
|
|
|
2191
2356
|
const raw = '/' + name + (args === '' ? '' : ' ' + args);
|
|
2192
2357
|
const pending = this.#pending;
|
|
2193
2358
|
if (pending !== null) {
|
|
2359
|
+
pending.offAbort?.();
|
|
2194
2360
|
this.#pending = null;
|
|
2195
2361
|
pending.resolve({ text: raw, images: [] });
|
|
2196
2362
|
}
|
|
@@ -2215,7 +2381,7 @@ export class LocalTui {
|
|
|
2215
2381
|
formatHelpText(this.#commands()),
|
|
2216
2382
|
'',
|
|
2217
2383
|
full
|
|
2218
|
-
? `**Keyboard Shortcuts · ${hotkeyCount(this.#keybindings)}
|
|
2384
|
+
? `**Keyboard Shortcuts · ${hotkeyCount(this.#keybindings)} shortcuts**`
|
|
2219
2385
|
: '**Essential Shortcuts**',
|
|
2220
2386
|
'',
|
|
2221
2387
|
full
|
|
@@ -2246,6 +2412,42 @@ export class LocalTui {
|
|
|
2246
2412
|
this.#prompt = null;
|
|
2247
2413
|
pending.offAbort?.();
|
|
2248
2414
|
pending.resolve(answer);
|
|
2415
|
+
this.#restoreDisplacedSurface();
|
|
2416
|
+
}
|
|
2417
|
+
/** Save and close the visible overlay so a prompt owns both input and screen. */
|
|
2418
|
+
#displaceSurface() {
|
|
2419
|
+
const displaced = this.#trajectory !== null
|
|
2420
|
+
? { kind: 'trajectory', state: this.#trajectory }
|
|
2421
|
+
: this.#agentHub !== null
|
|
2422
|
+
? { kind: 'agentHub', state: this.#agentHub }
|
|
2423
|
+
: this.#settings !== null
|
|
2424
|
+
? { kind: 'settings', state: this.#settings }
|
|
2425
|
+
: this.#copySelector !== null
|
|
2426
|
+
? { kind: 'copySelector', state: this.#copySelector }
|
|
2427
|
+
: this.#search !== null ? { kind: 'search', state: this.#search } : null;
|
|
2428
|
+
this.#trajectory = null;
|
|
2429
|
+
this.#agentHub = null;
|
|
2430
|
+
this.#settings = null;
|
|
2431
|
+
this.#copySelector = null;
|
|
2432
|
+
this.#search = null;
|
|
2433
|
+
return displaced;
|
|
2434
|
+
}
|
|
2435
|
+
/** Restore the surface displaced by the settled prompt, if one existed. */
|
|
2436
|
+
#restoreDisplacedSurface() {
|
|
2437
|
+
const displaced = this.#promptDisplaced;
|
|
2438
|
+
this.#promptDisplaced = null;
|
|
2439
|
+
if (displaced === null)
|
|
2440
|
+
return;
|
|
2441
|
+
if (displaced.kind === 'trajectory')
|
|
2442
|
+
this.#trajectory = displaced.state;
|
|
2443
|
+
else if (displaced.kind === 'agentHub')
|
|
2444
|
+
this.#agentHub = displaced.state;
|
|
2445
|
+
else if (displaced.kind === 'settings')
|
|
2446
|
+
this.#settings = displaced.state;
|
|
2447
|
+
else if (displaced.kind === 'copySelector')
|
|
2448
|
+
this.#copySelector = displaced.state;
|
|
2449
|
+
else
|
|
2450
|
+
this.#search = displaced.state;
|
|
2249
2451
|
}
|
|
2250
2452
|
async #runCopy(args) {
|
|
2251
2453
|
if (args.trim() === '') {
|
|
@@ -2318,41 +2520,96 @@ export class LocalTui {
|
|
|
2318
2520
|
this.#follow = false;
|
|
2319
2521
|
this.#focusBlock = Math.max(0, this.#state.blocks.length - 1);
|
|
2320
2522
|
}
|
|
2523
|
+
/** Resolve a configured action for one key id, tolerating camelCase ids. */
|
|
2524
|
+
#boundAction(id) {
|
|
2525
|
+
return this.#keybindings[id] ?? this.#keybindings[id.toLowerCase()];
|
|
2526
|
+
}
|
|
2527
|
+
/** Run one configured action; returns true when the event was consumed. */
|
|
2321
2528
|
#runAction(action) {
|
|
2322
2529
|
if (this.#prompt !== null || this.#settings !== null || this.#copySelector !== null)
|
|
2323
|
-
return;
|
|
2530
|
+
return false;
|
|
2531
|
+
if (action === 'scroll-page-up') {
|
|
2532
|
+
if (this.#search !== null)
|
|
2533
|
+
return false;
|
|
2534
|
+
this.#scrollBy(-this.#pageSize());
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
if (action === 'scroll-page-down') {
|
|
2538
|
+
if (this.#search !== null)
|
|
2539
|
+
return false;
|
|
2540
|
+
this.#scrollBy(this.#pageSize());
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
if (action === 'scroll-fast-up') {
|
|
2544
|
+
if (this.#search !== null)
|
|
2545
|
+
return false;
|
|
2546
|
+
this.#scrollBy(-TRANSCRIPT_FAST_SCROLL);
|
|
2547
|
+
return true;
|
|
2548
|
+
}
|
|
2549
|
+
if (action === 'scroll-fast-down') {
|
|
2550
|
+
if (this.#search !== null)
|
|
2551
|
+
return false;
|
|
2552
|
+
this.#scrollBy(TRANSCRIPT_FAST_SCROLL);
|
|
2553
|
+
return true;
|
|
2554
|
+
}
|
|
2555
|
+
if (action === 'toggle-tools') {
|
|
2556
|
+
if (this.#search !== null)
|
|
2557
|
+
return false;
|
|
2558
|
+
this.#toggleToolExpansion();
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
2561
|
+
if (action === 'search-history') {
|
|
2562
|
+
if (this.#images.length > 0)
|
|
2563
|
+
return false;
|
|
2564
|
+
this.#search = createHistorySearch(this.#history);
|
|
2565
|
+
this.#ac = null;
|
|
2566
|
+
this.#render();
|
|
2567
|
+
return true;
|
|
2568
|
+
}
|
|
2569
|
+
if (action === 'search-transcript') {
|
|
2570
|
+
// An empty composer has no forward-char target, so Ctrl+F searches the
|
|
2571
|
+
// transcript there; a non-empty draft keeps the editor's own binding.
|
|
2572
|
+
if (this.#search !== null)
|
|
2573
|
+
return false;
|
|
2574
|
+
if (this.#editor.text !== '' || this.#images.length > 0)
|
|
2575
|
+
return false;
|
|
2576
|
+
this.#transcriptSearch = createTranscriptSearch();
|
|
2577
|
+
this.#ac = null;
|
|
2578
|
+
this.#render();
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2324
2581
|
if (action === 'inspect-subagent') {
|
|
2325
2582
|
void this.#pickSubagent();
|
|
2326
|
-
return;
|
|
2583
|
+
return true;
|
|
2327
2584
|
}
|
|
2328
2585
|
if (action === 'retry') {
|
|
2329
2586
|
this.#submit('/retry');
|
|
2330
|
-
return;
|
|
2587
|
+
return true;
|
|
2331
2588
|
}
|
|
2332
2589
|
if (action === 'cycle-model-forward') {
|
|
2333
2590
|
this.#submit('/model next');
|
|
2334
|
-
return;
|
|
2591
|
+
return true;
|
|
2335
2592
|
}
|
|
2336
2593
|
if (action === 'cycle-model-backward') {
|
|
2337
2594
|
this.#submit('/model previous');
|
|
2338
|
-
return;
|
|
2595
|
+
return true;
|
|
2339
2596
|
}
|
|
2340
2597
|
if (action === 'cycle-reasoning') {
|
|
2341
2598
|
this.#submit('/model reasoning');
|
|
2342
|
-
return;
|
|
2599
|
+
return true;
|
|
2343
2600
|
}
|
|
2344
2601
|
if (action === 'copy-prompt') {
|
|
2345
2602
|
void this.#copyPicked(this.#editor.text, 'current prompt');
|
|
2346
|
-
return;
|
|
2603
|
+
return true;
|
|
2347
2604
|
}
|
|
2348
2605
|
if (action === 'copy-line') {
|
|
2349
2606
|
const text = this.#editor.text.slice(lineStart(this.#editor.text, this.#editor.cursor), lineEnd(this.#editor.text, this.#editor.cursor));
|
|
2350
2607
|
void this.#copyPicked(text, 'current line');
|
|
2351
|
-
return;
|
|
2608
|
+
return true;
|
|
2352
2609
|
}
|
|
2353
2610
|
if (action === 'paste-clipboard') {
|
|
2354
2611
|
this.#startAsyncPaste(this.#pasteClipboard());
|
|
2355
|
-
return;
|
|
2612
|
+
return true;
|
|
2356
2613
|
}
|
|
2357
2614
|
try {
|
|
2358
2615
|
this.#term.input.setRawMode?.(false);
|
|
@@ -2369,6 +2626,43 @@ export class LocalTui {
|
|
|
2369
2626
|
this.#refreshAutocomplete();
|
|
2370
2627
|
this.#render();
|
|
2371
2628
|
}
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
/** Expand or collapse the newest tool card, or the whole tool catalog. */
|
|
2632
|
+
#toggleToolExpansion() {
|
|
2633
|
+
const last = this.#state.blocks.at(-1);
|
|
2634
|
+
const tool = last?.kind === 'toolCatalog'
|
|
2635
|
+
? undefined
|
|
2636
|
+
: this.#state.blocks.findLast(block => block.kind === 'tool');
|
|
2637
|
+
if (last?.kind === 'toolCatalog') {
|
|
2638
|
+
this.#toolsExpanded = !this.#toolsExpanded;
|
|
2639
|
+
}
|
|
2640
|
+
else if (tool?.kind === 'tool') {
|
|
2641
|
+
if (this.#expandedToolCalls.has(tool.callId))
|
|
2642
|
+
this.#expandedToolCalls.delete(tool.callId);
|
|
2643
|
+
else
|
|
2644
|
+
this.#expandedToolCalls.add(tool.callId);
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
this.#toolsExpanded = !this.#toolsExpanded;
|
|
2648
|
+
}
|
|
2649
|
+
this.#render();
|
|
2650
|
+
}
|
|
2651
|
+
/** Fold one event into the active transcript search. */
|
|
2652
|
+
#applyTranscriptSearch(event) {
|
|
2653
|
+
const state = this.#transcriptSearch;
|
|
2654
|
+
if (state === null)
|
|
2655
|
+
return;
|
|
2656
|
+
const command = applyTranscriptSearchEvent(state, event, this.#state.blocks.map(blockSearchText));
|
|
2657
|
+
if (command.kind === 'close') {
|
|
2658
|
+
this.#transcriptSearch = null;
|
|
2659
|
+
this.#render();
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
this.#transcriptSearch = command.state;
|
|
2663
|
+
if (command.kind === 'focus')
|
|
2664
|
+
this.#focusBlock = command.block;
|
|
2665
|
+
this.#render();
|
|
2372
2666
|
}
|
|
2373
2667
|
}
|
|
2374
2668
|
/**
|
|
@@ -2389,7 +2683,7 @@ export function apply(ctx, config) {
|
|
|
2389
2683
|
return () => { process.stdout.removeListener('resize', listener); };
|
|
2390
2684
|
},
|
|
2391
2685
|
};
|
|
2392
|
-
const tui = new LocalTui(term, config.model, config.colors
|
|
2686
|
+
const tui = new LocalTui(term, config.model, config.colors, parseThemeName(config.theme), copyToClipboard, {
|
|
2393
2687
|
deferInitialRender: true,
|
|
2394
2688
|
terminalProfile,
|
|
2395
2689
|
alternateScreenOverlays: terminalProfile === 'direct',
|
|
@@ -2399,7 +2693,7 @@ export function apply(ctx, config) {
|
|
|
2399
2693
|
ctx.provide(TUI_SERVICE, tui);
|
|
2400
2694
|
ctx.effect(() => () => { tui.dispose(); });
|
|
2401
2695
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
2402
|
-
const scope = settingsCtx.settings.register(TUI_SETTINGS_NAMESPACE, TuiSettingsSchema, { base: { theme: parseThemeName(config.theme), colors: config.colors
|
|
2696
|
+
const scope = settingsCtx.settings.register(TUI_SETTINGS_NAMESPACE, TuiSettingsSchema, { base: { theme: parseThemeName(config.theme), colors: resolveColors(config.colors, term.output.isTTY === true), expandTools: false } });
|
|
2403
2697
|
tui.applyStoredPrefs(scope.get());
|
|
2404
2698
|
tui.setPrefsPersist((prefs) => { void scope.update(prefs); });
|
|
2405
2699
|
scope.watch((next) => { tui.applyStoredPrefs(next); });
|