@sagmans/dsh-tui 0.3.0 → 0.4.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/README.md +158 -6
- package/cordis.patch.yml +14 -0
- package/lib/agent/projections.d.ts +22 -6
- package/lib/agent/projections.d.ts.map +1 -1
- package/lib/agent/projections.js +21 -10
- package/lib/agent/projections.js.map +1 -1
- package/lib/agent/prompt-history.d.ts +89 -0
- package/lib/agent/prompt-history.d.ts.map +1 -0
- package/lib/agent/prompt-history.js +380 -0
- package/lib/agent/prompt-history.js.map +1 -0
- package/lib/agent/status.d.ts +6 -0
- package/lib/agent/status.d.ts.map +1 -1
- package/lib/agent/status.js +1 -0
- package/lib/agent/status.js.map +1 -1
- package/lib/herdr/client.d.ts +57 -0
- package/lib/herdr/client.d.ts.map +1 -0
- package/lib/herdr/client.js +218 -0
- package/lib/herdr/client.js.map +1 -0
- package/lib/herdr/constants.d.ts +93 -0
- package/lib/herdr/constants.d.ts.map +1 -0
- package/lib/herdr/constants.js +79 -0
- package/lib/herdr/constants.js.map +1 -0
- package/lib/herdr/reporter.d.ts +60 -0
- package/lib/herdr/reporter.d.ts.map +1 -0
- package/lib/herdr/reporter.js +217 -0
- package/lib/herdr/reporter.js.map +1 -0
- package/lib/herdr/state.d.ts +57 -0
- package/lib/herdr/state.d.ts.map +1 -0
- package/lib/herdr/state.js +62 -0
- package/lib/herdr/state.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +252 -20
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +1 -1
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +3 -0
- package/lib/input/actions.js.map +1 -1
- package/lib/input/ghost.d.ts +46 -0
- package/lib/input/ghost.d.ts.map +1 -0
- package/lib/input/ghost.js +58 -0
- package/lib/input/ghost.js.map +1 -0
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +2 -0
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/submission.d.ts +24 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +28 -1
- package/lib/input/submission.js.map +1 -1
- package/lib/stash/lock.d.ts +72 -0
- package/lib/stash/lock.d.ts.map +1 -0
- package/lib/stash/lock.js +374 -0
- package/lib/stash/lock.js.map +1 -0
- package/lib/stash/paths.d.ts +22 -0
- package/lib/stash/paths.d.ts.map +1 -0
- package/lib/stash/paths.js +83 -0
- package/lib/stash/paths.js.map +1 -0
- package/lib/stash/private-fs.d.ts +61 -0
- package/lib/stash/private-fs.d.ts.map +1 -0
- package/lib/stash/private-fs.js +379 -0
- package/lib/stash/private-fs.js.map +1 -0
- package/lib/stash/schema.d.ts +57 -0
- package/lib/stash/schema.d.ts.map +1 -0
- package/lib/stash/schema.js +127 -0
- package/lib/stash/schema.js.map +1 -0
- package/lib/stash/store.d.ts +92 -0
- package/lib/stash/store.d.ts.map +1 -0
- package/lib/stash/store.js +262 -0
- package/lib/stash/store.js.map +1 -0
- package/lib/stash.d.ts +91 -0
- package/lib/stash.d.ts.map +1 -0
- package/lib/stash.js +296 -0
- package/lib/stash.js.map +1 -0
- package/lib/theme-settings.d.ts +37 -8
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +60 -3
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +1 -1
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +8 -0
- package/lib/theme-tokens.js.map +1 -1
- package/lib/todo-guard.d.ts +116 -0
- package/lib/todo-guard.d.ts.map +1 -0
- package/lib/todo-guard.js +259 -0
- package/lib/todo-guard.js.map +1 -0
- package/lib/ui/editor.d.ts +41 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +98 -4
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/history-picker.d.ts +15 -0
- package/lib/ui/history-picker.d.ts.map +1 -0
- package/lib/ui/history-picker.js +26 -0
- package/lib/ui/history-picker.js.map +1 -0
- package/lib/ui/picker.d.ts +11 -1
- package/lib/ui/picker.d.ts.map +1 -1
- package/lib/ui/picker.js +17 -2
- package/lib/ui/picker.js.map +1 -1
- package/lib/ui/prompt.d.ts +2 -0
- package/lib/ui/prompt.d.ts.map +1 -1
- package/lib/ui/prompt.js +4 -0
- package/lib/ui/prompt.js.map +1 -1
- package/lib/ui/stash-picker.d.ts +43 -0
- package/lib/ui/stash-picker.d.ts.map +1 -0
- package/lib/ui/stash-picker.js +78 -0
- package/lib/ui/stash-picker.js.map +1 -0
- package/lib/ui/status.d.ts +2 -0
- package/lib/ui/status.d.ts.map +1 -1
- package/lib/ui/status.js +7 -0
- package/lib/ui/status.js.map +1 -1
- package/lib/work.d.ts +3 -1
- package/lib/work.d.ts.map +1 -1
- package/lib/work.js +9 -1
- package/lib/work.js.map +1 -1
- package/package.json +5 -1
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import { ProcessTerminal, ScrollView, VStack, isKeyRelease, matchesKey } from '@earendil-works/pi-tui';
|
|
5
5
|
import { startAgent } from "./agent/host.js";
|
|
6
6
|
import { PICKER_LIMIT, createSessionHistory, presetOfStoredSession, readSessionTitle, } from "./agent/history.js";
|
|
7
|
+
import { createPromptHistory } from "./agent/prompt-history.js";
|
|
7
8
|
import { createPresetRoster, parsePresetArgument } from "./agent/presets.js";
|
|
8
9
|
import { createToolPresenter } from "./agent/present.js";
|
|
9
10
|
import { forkPoint } from "./agent/fork.js";
|
|
@@ -15,6 +16,7 @@ import { SubagentRoster, createSubagentControl, describeSubagents, parseSubagent
|
|
|
15
16
|
import { describeMissingOptional, describeMissingRequired, probeComposition } from "./compat/probe.js";
|
|
16
17
|
import { ApprovalGate, QuestionGate, toGateQuestions } from "./gates.js";
|
|
17
18
|
import { createCompletionProvider } from "./input/completion.js";
|
|
19
|
+
import { ghostSuffix } from "./input/ghost.js";
|
|
18
20
|
import { LOCAL_COMMANDS, classifySubmission } from "./input/submission.js";
|
|
19
21
|
import { createDeferredNotice } from "./settings-notice.js";
|
|
20
22
|
import { ChordReader, DEFAULT_PREFIX_KEYS, DEFAULT_PREFIX_WINDOW_S, chordBindings, chordKeysLine, installKeybindings, surfaceKeysLine, } from "./input/keymap.js";
|
|
@@ -26,6 +28,8 @@ import { WarningSafeTui } from "./terminal/warning-screen.js";
|
|
|
26
28
|
import { BELL, shouldRingBell } from "./terminal/bell.js";
|
|
27
29
|
import { clipboardSequence } from "./terminal/clipboard.js";
|
|
28
30
|
import { CLEAR_TITLE, windowTitle } from "./terminal/title.js";
|
|
31
|
+
import { sessionStartReason } from "./herdr/state.js";
|
|
32
|
+
import { createHerdrReporter } from "./herdr/reporter.js";
|
|
29
33
|
import { defaultExportFile, transcriptToText } from "./export.js";
|
|
30
34
|
import { createTheme, forwardEditorTheme, forwardMarkdownTheme } from "./theme.js";
|
|
31
35
|
import { detectColourMode } from "./theme-capability.js";
|
|
@@ -33,19 +37,23 @@ import { defaultSettings, readScope, settingsProblemMessage, toOverrides, TUI_SE
|
|
|
33
37
|
import { pendingPrompts } from "./queue.js";
|
|
34
38
|
import { renderThemeTable } from "./theme-command.js";
|
|
35
39
|
import { KEYMAP_LAYERS, keymapLayer, renderKeymap } from "./keys-command.js";
|
|
40
|
+
import { resetSequence } from "./theme-tokens.js";
|
|
36
41
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
37
42
|
import { formatTokens } from "./tokens.js";
|
|
38
43
|
import { TranscriptModel } from "./transcript.js";
|
|
39
44
|
import { WorkFold, describeTodos, planSelectedActive, planToggleLine, readPlanState } from "./work.js";
|
|
40
45
|
import { WorkDock } from "./ui/dock.js";
|
|
41
46
|
import { GateInputBar } from "./ui/gate-input.js";
|
|
47
|
+
import { HistoryPicker } from "./ui/history-picker.js";
|
|
42
48
|
import { PromptBar } from "./ui/prompt.js";
|
|
43
49
|
import { MarkdownRenderer } from "./ui/markdown.js";
|
|
44
50
|
import { createMermaidTransform } from "./ui/mermaid.js";
|
|
45
51
|
import { EffortPicker, ModelPicker, PROVIDER_DEFAULT_EFFORT_ID, PresetPicker, SessionPicker, effortChoices, } from "./ui/picker.js";
|
|
46
52
|
import { QueueBar } from "./ui/queue.js";
|
|
47
|
-
import { StatusBar } from "./ui/status.js";
|
|
53
|
+
import { shortPath, StatusBar } from "./ui/status.js";
|
|
48
54
|
import { DEFAULT_VIEW_STATE, TranscriptView } from "./ui/view.js";
|
|
55
|
+
import { PromptStash } from "./stash.js";
|
|
56
|
+
import { confirmedClear, StashConfirmPicker, StashPicker } from "./ui/stash-picker.js";
|
|
49
57
|
export const name = 'tui';
|
|
50
58
|
/**
|
|
51
59
|
* Services the surface cannot run without.
|
|
@@ -65,6 +73,8 @@ const backHint = (map) => `${hintKeys(map, 'surface.back') || BACK_HINT_FALLBACK
|
|
|
65
73
|
const TITLE_CONCURRENCY = 4;
|
|
66
74
|
/** How often the running-state clock repaints while a turn is open. */
|
|
67
75
|
const STATUS_TICK_MS = 1000;
|
|
76
|
+
/** Reverse video for the cell the cursor occupies, so a ghost keeps the cursor visible. */
|
|
77
|
+
const GHOST_CURSOR_PREFIX = '\u001b[7m';
|
|
68
78
|
function asRecord(value) {
|
|
69
79
|
return typeof value === 'object' && value !== null ? value : undefined;
|
|
70
80
|
}
|
|
@@ -162,6 +172,10 @@ export function apply(ctx, config) {
|
|
|
162
172
|
let prefixWindowMs = DEFAULT_PREFIX_WINDOW_S * MS_PER_SECOND;
|
|
163
173
|
/** Every action's keys in force; the settings document owns it and a press reads it live. */
|
|
164
174
|
let keymap = defaultKeymap();
|
|
175
|
+
/** Whether prompts are recorded and offered, and the cap on how many; the settings document owns all three. */
|
|
176
|
+
let historyEnabled = defaultSettings().history.enabled;
|
|
177
|
+
let historyGhost = defaultSettings().history.ghost;
|
|
178
|
+
let historyMaxEntries = defaultSettings().history.maxEntries;
|
|
165
179
|
/**
|
|
166
180
|
* The chord between a prefix and the action that follows it.
|
|
167
181
|
*
|
|
@@ -187,6 +201,9 @@ export function apply(ctx, config) {
|
|
|
187
201
|
// Installed where the library reads it, so a remap lands on the next press
|
|
188
202
|
// rather than at the next restart.
|
|
189
203
|
installKeybindings(keymap);
|
|
204
|
+
historyEnabled = section.history.enabled;
|
|
205
|
+
historyGhost = section.history.ghost;
|
|
206
|
+
historyMaxEntries = section.history.maxEntries;
|
|
190
207
|
// A chord armed under the keymap the reader just replaced is not their chord.
|
|
191
208
|
keyChord.disarm();
|
|
192
209
|
};
|
|
@@ -219,7 +236,10 @@ export function apply(ctx, config) {
|
|
|
219
236
|
scope = settingsCtx.settings.register(TUI_SETTINGS_NAMESPACE, TuiSettingsSchema);
|
|
220
237
|
}
|
|
221
238
|
catch (error) {
|
|
222
|
-
|
|
239
|
+
// Nothing registered means nothing to read, so the reader's switch cannot
|
|
240
|
+
// be confirmed: recording stays off rather than falling back to on.
|
|
241
|
+
historyEnabled = false;
|
|
242
|
+
settingsNotice.post(settingsProblemMessage(error) + ' · prompt history stays off until the section parses');
|
|
223
243
|
return;
|
|
224
244
|
}
|
|
225
245
|
readSection = () => readScope(scope, message => settingsNotice.post(message));
|
|
@@ -235,6 +255,33 @@ export function apply(ctx, config) {
|
|
|
235
255
|
*/
|
|
236
256
|
let presentScope;
|
|
237
257
|
const model = new TranscriptModel(createToolPresenter(ctx, () => presentScope));
|
|
258
|
+
/**
|
|
259
|
+
* The reader's prompt history: global across projects, recorded from every
|
|
260
|
+
* submitted line, and offered back as they type. Built before the bar so its
|
|
261
|
+
* first load cannot race the first suggestion.
|
|
262
|
+
*/
|
|
263
|
+
const promptHistory = createPromptHistory({
|
|
264
|
+
cap: () => historyMaxEntries,
|
|
265
|
+
warn: message => {
|
|
266
|
+
model.notice(message);
|
|
267
|
+
tui.requestRender();
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
/**
|
|
271
|
+
* The dimmed completion drawn from recorded prompts.
|
|
272
|
+
*
|
|
273
|
+
* Colour is the affordance: with styling off the suggestion would be
|
|
274
|
+
* unreadable text the reader could still accept, which is worse than none.
|
|
275
|
+
* The cursor cell is also reversed so the cursor stays visible on the ghost.
|
|
276
|
+
*/
|
|
277
|
+
const ghostBrush = {
|
|
278
|
+
enabled: () => historyEnabled && historyGhost && theme.color && theme.visible('editor.ghost'),
|
|
279
|
+
suggestion: input => ghostSuffix({ entries: promptHistory.entries(), ...input }),
|
|
280
|
+
paint: (text, cell) => {
|
|
281
|
+
const styled = theme.style('editor.ghost', text);
|
|
282
|
+
return cell === 'cursor' ? GHOST_CURSOR_PREFIX + styled + resetSequence() : styled;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
238
285
|
const work = new WorkFold();
|
|
239
286
|
const modelSwitch = new ModelSwitch();
|
|
240
287
|
const agentPresets = createPresetRoster(ctx);
|
|
@@ -285,7 +332,7 @@ export function apply(ctx, config) {
|
|
|
285
332
|
// send the library submits on by default. A settings document read after this
|
|
286
333
|
// point installs over it, which is why the bar reads the map per press.
|
|
287
334
|
installKeybindings(keymap);
|
|
288
|
-
const editor = new GateInputBar(tui, theme.editor, () => keymap);
|
|
335
|
+
const editor = new GateInputBar(tui, theme.editor, () => keymap, ghostBrush);
|
|
289
336
|
// Answers are written in the reader's own editor, which is why a question
|
|
290
337
|
// borrows the bar instead of drawing a second one beside it.
|
|
291
338
|
const promptBar = new PromptBar(editor);
|
|
@@ -306,6 +353,9 @@ export function apply(ctx, config) {
|
|
|
306
353
|
jobs = agent === undefined || jobDirectory === undefined ? [] : jobDirectory.list(agent.agent);
|
|
307
354
|
tui.requestRender();
|
|
308
355
|
};
|
|
356
|
+
// The bank is built once the picker exists to answer for it, so the status
|
|
357
|
+
// source is late-bound: the footer must not read a half-constructed stash.
|
|
358
|
+
let stash;
|
|
309
359
|
const statusFacts = createStatusFacts(ctx, {
|
|
310
360
|
sessionId: () => activeSession,
|
|
311
361
|
activity: () => ({ running: turnOpen, startedAt: turnStartedAt }),
|
|
@@ -316,6 +366,7 @@ export function apply(ctx, config) {
|
|
|
316
366
|
// a hint stored with the transcript would keep naming the key of the day it
|
|
317
367
|
// was written, and the reader may remap it with the row already on screen.
|
|
318
368
|
back: () => (viewedSession === activeSession ? undefined : backHint(keymap)),
|
|
369
|
+
stash: () => stash?.entryCount,
|
|
319
370
|
});
|
|
320
371
|
const statusBar = new StatusBar(statusFacts, theme);
|
|
321
372
|
const dock = new WorkDock(() => work.state(), theme, () => jobs, () => roster.list());
|
|
@@ -330,11 +381,28 @@ export function apply(ctx, config) {
|
|
|
330
381
|
disposers.push(() => clearInterval(statusTicker));
|
|
331
382
|
// A window that outlived the surface would repaint a screen that is gone.
|
|
332
383
|
disposers.push(() => keyChord.disarm());
|
|
384
|
+
// A containing Herdr is told what this pane is doing; away from one the
|
|
385
|
+
// reporter is inert, so the surface never depends on being multiplexed.
|
|
386
|
+
const herdr = createHerdrReporter();
|
|
387
|
+
// Kept out of the disposal list on purpose: the row is handed back before
|
|
388
|
+
// this stops guarding it, so a host that leaves during the release still
|
|
389
|
+
// releases synchronously.
|
|
390
|
+
const unregisterExit = herdr.registerExitRelease();
|
|
333
391
|
restore.add(() => tui.stop());
|
|
334
392
|
ctx.effect(() => () => {
|
|
393
|
+
// The pane stops being an agent before the process that claimed it unwinds:
|
|
394
|
+
// a release that ran after the reports were unregistered would race them,
|
|
395
|
+
// and one that never ran would leave a row that reads as a live agent. The
|
|
396
|
+
// reports already on the wire are settled first, because Herdr ignores a
|
|
397
|
+
// release for a pane nothing has claimed yet — the report that followed it
|
|
398
|
+
// would otherwise claim the row back. The promise is returned so a host that
|
|
399
|
+
// waits for teardown waits for the row too, and the exit listener outlives
|
|
400
|
+
// the wait, so one that does not still hands the row back synchronously.
|
|
401
|
+
const released = herdr.release().catch(() => undefined);
|
|
335
402
|
restore.restore();
|
|
336
403
|
for (const dispose of disposers.reverse())
|
|
337
404
|
dispose();
|
|
405
|
+
return released.finally(unregisterExit);
|
|
338
406
|
});
|
|
339
407
|
tui.setLayoutRoot(new VStack([
|
|
340
408
|
{
|
|
@@ -359,26 +427,37 @@ export function apply(ctx, config) {
|
|
|
359
427
|
return;
|
|
360
428
|
exited = true;
|
|
361
429
|
clearInterval(statusTicker);
|
|
362
|
-
//
|
|
363
|
-
//
|
|
430
|
+
// The screen goes back at once, so leaving feels like leaving; the row goes
|
|
431
|
+
// back behind it. Reports already on the wire are settled first, because
|
|
432
|
+
// Herdr ignores the release of a pane nothing has claimed yet — the report
|
|
433
|
+
// that followed it would otherwise claim the row back during shutdown.
|
|
364
434
|
terminal.write(CLEAR_TITLE);
|
|
365
435
|
restore.restore();
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
436
|
+
void herdr
|
|
437
|
+
.release()
|
|
438
|
+
// Nobody is left to report a release that failed on the way out, and a
|
|
439
|
+
// row that could not be cleared is not a reason to keep the process.
|
|
440
|
+
.catch(() => undefined)
|
|
441
|
+
.finally(() => {
|
|
442
|
+
// Everything below is written after the release: a failure nobody can
|
|
443
|
+
// read is not a failure that was reported.
|
|
444
|
+
if (reason !== undefined)
|
|
445
|
+
terminal.write(`\ndsh-tui: ${reason}\n`);
|
|
446
|
+
// The hint is computed here rather than read from the context because
|
|
447
|
+
// only the surface knows which session it is leaving: a fork or a
|
|
448
|
+
// switch moves it. A run that opened nothing has nothing to offer back:
|
|
449
|
+
// the identity it was launched with names no log, so pointing at it
|
|
450
|
+
// would send the reader to a conversation that does not exist.
|
|
451
|
+
if (sessionOpened)
|
|
452
|
+
terminal.write(`\n${resumeHint(String(activeSession), PROFILE_NAME)}\n`);
|
|
453
|
+
appExit(code);
|
|
454
|
+
});
|
|
379
455
|
};
|
|
380
456
|
const openGate = (next) => {
|
|
381
457
|
pending = next;
|
|
458
|
+
// The card's own title names the decision, which is what a reader glancing
|
|
459
|
+
// at a wall of panes needs in order to know which one to open.
|
|
460
|
+
herdr.block(next.gate.card().title);
|
|
382
461
|
// A gate owns the keyboard: the editor must not collect the decision keys.
|
|
383
462
|
editor.disableSubmit = true;
|
|
384
463
|
tui.setFocus(null);
|
|
@@ -390,6 +469,7 @@ export function apply(ctx, config) {
|
|
|
390
469
|
};
|
|
391
470
|
const closeGate = () => {
|
|
392
471
|
pending = undefined;
|
|
472
|
+
herdr.unblock();
|
|
393
473
|
editor.disableSubmit = false;
|
|
394
474
|
// The question is answered or skipped, so the reader gets their prompt back
|
|
395
475
|
// in the bar they left it in.
|
|
@@ -400,7 +480,7 @@ export function apply(ctx, config) {
|
|
|
400
480
|
/**
|
|
401
481
|
* What each key the surface answers itself does; false hands the press back.
|
|
402
482
|
*
|
|
403
|
-
* Keyed by the table's own ids, so a key added to {@link
|
|
483
|
+
* Keyed by the table's own ids, so a key added to {@link SURFACE_ACTIONS}
|
|
404
484
|
* without a handler here fails to compile rather than doing nothing.
|
|
405
485
|
*/
|
|
406
486
|
const surfaceActions = {
|
|
@@ -423,6 +503,10 @@ export function apply(ctx, config) {
|
|
|
423
503
|
void openEffortPicker();
|
|
424
504
|
return true;
|
|
425
505
|
},
|
|
506
|
+
history: () => {
|
|
507
|
+
void openHistoryPicker();
|
|
508
|
+
return true;
|
|
509
|
+
},
|
|
426
510
|
back: () => {
|
|
427
511
|
if (viewedSession !== activeSession)
|
|
428
512
|
void showAgentSession();
|
|
@@ -564,6 +648,7 @@ export function apply(ctx, config) {
|
|
|
564
648
|
const settlePicker = (id) => {
|
|
565
649
|
const settle = pendingPicker?.settle;
|
|
566
650
|
pendingPicker = undefined;
|
|
651
|
+
herdr.unblock();
|
|
567
652
|
editor.disableSubmit = false;
|
|
568
653
|
tui.setFocus(editor);
|
|
569
654
|
settle?.(id);
|
|
@@ -590,10 +675,39 @@ export function apply(ctx, config) {
|
|
|
590
675
|
/** Take the keyboard for a picker and answer with the id it settled on. */
|
|
591
676
|
const openPicker = (picker, vet) => new Promise(resolve => {
|
|
592
677
|
pendingPicker = { picker, settle: resolve, vet };
|
|
678
|
+
// A picker owns the keyboard exactly as a gate does: nothing moves until
|
|
679
|
+
// the reader chooses, so it is the same kind of wait.
|
|
680
|
+
herdr.block(picker.card().title);
|
|
593
681
|
editor.disableSubmit = true;
|
|
594
682
|
tui.setFocus(null);
|
|
595
683
|
tui.requestRender();
|
|
596
684
|
});
|
|
685
|
+
/**
|
|
686
|
+
* Reverse search over recorded prompts, seeded with whatever is in the bar.
|
|
687
|
+
*
|
|
688
|
+
* A pick replaces the draft; a cancel leaves it exactly as it was, because the
|
|
689
|
+
* list was opened to look rather than to lose what is already typed.
|
|
690
|
+
*/
|
|
691
|
+
const openHistoryPicker = async () => {
|
|
692
|
+
if (!historyEnabled) {
|
|
693
|
+
model.notice('prompt history is disabled in ' + TUI_SETTINGS_NAMESPACE + ' settings');
|
|
694
|
+
tui.requestRender();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (promptHistory.entries().length === 0) {
|
|
698
|
+
const blocked = promptHistory.blockedReason();
|
|
699
|
+
model.notice(blocked === undefined ? 'no prompt history yet' : 'prompt history is unavailable: ' + blocked);
|
|
700
|
+
tui.requestRender();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
// The expanded text is what the reader wrote; a large paste sits in the bar
|
|
704
|
+
// as a marker, and seeding with it would filter out the prompt it came from.
|
|
705
|
+
const picked = await openPicker(new HistoryPicker(() => promptHistory.entries(), editor.getExpandedText(), () => keymap));
|
|
706
|
+
if (picked === undefined)
|
|
707
|
+
return;
|
|
708
|
+
editor.setText(picked);
|
|
709
|
+
tui.requestRender();
|
|
710
|
+
};
|
|
597
711
|
/** The roster as the picker paints it, refreshed when the picker opens. */
|
|
598
712
|
let presetRows = [];
|
|
599
713
|
/** Choose the mode a session that has not started yet will run. */
|
|
@@ -603,6 +717,28 @@ export function apply(ctx, config) {
|
|
|
603
717
|
presetRows = await agentPresets.list();
|
|
604
718
|
return await openPicker(new PresetPicker(() => presetRows, () => currentId, () => keymap));
|
|
605
719
|
};
|
|
720
|
+
/**
|
|
721
|
+
* The prompt bank for this working directory.
|
|
722
|
+
*
|
|
723
|
+
* The surface owns the editor, the picker, and the notices, so the bank is
|
|
724
|
+
* handed the few things it needs to reach them and nothing else: the commands
|
|
725
|
+
* stay free of terminal state and are exercised without one in the tests.
|
|
726
|
+
*/
|
|
727
|
+
const stashCwdLabel = shortPath(process.cwd(), process.env.HOME);
|
|
728
|
+
stash = new PromptStash({
|
|
729
|
+
getEditorText: () => editor.getExpandedText(),
|
|
730
|
+
setEditorText: text => {
|
|
731
|
+
editor.setText(text);
|
|
732
|
+
tui.requestRender();
|
|
733
|
+
},
|
|
734
|
+
// A question answers in this editor, so a draft written into a borrowed bar
|
|
735
|
+
// would become somebody's answer instead of a parked prompt.
|
|
736
|
+
editorIsAvailable: () => !promptBar.isBorrowed(),
|
|
737
|
+
notice: message => model.notice(message),
|
|
738
|
+
pick: (entries, label) => openPicker(new StashPicker(entries, label, () => keymap)),
|
|
739
|
+
confirm: async (count) => confirmedClear(await openPicker(new StashConfirmPicker(count, () => keymap))),
|
|
740
|
+
render: () => tui.requestRender(),
|
|
741
|
+
}, { cwd: process.cwd() });
|
|
606
742
|
/** Title the listed sessions without making the reader wait for the slowest log. */
|
|
607
743
|
const loadTitles = async (history, sessions, titles) => {
|
|
608
744
|
const queue = [...sessions];
|
|
@@ -812,6 +948,13 @@ export function apply(ctx, config) {
|
|
|
812
948
|
activeSession = id;
|
|
813
949
|
viewedSession = id;
|
|
814
950
|
agent = handle;
|
|
951
|
+
// The agent's own id is reported rather than the requested one: a resume can
|
|
952
|
+
// be answered by the session the log actually holds.
|
|
953
|
+
herdr.session({
|
|
954
|
+
id: String(handle.sessionId),
|
|
955
|
+
cwd: process.cwd(),
|
|
956
|
+
reason: sessionStartReason({ forked: fork !== undefined, resumed: resume }),
|
|
957
|
+
});
|
|
815
958
|
// A session with no history to fold still has to present its first live
|
|
816
959
|
// card through the right scope, so the scope is set before any event can.
|
|
817
960
|
presentScope = handle.agent;
|
|
@@ -1433,6 +1576,47 @@ export function apply(ctx, config) {
|
|
|
1433
1576
|
...(facts.effort === undefined ? {} : { reasoningEffort: facts.effort }),
|
|
1434
1577
|
});
|
|
1435
1578
|
};
|
|
1579
|
+
/** Show where history is kept, or forget it; the file is global to this machine. */
|
|
1580
|
+
const runHistoryCommand = (argument) => {
|
|
1581
|
+
// The line that asked for this is recorded before the command runs, but that
|
|
1582
|
+
// write rides the store's queue; waiting for it lets the count describe the
|
|
1583
|
+
// file the reader has, not the state before their own line landed.
|
|
1584
|
+
void promptHistory.flush().then(() => {
|
|
1585
|
+
if (argument === '') {
|
|
1586
|
+
const count = promptHistory.entries().length;
|
|
1587
|
+
const blocked = promptHistory.blockedReason();
|
|
1588
|
+
model.notice([
|
|
1589
|
+
count + (count === 1 ? ' prompt recorded' : ' prompts recorded'),
|
|
1590
|
+
promptHistory.path(),
|
|
1591
|
+
blocked === undefined ? undefined : 'writes disabled: ' + blocked,
|
|
1592
|
+
].filter(part => part !== undefined).join(' · '));
|
|
1593
|
+
tui.requestRender();
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (argument !== 'clear') {
|
|
1597
|
+
model.notice('usage: /history shows where history is kept · /history clear forgets every prompt');
|
|
1598
|
+
tui.requestRender();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
// A refused write cannot remove anything, so saying "forgot 0 prompts"
|
|
1602
|
+
// would describe a successful clear the file never had.
|
|
1603
|
+
const blocked = promptHistory.blockedReason();
|
|
1604
|
+
if (blocked !== undefined) {
|
|
1605
|
+
model.notice('prompt history is unavailable: ' + blocked);
|
|
1606
|
+
tui.requestRender();
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
return promptHistory.clear().then(removed => {
|
|
1610
|
+
model.notice('forgot ' + removed + (removed === 1 ? ' prompt' : ' prompts'));
|
|
1611
|
+
tui.requestRender();
|
|
1612
|
+
});
|
|
1613
|
+
}).catch(error => {
|
|
1614
|
+
// The store keeps what the file still holds, so the reader is told the
|
|
1615
|
+
// clear failed rather than being shown a count that never landed.
|
|
1616
|
+
model.notice('could not clear prompt history: ' + (error instanceof Error ? error.message : String(error)));
|
|
1617
|
+
tui.requestRender();
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1436
1620
|
/**
|
|
1437
1621
|
* Carry out one classified line, wherever it was asked for.
|
|
1438
1622
|
*
|
|
@@ -1493,6 +1677,36 @@ export function apply(ctx, config) {
|
|
|
1493
1677
|
case 'copy':
|
|
1494
1678
|
runCopyCommand();
|
|
1495
1679
|
return;
|
|
1680
|
+
case 'history':
|
|
1681
|
+
runHistoryCommand(submission.argument);
|
|
1682
|
+
return;
|
|
1683
|
+
case 'stash':
|
|
1684
|
+
// Submitting a command consumes the line it was typed on, so this path
|
|
1685
|
+
// can only carry a draft it was given; parking the bar's own draft is
|
|
1686
|
+
// what the chord is for.
|
|
1687
|
+
if (submission.argument.trim() === '')
|
|
1688
|
+
model.notice('usage: /stash <draft>, or ctrl+x then s to park the editor');
|
|
1689
|
+
else
|
|
1690
|
+
void stash?.stashEditor(submission.argument);
|
|
1691
|
+
return;
|
|
1692
|
+
case 'stash-draft':
|
|
1693
|
+
void stash?.stashEditor();
|
|
1694
|
+
return;
|
|
1695
|
+
case 'stash-pop':
|
|
1696
|
+
void stash?.pop(submission.selector);
|
|
1697
|
+
return;
|
|
1698
|
+
case 'stash-apply':
|
|
1699
|
+
void stash?.apply(submission.selector);
|
|
1700
|
+
return;
|
|
1701
|
+
case 'stash-list':
|
|
1702
|
+
void stash?.list(stashCwdLabel);
|
|
1703
|
+
return;
|
|
1704
|
+
case 'stash-drop':
|
|
1705
|
+
void stash?.drop(submission.selector);
|
|
1706
|
+
return;
|
|
1707
|
+
case 'stash-clear':
|
|
1708
|
+
void stash?.clear();
|
|
1709
|
+
return;
|
|
1496
1710
|
case 'status': {
|
|
1497
1711
|
const facts = statusFacts();
|
|
1498
1712
|
const context = facts.contextTokens === undefined
|
|
@@ -1553,7 +1767,17 @@ export function apply(ctx, config) {
|
|
|
1553
1767
|
}
|
|
1554
1768
|
}
|
|
1555
1769
|
};
|
|
1556
|
-
editor.onSubmit = text =>
|
|
1770
|
+
editor.onSubmit = text => {
|
|
1771
|
+
const submission = classifySubmission(text);
|
|
1772
|
+
if (submission.kind === 'empty')
|
|
1773
|
+
return;
|
|
1774
|
+
// The library's own history feeds the up/down keys; the store below feeds
|
|
1775
|
+
// ghost completion and reverse search, and is global rather than per-session.
|
|
1776
|
+
editor.addToHistory(text);
|
|
1777
|
+
if (historyEnabled)
|
|
1778
|
+
promptHistory.record(text);
|
|
1779
|
+
runSubmission(submission);
|
|
1780
|
+
};
|
|
1557
1781
|
disposers.push(ctx.on('session/event', (session, event) => {
|
|
1558
1782
|
// The surface state — activity, timer, title, bell, job board — belongs to
|
|
1559
1783
|
// the agent this terminal drives, even while a child is on screen.
|
|
@@ -1562,12 +1786,14 @@ export function apply(ctx, config) {
|
|
|
1562
1786
|
turnOpen = true;
|
|
1563
1787
|
turnStartedAt = Date.now();
|
|
1564
1788
|
terminal.write(windowTitle(process.cwd(), 'working'));
|
|
1789
|
+
herdr.working();
|
|
1565
1790
|
}
|
|
1566
1791
|
if (event.type === 'turn/end') {
|
|
1567
1792
|
const ranFor = turnStartedAt === undefined ? 0 : Date.now() - turnStartedAt;
|
|
1568
1793
|
turnOpen = false;
|
|
1569
1794
|
turnStartedAt = undefined;
|
|
1570
1795
|
terminal.write(windowTitle(process.cwd(), 'ready'));
|
|
1796
|
+
herdr.idle();
|
|
1571
1797
|
if (shouldRingBell({ bell: resolved.bell, ranForMs: ranFor, exiting: exited }))
|
|
1572
1798
|
terminal.write(BELL);
|
|
1573
1799
|
// A job the turn started may have settled while the reader was watching
|
|
@@ -1742,7 +1968,13 @@ export function apply(ctx, config) {
|
|
|
1742
1968
|
if (!resolved.resumePicker)
|
|
1743
1969
|
await presetFor(resolved.sessionId, resolved.resume, undefined);
|
|
1744
1970
|
tui.start();
|
|
1971
|
+
// Loaded now so the footer's count is real from the first paint, and because
|
|
1972
|
+
// a bank that cannot be read should say so before a reader trusts it.
|
|
1973
|
+
void stash?.open();
|
|
1745
1974
|
terminal.write(windowTitle(process.cwd(), 'ready'));
|
|
1975
|
+
// Claiming the pane's agent row does not wait for a session: the pane is
|
|
1976
|
+
// already on screen and already idle, and a session may still be chosen.
|
|
1977
|
+
herdr.publish();
|
|
1746
1978
|
// A refused settings edit is only visible now that the surface owns the
|
|
1747
1979
|
// screen; whatever the scope found before this point prints here instead.
|
|
1748
1980
|
settingsNotice.open(message => model.notice(message));
|