@agi-fans/dsh-tui 0.16.0 → 0.18.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 +53 -1
- package/lib/chrome/status-config.d.ts +4 -1
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/tool-args.d.ts +9 -0
- package/lib/chrome/tool-args.js +20 -0
- package/lib/chrome/tool-args.js.map +1 -0
- package/lib/chrome/tool-renderers.js +4 -15
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/commands/auth.js +4 -2
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/jobs.d.ts +2 -2
- package/lib/commands/jobs.js +2 -2
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/session.js +2 -4
- package/lib/commands/session.js.map +1 -1
- package/lib/definition.d.ts +3 -0
- package/lib/definition.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +44 -0
- package/lib/runtime/composer-images.js +149 -0
- package/lib/runtime/composer-images.js.map +1 -0
- package/lib/runtime/herdr-agent.d.ts +95 -0
- package/lib/runtime/herdr-agent.js +258 -0
- package/lib/runtime/herdr-agent.js.map +1 -0
- package/lib/runtime/plain-tui.d.ts +34 -0
- package/lib/runtime/plain-tui.js +119 -0
- package/lib/runtime/plain-tui.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +46 -7
- package/lib/runtime/provider-local.js +181 -299
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/session-runtime.d.ts +9 -1
- package/lib/runtime/session-runtime.js +2 -2
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/startup-notices.js +6 -9
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/tool-presentation.js +18 -3
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/content-text.d.ts +12 -0
- package/lib/session/content-text.js +13 -0
- package/lib/session/content-text.js.map +1 -0
- package/lib/session/history-store.js.map +1 -0
- package/lib/session/job-notice.d.ts +2 -2
- package/lib/session/job-notice.js.map +1 -1
- package/lib/session/json-file.d.ts +5 -0
- package/lib/session/json-file.js +21 -0
- package/lib/session/json-file.js.map +1 -0
- package/lib/session/model-favorites.js +15 -25
- package/lib/session/model-favorites.js.map +1 -1
- package/lib/session/release-notes.js +1 -18
- package/lib/session/release-notes.js.map +1 -1
- package/lib/session/session-configuration.d.ts +0 -3
- package/lib/session/session-configuration.js +0 -4
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +6 -4
- package/lib/session/session-controller.js +160 -182
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +20 -0
- package/lib/session/session-library.js +55 -13
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.js +16 -46
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +51 -0
- package/lib/session/subagent-tracker.js +128 -0
- package/lib/session/subagent-tracker.js.map +1 -0
- package/lib/session/tui-settings.d.ts +41 -18
- package/lib/session/tui-settings.js +57 -39
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/update-check.js +2 -15
- package/lib/session/update-check.js.map +1 -1
- package/lib/session/version-compare.d.ts +3 -0
- package/lib/session/version-compare.js +21 -0
- package/lib/session/version-compare.js.map +1 -0
- package/lib/views/at-complete.d.ts +2 -0
- package/lib/views/at-complete.js +2 -2
- package/lib/views/at-complete.js.map +1 -1
- package/lib/views/event-views.d.ts +11 -270
- package/lib/views/event-views.js +21 -976
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/settings-list.d.ts +2 -2
- package/lib/views/settings-list.js +12 -11
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-export.js +0 -2
- package/lib/views/transcript-export.js.map +1 -1
- package/lib/views/transcript-render.d.ts +179 -0
- package/lib/views/transcript-render.js +916 -0
- package/lib/views/transcript-render.js.map +1 -0
- package/lib/views/transcript-types.d.ts +104 -0
- package/lib/views/transcript-types.js +53 -0
- package/lib/views/transcript-types.js.map +1 -0
- package/package.json +39 -37
- package/lib/views/history-store.js.map +0 -1
- /package/lib/{views → session}/history-store.d.ts +0 -0
- /package/lib/{views → session}/history-store.js +0 -0
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* @module @agi-fans/dsh-tui
|
|
12
12
|
*/
|
|
13
13
|
import { TerminalNotificationController, terminalNotificationSequence, terminalProgressSequence, } from "./terminal-notifications.js";
|
|
14
|
+
import { HerdrAgentReporter } from "./herdr-agent.js";
|
|
15
|
+
import { ComposerImages } from "./composer-images.js";
|
|
16
|
+
import { PlainTui } from "./plain-tui.js";
|
|
14
17
|
import { homedir } from 'node:os';
|
|
15
18
|
import { join, sep } from 'node:path';
|
|
16
|
-
import { createInterface } from 'node:readline';
|
|
17
19
|
import { StringDecoder } from 'node:string_decoder';
|
|
18
20
|
import { TUI_SERVICE, } from "../definition.js";
|
|
19
21
|
import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
|
|
@@ -28,28 +30,43 @@ import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
|
|
|
28
30
|
import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
|
|
29
31
|
import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryListMetrics, } from "../views/trajectory.js";
|
|
30
32
|
import { applyAgentHubEvent, createAgentHub, updateAgentHub, } from "../views/agent-hub.js";
|
|
31
|
-
import { applyEvent, applyStreamChunk,
|
|
33
|
+
import { applyEvent, applyStreamChunk, blockSearchText, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
|
|
32
34
|
import { applyTranscriptSearchEvent, createTranscriptSearch, searchBlockIndexes, } from "../views/transcript-search.js";
|
|
33
35
|
import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
|
|
34
36
|
import { flushPending, parseKeys } from "../input/keys.js";
|
|
35
37
|
import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
|
|
36
|
-
import { colorDisabledByEnv,
|
|
38
|
+
import { colorDisabledByEnv, detectTrueColor } from "../chrome/theme.js";
|
|
37
39
|
import { renderTool } from "../chrome/tool-renderers.js";
|
|
38
|
-
import {
|
|
40
|
+
import { TUI_SETTINGS_ENTRY, TUI_SETTINGS_FIELDS } from "../session/tui-settings.js";
|
|
39
41
|
import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
40
|
-
import { HistoryStore } from "../
|
|
42
|
+
import { HistoryStore } from "../session/history-store.js";
|
|
41
43
|
import { loadKeybindings } from "../input/keybindings-config.js";
|
|
42
44
|
import { editExternally } from "../input/external-editor.js";
|
|
45
|
+
import z from '@deepseek-ai/schemastery';
|
|
43
46
|
import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "../views/prompt-selector.js";
|
|
44
47
|
import { refreshProjectContext, resolveProjectContext } from "../session/project-context.js";
|
|
45
48
|
import { pickWelcomeTips } from "../chrome/welcome-tips.js";
|
|
46
49
|
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
|
|
47
|
-
import {
|
|
50
|
+
import { imagePathCandidates, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
48
51
|
import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
|
|
49
52
|
const DOUBLE_CTRL_C_MS = 500;
|
|
50
53
|
const DOUBLE_ESCAPE_MS = 500;
|
|
51
54
|
const STREAMING_REVEAL_MS = 1000 / 30;
|
|
52
55
|
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1_000;
|
|
56
|
+
/**
|
|
57
|
+
* Durable settlement events that arrive in bursts while a turn runs. Renders
|
|
58
|
+
* for these are coalesced through the stream-render timer; rarer control
|
|
59
|
+
* events (command lifecycle, inbox splices, titles, notices) render at once.
|
|
60
|
+
*/
|
|
61
|
+
const BURST_EVENT_TYPES = new Set([
|
|
62
|
+
'step/start',
|
|
63
|
+
'step/end',
|
|
64
|
+
'tool/call',
|
|
65
|
+
'tool/result',
|
|
66
|
+
'assistant/message',
|
|
67
|
+
'assistant/attempt',
|
|
68
|
+
'llm/retry',
|
|
69
|
+
]);
|
|
53
70
|
function shortenPath(cwd) {
|
|
54
71
|
const home = homedir();
|
|
55
72
|
if (cwd === home)
|
|
@@ -73,6 +90,17 @@ function windowTitleText(title) {
|
|
|
73
90
|
return [...cleaned].slice(0, WINDOW_TITLE_MAX).join('');
|
|
74
91
|
}
|
|
75
92
|
export const name = 'omdsh-tui';
|
|
93
|
+
/**
|
|
94
|
+
* Row schema. The durable preferences ride as volatile fields, so a settings
|
|
95
|
+
* form edit commits into their references and reaches this fiber through
|
|
96
|
+
* `loader/volatile-update` without remounting the terminal provider.
|
|
97
|
+
*/
|
|
98
|
+
export const Config = z.object({
|
|
99
|
+
model: z.string(),
|
|
100
|
+
historyPath: z.string(),
|
|
101
|
+
keybindingsPath: z.string(),
|
|
102
|
+
...TUI_SETTINGS_FIELDS,
|
|
103
|
+
});
|
|
76
104
|
/**
|
|
77
105
|
* Local terminal presentation service.
|
|
78
106
|
*/
|
|
@@ -146,11 +174,11 @@ export class LocalTui {
|
|
|
146
174
|
#pasteBuf = '';
|
|
147
175
|
#pasteInFlight = 0;
|
|
148
176
|
#deferredPasteEvents = [];
|
|
149
|
-
#images =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
#
|
|
177
|
+
#images = new ComposerImages({
|
|
178
|
+
editor: this.#editor,
|
|
179
|
+
notice: text => { this.notice(text, { level: 'error' }); },
|
|
180
|
+
});
|
|
181
|
+
#plain;
|
|
154
182
|
#offData = null;
|
|
155
183
|
#offResize = null;
|
|
156
184
|
#pwd;
|
|
@@ -173,6 +201,12 @@ export class LocalTui {
|
|
|
173
201
|
#checkUpdates = true;
|
|
174
202
|
#startupChangelog = 'summary';
|
|
175
203
|
#notifications = new TerminalNotificationController();
|
|
204
|
+
/**
|
|
205
|
+
* Herdr lifecycle authority for this pane. The plugin entry injects the
|
|
206
|
+
* environment-aware reporter; a directly constructed instance is inert so
|
|
207
|
+
* tests running inside a Herdr pane cannot report against the live server.
|
|
208
|
+
*/
|
|
209
|
+
#herdr;
|
|
176
210
|
#notificationPolicy = 'off';
|
|
177
211
|
#notificationThreshold = '30s';
|
|
178
212
|
#statusBar = defaultStatusBarConfig();
|
|
@@ -220,7 +254,6 @@ export class LocalTui {
|
|
|
220
254
|
#searchFiles;
|
|
221
255
|
#searchFileMentions;
|
|
222
256
|
#searchSessions;
|
|
223
|
-
#validateImageDraft;
|
|
224
257
|
#autocompleteDebounceMs;
|
|
225
258
|
#persistPrefs = null;
|
|
226
259
|
#agentBehavior;
|
|
@@ -264,8 +297,15 @@ export class LocalTui {
|
|
|
264
297
|
this.#terminalProfile = paths.terminalProfile ?? detectTerminalProfile();
|
|
265
298
|
this.#resizeDebounceMs = Math.max(0, paths.resizeDebounceMs ?? 120);
|
|
266
299
|
this.#streamRenderMs = Math.max(0, paths.streamRenderMs ?? 8);
|
|
300
|
+
this.#herdr = paths.herdrReporter ?? new HerdrAgentReporter({ env: {} });
|
|
267
301
|
this.#syncTrueColor();
|
|
268
302
|
this.#tty = term.input.isTTY === true;
|
|
303
|
+
this.#plain = new PlainTui({
|
|
304
|
+
term,
|
|
305
|
+
blocks: () => this.#state.blocks,
|
|
306
|
+
promptRequest: () => this.#prompt?.request,
|
|
307
|
+
finishPrompt: answer => { this.#finishPrompt(answer); },
|
|
308
|
+
});
|
|
269
309
|
this.#pwd = shortenPath(project.root);
|
|
270
310
|
this.#branch = project.gitLabel;
|
|
271
311
|
this.#renderer = new MainScreenRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, {
|
|
@@ -301,6 +341,7 @@ export class LocalTui {
|
|
|
301
341
|
term.output.write('\x1b[?2004h');
|
|
302
342
|
}
|
|
303
343
|
this.#render();
|
|
344
|
+
this.#herdr.start();
|
|
304
345
|
}
|
|
305
346
|
/** Refresh Git workspace metadata after a turn; the footer shows realtime branch/dirty state. */
|
|
306
347
|
#refreshProjectContext() {
|
|
@@ -325,10 +366,18 @@ export class LocalTui {
|
|
|
325
366
|
this.#syncStreamingReveal(undefined);
|
|
326
367
|
this.#syncTick();
|
|
327
368
|
if (this.#tty) {
|
|
328
|
-
|
|
369
|
+
// Settlement events arrive in bursts (parallel tool calls, in-process
|
|
370
|
+
// subagent settlements); coalesce them like setSession. Control events
|
|
371
|
+
// (user echo, command lifecycle, inbox splices) stay immediate.
|
|
372
|
+
if (this.#streamRenderMs > 0 && this.#busy() && BURST_EVENT_TYPES.has(event.type)) {
|
|
373
|
+
this.#scheduleStreamRender();
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
this.#render();
|
|
377
|
+
}
|
|
329
378
|
}
|
|
330
379
|
else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
|
|
331
|
-
this.#
|
|
380
|
+
this.#plain.print();
|
|
332
381
|
}
|
|
333
382
|
}
|
|
334
383
|
/**
|
|
@@ -353,6 +402,10 @@ export class LocalTui {
|
|
|
353
402
|
}
|
|
354
403
|
}
|
|
355
404
|
setStatus(status) {
|
|
405
|
+
// Only the displayed top-level agent drives the Herdr pane state; an
|
|
406
|
+
// inspected child must not, and closing the inspector re-syncs the root.
|
|
407
|
+
if (this.#inspected === undefined)
|
|
408
|
+
this.#herdr.setRunning(status === 'running');
|
|
356
409
|
if (this.#state.status === 'compacting')
|
|
357
410
|
return;
|
|
358
411
|
this.#state = { ...this.#state, status };
|
|
@@ -463,7 +516,7 @@ export class LocalTui {
|
|
|
463
516
|
this.#render();
|
|
464
517
|
}
|
|
465
518
|
setImageValidator(validate) {
|
|
466
|
-
this.#
|
|
519
|
+
this.#images.setValidator(validate);
|
|
467
520
|
}
|
|
468
521
|
toolCallContext(callId) {
|
|
469
522
|
const block = this.#state.blocks.findLast(candidate => candidate.kind === 'tool' && String(candidate.callId) === callId);
|
|
@@ -501,14 +554,14 @@ export class LocalTui {
|
|
|
501
554
|
if (this.#tty)
|
|
502
555
|
this.#render();
|
|
503
556
|
else
|
|
504
|
-
this.#
|
|
557
|
+
this.#plain.print();
|
|
505
558
|
}
|
|
506
559
|
commandOutput(command, text) {
|
|
507
560
|
this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'commandOutput', command, text }] };
|
|
508
561
|
if (this.#tty)
|
|
509
562
|
this.#render();
|
|
510
563
|
else
|
|
511
|
-
this.#
|
|
564
|
+
this.#plain.print();
|
|
512
565
|
}
|
|
513
566
|
prompt(request) {
|
|
514
567
|
if (this.#prompt !== null)
|
|
@@ -516,6 +569,7 @@ export class LocalTui {
|
|
|
516
569
|
if (this.#disposed || request.signal?.aborted === true)
|
|
517
570
|
return Promise.resolve(null);
|
|
518
571
|
this.#emitNotification(this.#notifications.humanPrompt());
|
|
572
|
+
this.#herdr.prompt(request.title);
|
|
519
573
|
this.#editor.setText('');
|
|
520
574
|
this.#ac = null;
|
|
521
575
|
const displaced = this.#displaceSurface();
|
|
@@ -552,17 +606,18 @@ export class LocalTui {
|
|
|
552
606
|
this.#trajectory = null;
|
|
553
607
|
const state = replayEvents(events, presentations);
|
|
554
608
|
this.#state = { ...state, status, compactCommandId: undefined };
|
|
555
|
-
this.#
|
|
609
|
+
this.#plain.resetPrinted();
|
|
556
610
|
this.#followTail();
|
|
557
611
|
this.#deferInitialRender = false;
|
|
558
612
|
this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
|
|
559
613
|
if (this.#tty)
|
|
560
614
|
this.#render();
|
|
561
615
|
else
|
|
562
|
-
this.#
|
|
616
|
+
this.#plain.print();
|
|
563
617
|
}
|
|
564
618
|
setSession(info) {
|
|
565
619
|
this.#sessionId = info.id;
|
|
620
|
+
this.#herdr.setSession(info.id);
|
|
566
621
|
const title = info.title?.trim();
|
|
567
622
|
this.#sessionTitle = title === undefined || title === '' ? undefined : title;
|
|
568
623
|
this.#recentSessions = info.recent.map((session) => ({ ...session }));
|
|
@@ -602,32 +657,37 @@ export class LocalTui {
|
|
|
602
657
|
#syncTrueColor() {
|
|
603
658
|
this.#trueColor = this.#colors && detectTrueColor();
|
|
604
659
|
}
|
|
605
|
-
/** Apply prefs loaded from
|
|
660
|
+
/** Apply prefs loaded from this entry's config (does not persist). */
|
|
606
661
|
applyStoredPrefs(prefs) {
|
|
607
|
-
|
|
608
|
-
this.#themeName = prefs.theme;
|
|
609
|
-
this.#colors = prefs.colors;
|
|
610
|
-
this.#syncTrueColor();
|
|
611
|
-
this.#motion = prefs.motion ?? 'full';
|
|
612
|
-
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
613
|
-
this.#expandTools = prefs.expandTools;
|
|
614
|
-
this.#checkUpdates = prefs.checkUpdates ?? true;
|
|
615
|
-
this.#startupChangelog = prefs.startupChangelog ?? 'summary';
|
|
616
|
-
this.#notificationPolicy = prefs.notifications ?? 'off';
|
|
617
|
-
this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
|
|
618
|
-
this.#configureNotifications();
|
|
619
|
-
this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
|
|
620
|
-
this.#toolsExpanded = prefs.expandTools;
|
|
621
|
-
if (previousMotion !== this.#motion) {
|
|
622
|
-
this.#stopRevealTick();
|
|
623
|
-
this.#reveal = undefined;
|
|
624
|
-
}
|
|
625
|
-
this.#syncTick();
|
|
662
|
+
this.#applyPrefs(prefs, { persist: false, forceToolsSync: true });
|
|
626
663
|
if (this.#settings !== null)
|
|
627
664
|
this.#settings = { ...this.#settings, prefs };
|
|
628
665
|
if (this.#tty)
|
|
629
666
|
this.#render();
|
|
630
667
|
}
|
|
668
|
+
/** Currently applied preferences; sibling rows read these instead of the config references. */
|
|
669
|
+
prefs() {
|
|
670
|
+
return {
|
|
671
|
+
theme: this.#themeName,
|
|
672
|
+
colors: this.#colors,
|
|
673
|
+
motion: this.#motion,
|
|
674
|
+
terminalProgress: this.#terminalProgress,
|
|
675
|
+
expandTools: this.#expandTools,
|
|
676
|
+
checkUpdates: this.#checkUpdates,
|
|
677
|
+
startupChangelog: this.#startupChangelog,
|
|
678
|
+
notifications: this.#notificationPolicy,
|
|
679
|
+
notificationThreshold: this.#notificationThreshold,
|
|
680
|
+
statusBar: {
|
|
681
|
+
...this.#statusBar,
|
|
682
|
+
groups: [...this.#statusBar.groups],
|
|
683
|
+
...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
|
|
684
|
+
...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
|
|
685
|
+
...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
|
|
686
|
+
...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
|
|
687
|
+
...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
631
691
|
/** Called after a live `/settings` change. */
|
|
632
692
|
setPrefsPersist(persist) {
|
|
633
693
|
this.#persistPrefs = persist;
|
|
@@ -650,7 +710,7 @@ export class LocalTui {
|
|
|
650
710
|
this.#agentBehavior = undefined;
|
|
651
711
|
this.#agentBehaviorPending = false;
|
|
652
712
|
if (this.#settings !== null)
|
|
653
|
-
this.#settings = createSettings(this
|
|
713
|
+
this.#settings = createSettings(this.prefs());
|
|
654
714
|
if (this.#tty && !this.#disposed)
|
|
655
715
|
this.#render();
|
|
656
716
|
};
|
|
@@ -667,7 +727,7 @@ export class LocalTui {
|
|
|
667
727
|
return Promise.resolve(null);
|
|
668
728
|
}
|
|
669
729
|
if (!this.#tty)
|
|
670
|
-
return this.#
|
|
730
|
+
return this.#plain.readline(signal);
|
|
671
731
|
// Lines submitted while a turn was still running were queued instead of
|
|
672
732
|
// dropped; serve the oldest before waiting for fresh input.
|
|
673
733
|
const queued = this.#queuedSubmissions.shift();
|
|
@@ -695,16 +755,7 @@ export class LocalTui {
|
|
|
695
755
|
return (await this.readInput())?.text ?? null;
|
|
696
756
|
}
|
|
697
757
|
restoreInput(submission) {
|
|
698
|
-
|
|
699
|
-
const currentImages = this.#images;
|
|
700
|
-
let rebasedCurrent = currentText;
|
|
701
|
-
for (let index = currentImages.length - 1; index >= 0; index -= 1) {
|
|
702
|
-
const image = currentImages[index];
|
|
703
|
-
rebasedCurrent = rebasedCurrent.replaceAll(imageMarker(index, image), imageMarker(index + submission.images.length, image));
|
|
704
|
-
}
|
|
705
|
-
const separator = submission.text !== '' && rebasedCurrent !== '' ? '\n' : '';
|
|
706
|
-
this.#images = [...submission.images.map(image => ({ ...image })), ...currentImages];
|
|
707
|
-
this.#editor.setText(submission.text + separator + rebasedCurrent);
|
|
758
|
+
this.#images.restore(submission);
|
|
708
759
|
this.#refreshAutocomplete();
|
|
709
760
|
if (this.#tty)
|
|
710
761
|
this.#render();
|
|
@@ -714,14 +765,14 @@ export class LocalTui {
|
|
|
714
765
|
return;
|
|
715
766
|
this.#queueEditPending = false;
|
|
716
767
|
if (submission === null) {
|
|
717
|
-
if (this.#editor.text === '' && this.#images.
|
|
768
|
+
if (this.#editor.text === '' && this.#images.count === 0 && this.#queueEditNewer?.length === 0) {
|
|
718
769
|
this.#queueEditNewer = null;
|
|
719
770
|
}
|
|
720
771
|
return;
|
|
721
772
|
}
|
|
722
773
|
if (this.#queueEditNewer === null)
|
|
723
774
|
this.#queueEditNewer = [];
|
|
724
|
-
if (this.#editor.text !== '' || this.#images.
|
|
775
|
+
if (this.#editor.text !== '' || this.#images.count > 0) {
|
|
725
776
|
this.#queueEditNewer.unshift(this.#currentSubmission());
|
|
726
777
|
}
|
|
727
778
|
this.#replaceInput(submission);
|
|
@@ -729,12 +780,11 @@ export class LocalTui {
|
|
|
729
780
|
#currentSubmission() {
|
|
730
781
|
return {
|
|
731
782
|
text: this.#editor.text,
|
|
732
|
-
images: this.#images.
|
|
783
|
+
images: this.#images.copies(),
|
|
733
784
|
};
|
|
734
785
|
}
|
|
735
786
|
#replaceInput(submission) {
|
|
736
|
-
this.#images
|
|
737
|
-
this.#editor.setText(submission.text);
|
|
787
|
+
this.#images.replace(submission);
|
|
738
788
|
this.#historyIndex = 0;
|
|
739
789
|
this.#refreshAutocomplete();
|
|
740
790
|
if (this.#tty)
|
|
@@ -814,11 +864,12 @@ export class LocalTui {
|
|
|
814
864
|
this.#autocompleteAbort?.abort();
|
|
815
865
|
this.#autocompleteTimer = null;
|
|
816
866
|
this.#autocompleteAbort = null;
|
|
817
|
-
this.#
|
|
867
|
+
this.#plain.dispose();
|
|
818
868
|
this.#offAgentBehaviorWatch?.();
|
|
819
869
|
this.#offAgentBehaviorWatch = undefined;
|
|
820
870
|
this.#settlePending(null);
|
|
821
871
|
this.#finishPrompt(null);
|
|
872
|
+
this.#herdr.release();
|
|
822
873
|
}
|
|
823
874
|
/** Settle one pending read, detaching its abort listener. */
|
|
824
875
|
#settlePending(submission) {
|
|
@@ -833,96 +884,6 @@ export class LocalTui {
|
|
|
833
884
|
refresh() {
|
|
834
885
|
this.#render();
|
|
835
886
|
}
|
|
836
|
-
#readlinePlain(signal) {
|
|
837
|
-
return new Promise((resolve) => {
|
|
838
|
-
if (this.#lineReader === null) {
|
|
839
|
-
this.#lineReader = createInterface({ input: this.#term.input });
|
|
840
|
-
// Permanent listeners: once() handlers would auto-pause the input
|
|
841
|
-
// stream after one line and miss the EOF close.
|
|
842
|
-
this.#lineReader.on('line', (line) => { this.#plainResolve(line); });
|
|
843
|
-
this.#lineReader.on('close', () => {
|
|
844
|
-
this.#plainClosed = true;
|
|
845
|
-
this.#plainResolve(null);
|
|
846
|
-
});
|
|
847
|
-
}
|
|
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); };
|
|
859
|
-
}
|
|
860
|
-
this.#pumpPlain();
|
|
861
|
-
});
|
|
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
|
-
*/
|
|
868
|
-
#plainResolve(line) {
|
|
869
|
-
if (this.#prompt !== null && line !== null) {
|
|
870
|
-
const value = line.trim();
|
|
871
|
-
if (value === '') {
|
|
872
|
-
this.#finishPrompt(null);
|
|
873
|
-
}
|
|
874
|
-
else if (this.#prompt.request.allowCustom === false) {
|
|
875
|
-
const options = this.#prompt.request.options ?? [];
|
|
876
|
-
const numeric = /^\d+$/u.test(value) ? Number(value) - 1 : -1;
|
|
877
|
-
const option = numeric >= 0
|
|
878
|
-
? options[numeric]
|
|
879
|
-
: options.find(item => item.label.toLowerCase() === value.toLowerCase());
|
|
880
|
-
this.#finishPrompt(option?.value ?? option?.label ?? null);
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
this.#finishPrompt(value);
|
|
884
|
-
}
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (this.#prompt !== null)
|
|
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() {
|
|
895
|
-
const pending = this.#plainPending;
|
|
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
|
-
}
|
|
910
|
-
}
|
|
911
|
-
/** Print plain-mode blocks that settled since the last flush. */
|
|
912
|
-
#printPlain() {
|
|
913
|
-
const theme = createTheme(false, false);
|
|
914
|
-
const width = this.#term.width();
|
|
915
|
-
const fresh = this.#state.blocks.slice(this.#plainPrinted);
|
|
916
|
-
let out = '';
|
|
917
|
-
for (const block of fresh) {
|
|
918
|
-
// Pipe / CI output is not a viewport: print the full tool body.
|
|
919
|
-
for (const line of blockLines(block, theme, width, 0, true))
|
|
920
|
-
out += line + '\n';
|
|
921
|
-
}
|
|
922
|
-
this.#plainPrinted = this.#state.blocks.length;
|
|
923
|
-
if (out !== '')
|
|
924
|
-
this.#term.output.write(out);
|
|
925
|
-
}
|
|
926
887
|
#busy() {
|
|
927
888
|
if (this.#state.status !== 'idle')
|
|
928
889
|
return true;
|
|
@@ -1048,7 +1009,7 @@ export class LocalTui {
|
|
|
1048
1009
|
...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
|
|
1049
1010
|
input: this.#editor.text,
|
|
1050
1011
|
inputCursor: this.#editor.cursor,
|
|
1051
|
-
inputImages: this.#images.
|
|
1012
|
+
inputImages: this.#images.count,
|
|
1052
1013
|
queuedSubmissions: this.#queueEditNewer === null
|
|
1053
1014
|
? this.#queuedSubmissions
|
|
1054
1015
|
: [...this.#queuedSubmissions, ...this.#queueEditNewer],
|
|
@@ -1143,7 +1104,6 @@ export class LocalTui {
|
|
|
1143
1104
|
this.#scrollStart = this.#maxStart;
|
|
1144
1105
|
}
|
|
1145
1106
|
#utf8 = new StringDecoder('utf8');
|
|
1146
|
-
#plainQueue = [];
|
|
1147
1107
|
#onData(chunk) {
|
|
1148
1108
|
// Decode bytes across the whole stream so a multi-byte UTF-8 character
|
|
1149
1109
|
// split between data events is never corrupted into replacement chars.
|
|
@@ -1204,8 +1164,8 @@ export class LocalTui {
|
|
|
1204
1164
|
if (images.every((image) => image !== null)) {
|
|
1205
1165
|
let inserted = false;
|
|
1206
1166
|
for (const image of images) {
|
|
1207
|
-
if (await this.#
|
|
1208
|
-
this.#
|
|
1167
|
+
if (await this.#images.admit(image)) {
|
|
1168
|
+
this.#images.insert(image);
|
|
1209
1169
|
inserted = true;
|
|
1210
1170
|
}
|
|
1211
1171
|
}
|
|
@@ -1220,7 +1180,7 @@ export class LocalTui {
|
|
|
1220
1180
|
// image instead of leaking that stale path into the prompt.
|
|
1221
1181
|
const clipboardImage = await this.#readClipboardImage();
|
|
1222
1182
|
if (clipboardImage !== null) {
|
|
1223
|
-
this.#
|
|
1183
|
+
this.#images.insert(clipboardImage);
|
|
1224
1184
|
this.#refreshAutocomplete();
|
|
1225
1185
|
this.#render();
|
|
1226
1186
|
return;
|
|
@@ -1234,8 +1194,8 @@ export class LocalTui {
|
|
|
1234
1194
|
if (this.#search === null && this.#settings === null && this.#copySelector === null) {
|
|
1235
1195
|
const image = await this.#readClipboardImage();
|
|
1236
1196
|
if (image !== null) {
|
|
1237
|
-
if (await this.#
|
|
1238
|
-
this.#
|
|
1197
|
+
if (await this.#images.admit(image)) {
|
|
1198
|
+
this.#images.insert(image);
|
|
1239
1199
|
this.#refreshAutocomplete();
|
|
1240
1200
|
this.#render();
|
|
1241
1201
|
}
|
|
@@ -1249,8 +1209,8 @@ export class LocalTui {
|
|
|
1249
1209
|
if (images.length > 0) {
|
|
1250
1210
|
let inserted = false;
|
|
1251
1211
|
for (const candidate of images) {
|
|
1252
|
-
if (await this.#
|
|
1253
|
-
this.#
|
|
1212
|
+
if (await this.#images.admit(candidate)) {
|
|
1213
|
+
this.#images.insert(candidate);
|
|
1254
1214
|
inserted = true;
|
|
1255
1215
|
}
|
|
1256
1216
|
}
|
|
@@ -1266,98 +1226,6 @@ export class LocalTui {
|
|
|
1266
1226
|
if (text !== '')
|
|
1267
1227
|
await this.#acceptPastedText(text);
|
|
1268
1228
|
}
|
|
1269
|
-
/**
|
|
1270
|
-
* Run the Harness image-admission check for one paste candidate. A refusal
|
|
1271
|
-
* becomes an error notice and skips the draft, instead of failing the whole
|
|
1272
|
-
* submission after the user has typed a prompt around it.
|
|
1273
|
-
*/
|
|
1274
|
-
async #admitImage(image) {
|
|
1275
|
-
if (this.#validateImageDraft === undefined)
|
|
1276
|
-
return true;
|
|
1277
|
-
try {
|
|
1278
|
-
await this.#validateImageDraft(image);
|
|
1279
|
-
return true;
|
|
1280
|
-
}
|
|
1281
|
-
catch (error) {
|
|
1282
|
-
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
1283
|
-
return false;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
#insertImageDraft(input) {
|
|
1287
|
-
const size = input.width === undefined || input.height === undefined
|
|
1288
|
-
? probeImageDimensions(input.data, input.mediaType)
|
|
1289
|
-
: undefined;
|
|
1290
|
-
const image = {
|
|
1291
|
-
...input,
|
|
1292
|
-
...(input.width === undefined && size !== undefined ? { width: size.width } : {}),
|
|
1293
|
-
...(input.height === undefined && size !== undefined ? { height: size.height } : {}),
|
|
1294
|
-
};
|
|
1295
|
-
const marker = imageMarker(this.#images.length, image);
|
|
1296
|
-
const before = this.#editor.cursor > 0 && !/\s/u.test(this.#editor.text[this.#editor.cursor - 1] ?? '') ? ' ' : '';
|
|
1297
|
-
const after = this.#editor.cursor >= this.#editor.text.length || !/\s/u.test(this.#editor.text[this.#editor.cursor] ?? '')
|
|
1298
|
-
? ' '
|
|
1299
|
-
: '';
|
|
1300
|
-
this.#images.push(image);
|
|
1301
|
-
this.#editor.handle({ type: 'text', value: before + marker + after });
|
|
1302
|
-
}
|
|
1303
|
-
#removeImageAtCursor(key) {
|
|
1304
|
-
const cursor = this.#editor.cursor;
|
|
1305
|
-
for (let index = 0; index < this.#images.length; index += 1) {
|
|
1306
|
-
const image = this.#images[index];
|
|
1307
|
-
const marker = imageMarker(index, image);
|
|
1308
|
-
const start = this.#editor.text.indexOf(marker);
|
|
1309
|
-
if (start < 0)
|
|
1310
|
-
continue;
|
|
1311
|
-
let from = start;
|
|
1312
|
-
let to = start + marker.length;
|
|
1313
|
-
const touches = key === 'backspace'
|
|
1314
|
-
? cursor > start && cursor <= to
|
|
1315
|
-
: cursor >= start && cursor < to;
|
|
1316
|
-
if (!touches)
|
|
1317
|
-
continue;
|
|
1318
|
-
if (this.#editor.text[to] === ' ')
|
|
1319
|
-
to += 1;
|
|
1320
|
-
else if (from > 0 && this.#editor.text[from - 1] === ' ')
|
|
1321
|
-
from -= 1;
|
|
1322
|
-
const oldImages = this.#images;
|
|
1323
|
-
let text = this.#editor.text.slice(0, from) + this.#editor.text.slice(to);
|
|
1324
|
-
const nextImages = oldImages.filter((_, oldIndex) => oldIndex !== index);
|
|
1325
|
-
let nextIndex = 0;
|
|
1326
|
-
for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
|
|
1327
|
-
if (oldIndex === index)
|
|
1328
|
-
continue;
|
|
1329
|
-
const remaining = oldImages[oldIndex];
|
|
1330
|
-
text = text.replaceAll(imageMarker(oldIndex, remaining), imageMarker(nextIndex, remaining));
|
|
1331
|
-
nextIndex += 1;
|
|
1332
|
-
}
|
|
1333
|
-
this.#images = nextImages;
|
|
1334
|
-
this.#editor.setText(text, Math.min(from, text.length));
|
|
1335
|
-
this.#refreshAutocomplete();
|
|
1336
|
-
this.#render();
|
|
1337
|
-
return true;
|
|
1338
|
-
}
|
|
1339
|
-
return false;
|
|
1340
|
-
}
|
|
1341
|
-
#reconcileImageDrafts() {
|
|
1342
|
-
if (this.#images.length === 0)
|
|
1343
|
-
return;
|
|
1344
|
-
const oldImages = this.#images;
|
|
1345
|
-
const retained = oldImages.filter((image, index) => this.#editor.text.includes(imageMarker(index, image)));
|
|
1346
|
-
if (retained.length === oldImages.length)
|
|
1347
|
-
return;
|
|
1348
|
-
let text = this.#editor.text;
|
|
1349
|
-
let nextIndex = 0;
|
|
1350
|
-
for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
|
|
1351
|
-
const image = oldImages[oldIndex];
|
|
1352
|
-
const oldMarker = imageMarker(oldIndex, image);
|
|
1353
|
-
if (!text.includes(oldMarker))
|
|
1354
|
-
continue;
|
|
1355
|
-
text = text.replaceAll(oldMarker, imageMarker(nextIndex, image));
|
|
1356
|
-
nextIndex += 1;
|
|
1357
|
-
}
|
|
1358
|
-
this.#images = retained;
|
|
1359
|
-
this.#editor.setText(text, Math.min(this.#editor.cursor, text.length));
|
|
1360
|
-
}
|
|
1361
1229
|
#dispatch(event) {
|
|
1362
1230
|
if (this.#state.status === 'compacting') {
|
|
1363
1231
|
if (event.type === 'key' && event.id === 'pageUp') {
|
|
@@ -1483,7 +1351,7 @@ export class LocalTui {
|
|
|
1483
1351
|
}
|
|
1484
1352
|
else {
|
|
1485
1353
|
this.#editor.clear();
|
|
1486
|
-
this.#images
|
|
1354
|
+
this.#images.clear();
|
|
1487
1355
|
this.#historyIndex = 0;
|
|
1488
1356
|
this.#ac = null;
|
|
1489
1357
|
this.#render();
|
|
@@ -1511,9 +1379,9 @@ export class LocalTui {
|
|
|
1511
1379
|
return;
|
|
1512
1380
|
if (event.type === 'key' && event.id === 'escape') {
|
|
1513
1381
|
if (this.#inspected !== undefined) {
|
|
1514
|
-
if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.
|
|
1382
|
+
if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.count > 0)) {
|
|
1515
1383
|
this.#editor.clear();
|
|
1516
|
-
this.#images
|
|
1384
|
+
this.#images.clear();
|
|
1517
1385
|
this.#ac = null;
|
|
1518
1386
|
this.#render();
|
|
1519
1387
|
return;
|
|
@@ -1522,7 +1390,7 @@ export class LocalTui {
|
|
|
1522
1390
|
return;
|
|
1523
1391
|
}
|
|
1524
1392
|
if (this.#state.status !== 'idle' || this.#pending === null
|
|
1525
|
-
|| this.#editor.text !== '' || this.#images.
|
|
1393
|
+
|| this.#editor.text !== '' || this.#images.count > 0) {
|
|
1526
1394
|
this.#lastEscapeTime = 0;
|
|
1527
1395
|
}
|
|
1528
1396
|
else {
|
|
@@ -1539,8 +1407,11 @@ export class LocalTui {
|
|
|
1539
1407
|
}
|
|
1540
1408
|
}
|
|
1541
1409
|
if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
|
|
1542
|
-
&& this.#
|
|
1410
|
+
&& this.#images.removeAtCursor(event.id)) {
|
|
1411
|
+
this.#refreshAutocomplete();
|
|
1412
|
+
this.#render();
|
|
1543
1413
|
return;
|
|
1414
|
+
}
|
|
1544
1415
|
this.#applyCommand(this.#editor.handle(event));
|
|
1545
1416
|
}
|
|
1546
1417
|
#handlePrompt(event) {
|
|
@@ -1751,7 +1622,7 @@ export class LocalTui {
|
|
|
1751
1622
|
}
|
|
1752
1623
|
if (!hasAgents || event.type !== 'key' || event.id !== 'down')
|
|
1753
1624
|
return false;
|
|
1754
|
-
if (this.#editor.text !== '' || this.#images.
|
|
1625
|
+
if (this.#editor.text !== '' || this.#images.count > 0 || this.#historyIndex !== 0 || this.#ac !== null)
|
|
1755
1626
|
return false;
|
|
1756
1627
|
if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
|
|
1757
1628
|
return false;
|
|
@@ -1784,34 +1655,12 @@ export class LocalTui {
|
|
|
1784
1655
|
this.#agentHub = createAgentHub({ agents });
|
|
1785
1656
|
this.#render();
|
|
1786
1657
|
}
|
|
1787
|
-
#prefs
|
|
1788
|
-
|
|
1789
|
-
theme: this.#themeName,
|
|
1790
|
-
colors: this.#colors,
|
|
1791
|
-
motion: this.#motion,
|
|
1792
|
-
terminalProgress: this.#terminalProgress,
|
|
1793
|
-
expandTools: this.#expandTools,
|
|
1794
|
-
checkUpdates: this.#checkUpdates,
|
|
1795
|
-
startupChangelog: this.#startupChangelog,
|
|
1796
|
-
notifications: this.#notificationPolicy,
|
|
1797
|
-
notificationThreshold: this.#notificationThreshold,
|
|
1798
|
-
statusBar: {
|
|
1799
|
-
...this.#statusBar,
|
|
1800
|
-
groups: [...this.#statusBar.groups],
|
|
1801
|
-
...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
|
|
1802
|
-
...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
|
|
1803
|
-
...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
|
|
1804
|
-
...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
|
|
1805
|
-
...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
|
|
1806
|
-
},
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
#applyPrefs(prefs) {
|
|
1658
|
+
#applyPrefs(prefs, options) {
|
|
1659
|
+
const previousMotion = this.#motion;
|
|
1810
1660
|
const expandChanged = prefs.expandTools !== this.#expandTools;
|
|
1811
1661
|
this.#themeName = prefs.theme;
|
|
1812
1662
|
this.#colors = prefs.colors;
|
|
1813
1663
|
this.#syncTrueColor();
|
|
1814
|
-
const previousMotion = this.#motion;
|
|
1815
1664
|
this.#motion = prefs.motion ?? 'full';
|
|
1816
1665
|
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
1817
1666
|
this.#expandTools = prefs.expandTools;
|
|
@@ -1821,14 +1670,15 @@ export class LocalTui {
|
|
|
1821
1670
|
this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
|
|
1822
1671
|
this.#configureNotifications();
|
|
1823
1672
|
this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
|
|
1824
|
-
if (expandChanged)
|
|
1673
|
+
if (options.forceToolsSync || expandChanged)
|
|
1825
1674
|
this.#toolsExpanded = prefs.expandTools;
|
|
1826
1675
|
if (previousMotion !== this.#motion) {
|
|
1827
1676
|
this.#stopRevealTick();
|
|
1828
1677
|
this.#reveal = undefined;
|
|
1829
1678
|
}
|
|
1830
1679
|
this.#syncTick();
|
|
1831
|
-
|
|
1680
|
+
if (options.persist)
|
|
1681
|
+
this.#persistPrefs?.(prefs);
|
|
1832
1682
|
}
|
|
1833
1683
|
#applyStoredAgentBehavior(next) {
|
|
1834
1684
|
this.#agentBehavior = { language: next.language };
|
|
@@ -1878,7 +1728,7 @@ export class LocalTui {
|
|
|
1878
1728
|
return;
|
|
1879
1729
|
}
|
|
1880
1730
|
this.#settings = command.state;
|
|
1881
|
-
this.#applyPrefs(command.state.prefs);
|
|
1731
|
+
this.#applyPrefs(command.state.prefs, { persist: true, forceToolsSync: false });
|
|
1882
1732
|
this.#render();
|
|
1883
1733
|
return;
|
|
1884
1734
|
}
|
|
@@ -2090,7 +1940,7 @@ export class LocalTui {
|
|
|
2090
1940
|
}
|
|
2091
1941
|
#applyCommand(command) {
|
|
2092
1942
|
if (command.kind === 'changed') {
|
|
2093
|
-
this.#
|
|
1943
|
+
this.#images.reconcile();
|
|
2094
1944
|
if (command.edited === true)
|
|
2095
1945
|
this.#historyIndex = 0;
|
|
2096
1946
|
this.#refreshAutocomplete();
|
|
@@ -2125,7 +1975,7 @@ export class LocalTui {
|
|
|
2125
1975
|
}
|
|
2126
1976
|
if (command.kind === 'clear') {
|
|
2127
1977
|
this.#editor.clear();
|
|
2128
|
-
this.#images
|
|
1978
|
+
this.#images.clear();
|
|
2129
1979
|
this.#queueEditNewer = null;
|
|
2130
1980
|
this.#queueEditPending = false;
|
|
2131
1981
|
this.#historyIndex = 0;
|
|
@@ -2153,7 +2003,7 @@ export class LocalTui {
|
|
|
2153
2003
|
}
|
|
2154
2004
|
}
|
|
2155
2005
|
#historyPrev() {
|
|
2156
|
-
if (this.#images.
|
|
2006
|
+
if (this.#images.count > 0)
|
|
2157
2007
|
return;
|
|
2158
2008
|
if (this.#history.length === 0 || this.#historyIndex >= this.#history.length)
|
|
2159
2009
|
return;
|
|
@@ -2182,7 +2032,7 @@ export class LocalTui {
|
|
|
2182
2032
|
listener();
|
|
2183
2033
|
return true;
|
|
2184
2034
|
}
|
|
2185
|
-
if (this.#editor.text !== '' || this.#images.
|
|
2035
|
+
if (this.#editor.text !== '' || this.#images.count > 0 || this.#historyIndex !== 0)
|
|
2186
2036
|
return false;
|
|
2187
2037
|
const submission = this.#queuedSubmissions.pop();
|
|
2188
2038
|
if (submission !== undefined) {
|
|
@@ -2200,7 +2050,7 @@ export class LocalTui {
|
|
|
2200
2050
|
return true;
|
|
2201
2051
|
}
|
|
2202
2052
|
#historyNext() {
|
|
2203
|
-
if (this.#images.
|
|
2053
|
+
if (this.#images.count > 0)
|
|
2204
2054
|
return;
|
|
2205
2055
|
if (this.#historyIndex === 0)
|
|
2206
2056
|
return;
|
|
@@ -2228,7 +2078,7 @@ export class LocalTui {
|
|
|
2228
2078
|
this.#render();
|
|
2229
2079
|
return;
|
|
2230
2080
|
}
|
|
2231
|
-
const images = this.#images.
|
|
2081
|
+
const images = this.#images.copies();
|
|
2232
2082
|
const submittedText = images.length > 0 ? text.trim() : text;
|
|
2233
2083
|
const queueEditNewer = this.#queueEditNewer;
|
|
2234
2084
|
const historyText = stripComposerImageMarkers(submittedText, images)
|
|
@@ -2243,7 +2093,7 @@ export class LocalTui {
|
|
|
2243
2093
|
this.#queueEditNewer = null;
|
|
2244
2094
|
this.#queueEditPending = false;
|
|
2245
2095
|
this.#editor.setText('');
|
|
2246
|
-
this.#images
|
|
2096
|
+
this.#images.clear();
|
|
2247
2097
|
this.#ac = null;
|
|
2248
2098
|
this.#search = null;
|
|
2249
2099
|
this.#settings = null;
|
|
@@ -2279,7 +2129,7 @@ export class LocalTui {
|
|
|
2279
2129
|
this.#render();
|
|
2280
2130
|
}
|
|
2281
2131
|
#submitInspect(text) {
|
|
2282
|
-
const images = this.#images.
|
|
2132
|
+
const images = this.#images.copies();
|
|
2283
2133
|
const submittedText = images.length > 0 ? text.trim() : text;
|
|
2284
2134
|
const historyText = stripComposerImageMarkers(submittedText, images)
|
|
2285
2135
|
.replace(/[ \t]{2,}/gu, ' ')
|
|
@@ -2291,7 +2141,7 @@ export class LocalTui {
|
|
|
2291
2141
|
this.#historyIndex = 0;
|
|
2292
2142
|
this.#draft = '';
|
|
2293
2143
|
this.#editor.setText('');
|
|
2294
|
-
this.#images
|
|
2144
|
+
this.#images.clear();
|
|
2295
2145
|
this.#ac = null;
|
|
2296
2146
|
this.#search = null;
|
|
2297
2147
|
this.#followTail();
|
|
@@ -2410,6 +2260,7 @@ export class LocalTui {
|
|
|
2410
2260
|
if (pending === null)
|
|
2411
2261
|
return;
|
|
2412
2262
|
this.#prompt = null;
|
|
2263
|
+
this.#herdr.promptResolved();
|
|
2413
2264
|
pending.offAbort?.();
|
|
2414
2265
|
pending.resolve(answer);
|
|
2415
2266
|
this.#restoreDisplacedSurface();
|
|
@@ -2486,7 +2337,7 @@ export class LocalTui {
|
|
|
2486
2337
|
}
|
|
2487
2338
|
this.#search = null;
|
|
2488
2339
|
this.#ac = null;
|
|
2489
|
-
this.#settings = createSettings(this
|
|
2340
|
+
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior);
|
|
2490
2341
|
this.#render();
|
|
2491
2342
|
}
|
|
2492
2343
|
#notice(text) {
|
|
@@ -2559,7 +2410,7 @@ export class LocalTui {
|
|
|
2559
2410
|
return true;
|
|
2560
2411
|
}
|
|
2561
2412
|
if (action === 'search-history') {
|
|
2562
|
-
if (this.#images.
|
|
2413
|
+
if (this.#images.count > 0)
|
|
2563
2414
|
return false;
|
|
2564
2415
|
this.#search = createHistorySearch(this.#history);
|
|
2565
2416
|
this.#ac = null;
|
|
@@ -2571,7 +2422,7 @@ export class LocalTui {
|
|
|
2571
2422
|
// transcript there; a non-empty draft keeps the editor's own binding.
|
|
2572
2423
|
if (this.#search !== null)
|
|
2573
2424
|
return false;
|
|
2574
|
-
if (this.#editor.text !== '' || this.#images.
|
|
2425
|
+
if (this.#editor.text !== '' || this.#images.count > 0)
|
|
2575
2426
|
return false;
|
|
2576
2427
|
this.#transcriptSearch = createTranscriptSearch();
|
|
2577
2428
|
this.#ac = null;
|
|
@@ -2615,7 +2466,7 @@ export class LocalTui {
|
|
|
2615
2466
|
this.#term.input.setRawMode?.(false);
|
|
2616
2467
|
const text = editExternally(this.#editor.text);
|
|
2617
2468
|
this.#editor.setText(text);
|
|
2618
|
-
this.#
|
|
2469
|
+
this.#images.reconcile();
|
|
2619
2470
|
}
|
|
2620
2471
|
catch (error) {
|
|
2621
2472
|
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
@@ -2683,20 +2534,51 @@ export function apply(ctx, config) {
|
|
|
2683
2534
|
return () => { process.stdout.removeListener('resize', listener); };
|
|
2684
2535
|
},
|
|
2685
2536
|
};
|
|
2686
|
-
const tui = new LocalTui(term, config.model, config.colors,
|
|
2537
|
+
const tui = new LocalTui(term, config.model, config.colors.get(), config.theme.get(), copyToClipboard, {
|
|
2687
2538
|
deferInitialRender: true,
|
|
2688
2539
|
terminalProfile,
|
|
2689
2540
|
alternateScreenOverlays: terminalProfile === 'direct',
|
|
2541
|
+
herdrReporter: new HerdrAgentReporter(),
|
|
2690
2542
|
historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
|
|
2691
2543
|
keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
|
|
2692
2544
|
});
|
|
2693
2545
|
ctx.provide(TUI_SERVICE, tui);
|
|
2694
2546
|
ctx.effect(() => () => { tui.dispose(); });
|
|
2547
|
+
// Preferences are this entry's own volatile config: the initial read applies
|
|
2548
|
+
// them without persisting, a committed form edit dispatches a volatile update
|
|
2549
|
+
// to this fiber only, and an in-app `/settings` change writes back through the
|
|
2550
|
+
// settings service, which persists the edit into the active profile patch.
|
|
2551
|
+
const storedPrefs = () => {
|
|
2552
|
+
const prefs = {
|
|
2553
|
+
theme: config.theme.get(),
|
|
2554
|
+
colors: resolveColors(config.colors.get(), term.output.isTTY === true),
|
|
2555
|
+
motion: config.motion.get(),
|
|
2556
|
+
terminalProgress: config.terminalProgress.get(),
|
|
2557
|
+
expandTools: config.expandTools.get(),
|
|
2558
|
+
checkUpdates: config.checkUpdates.get(),
|
|
2559
|
+
startupChangelog: config.startupChangelog.get(),
|
|
2560
|
+
notifications: config.notifications.get(),
|
|
2561
|
+
notificationThreshold: config.notificationThreshold.get(),
|
|
2562
|
+
};
|
|
2563
|
+
const statusBar = config.statusBar.get();
|
|
2564
|
+
// The reference holds a frozen snapshot; the provider keeps a mutable copy.
|
|
2565
|
+
if (statusBar !== undefined)
|
|
2566
|
+
prefs.statusBar = structuredClone(statusBar);
|
|
2567
|
+
const statusPreset = config.statusPreset.get();
|
|
2568
|
+
if (statusPreset !== undefined)
|
|
2569
|
+
prefs.statusPreset = statusPreset;
|
|
2570
|
+
return prefs;
|
|
2571
|
+
};
|
|
2572
|
+
tui.applyStoredPrefs(storedPrefs());
|
|
2573
|
+
ctx.on('loader/volatile-update', () => { tui.applyStoredPrefs(storedPrefs()); });
|
|
2574
|
+
// Persistence needs the settings service; without one the provider still runs
|
|
2575
|
+
// and an in-app change stays session-local, as it did before.
|
|
2695
2576
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2577
|
+
tui.setPrefsPersist((prefs) => {
|
|
2578
|
+
const ops = Object.entries(prefs).map(([key, value]) => ({ op: 'set', path: [key], value }));
|
|
2579
|
+
void settingsCtx.settings.mutate(TUI_SETTINGS_ENTRY, ops)
|
|
2580
|
+
.catch((error) => { settingsCtx.logger.warn(error); });
|
|
2581
|
+
});
|
|
2700
2582
|
});
|
|
2701
2583
|
}
|
|
2702
2584
|
//# sourceMappingURL=provider-local.js.map
|