@aragon-agent/cli 0.6.24 → 0.6.26
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/CHANGELOG.md +1924 -1786
- package/README.md +2730 -2562
- package/dist/agent/controller.d.ts +49 -3
- package/dist/agent/controller.js +78 -3
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +49 -14
- package/dist/cli.js +214 -2
- package/dist/commands/branch-command.d.ts +61 -0
- package/dist/commands/branch-command.js +117 -0
- package/dist/commands/builtins.js +62 -40
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +48 -1
- package/dist/commands/session-command.d.ts +54 -0
- package/dist/commands/session-command.js +109 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/model-profiles.d.ts +8 -0
- package/dist/config/model-profiles.js +10 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +155 -0
- package/dist/proc/terminal-plan.js +360 -0
- package/dist/session/branch-invocation.d.ts +83 -0
- package/dist/session/branch-invocation.js +109 -0
- package/dist/session/branch-launch.d.ts +70 -0
- package/dist/session/branch-launch.js +248 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/session/cli-commands.d.ts +2 -0
- package/dist/session/cli-commands.js +36 -5
- package/dist/session/identity.d.ts +72 -0
- package/dist/session/identity.js +148 -0
- package/dist/session/limits.d.ts +66 -0
- package/dist/session/limits.js +66 -0
- package/dist/session/persist.d.ts +30 -5
- package/dist/session/startup-session.d.ts +107 -0
- package/dist/session/startup-session.js +315 -0
- package/dist/session/store.d.ts +60 -1
- package/dist/session/store.js +95 -6
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +154 -28
- package/dist/ui/Header.js +8 -16
- package/dist/ui/StatusBar.d.ts +8 -0
- package/dist/ui/StatusBar.js +12 -3
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/layout/header-hints.d.ts +68 -0
- package/dist/ui/layout/header-hints.js +138 -0
- package/dist/ui/layout/session-chip.d.ts +52 -0
- package/dist/ui/layout/session-chip.js +64 -0
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +8 -1
- package/dist/ui/overlays/HelpOverlay.js +17 -5
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -68
- package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
- package/dist/ui/overlays/SettingsScreen.js +34 -2
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +9 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -42,6 +42,11 @@ import { registerExecCommand } from './exec/cli-commands.js';
|
|
|
42
42
|
import { registerSessionsCommand } from './session/cli-commands.js';
|
|
43
43
|
import { registerDoctorCommand, registerInfoCommand } from './diagnostics/cli-commands.js';
|
|
44
44
|
import { App } from './ui/App.js';
|
|
45
|
+
import { readSessionView, publishSessionView } from './ui/session-bridge.js';
|
|
46
|
+
import { SessionCheckpointWriter } from './session/checkpoint.js';
|
|
47
|
+
import { resolveStartupSession, rotateToChildOf } from './session/startup-session.js';
|
|
48
|
+
import { createBranchLauncher } from './session/branch-launch.js';
|
|
49
|
+
import { EMPTY_USAGE_TOTAL } from './agent/reducer.js';
|
|
45
50
|
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
46
51
|
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
47
52
|
import { setFrameStatsProvider } from './commands/perf.js';
|
|
@@ -200,6 +205,13 @@ function toFlags(opts) {
|
|
|
200
205
|
// reports nothing, does nothing, and leaves compaction armed.
|
|
201
206
|
compaction: opts.compaction,
|
|
202
207
|
compactionThreshold: opts.compactionThreshold,
|
|
208
|
+
// Site three of three for `--resume` / `--save` -- ELEVEN features, one trap
|
|
209
|
+
// (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4, retry, fast, update,
|
|
210
|
+
// compaction, now session identity). A flag needs the `.option()`
|
|
211
|
+
// declaration below, a field on `CliFlags`, AND a line here; miss this one
|
|
212
|
+
// and `aragon --resume <id>` starts a brand new session with no error at all.
|
|
213
|
+
resumeSession: opts.resume,
|
|
214
|
+
saveSessionAs: opts.save,
|
|
203
215
|
};
|
|
204
216
|
}
|
|
205
217
|
// ---------------------------------------------------------------------------
|
|
@@ -388,7 +400,33 @@ function runInteractive(flags, extras = {}) {
|
|
|
388
400
|
process.exitCode = 2;
|
|
389
401
|
return;
|
|
390
402
|
}
|
|
403
|
+
// --- Session identity, resolved BEFORE the controller exists (D-9). ------
|
|
404
|
+
//
|
|
405
|
+
// BEFORE, not after, because every failure here is exit 2 with one copyable
|
|
406
|
+
// sentence and NO writes: a controller that already exists has to be disposed
|
|
407
|
+
// on a path that never rendered anything. The only write this performs is the
|
|
408
|
+
// `wx` claim of the new id.
|
|
409
|
+
const startup = resolveStartupSession({
|
|
410
|
+
...(flags.resumeSession === undefined ? {} : { resume: flags.resumeSession }),
|
|
411
|
+
...(flags.saveSessionAs === undefined ? {} : { saveAs: flags.saveSessionAs }),
|
|
412
|
+
cwd: process.cwd(),
|
|
413
|
+
// An explicit `--model` / `--provider` outranks the model recorded in the
|
|
414
|
+
// file: the user said what they wanted more recently than the file did.
|
|
415
|
+
explicitModel: flags.model !== undefined || flags.provider !== undefined,
|
|
416
|
+
});
|
|
417
|
+
if (!startup.ok) {
|
|
418
|
+
process.stderr.write(`${startup.message}\n`);
|
|
419
|
+
process.exitCode = 2;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// MUTABLE BECAUSE `/resume` ROTATES IT (D-15). The chip follows through the
|
|
423
|
+
// App's `onIdentity`, and the writer reads `parentId` from here on every write.
|
|
424
|
+
let identity = startup.identity;
|
|
391
425
|
const { controller, confirmBridge, humanInputBridge } = makeController(flags);
|
|
426
|
+
// THE RESTORE HAPPENS BEFORE THE FIRST FRAME. `initialEntries` seeds the
|
|
427
|
+
// reducer, so a resumed transcript is complete on frame one instead of filling
|
|
428
|
+
// in behind the user's eyes.
|
|
429
|
+
startup.apply(controller);
|
|
392
430
|
void controller.refreshModelMetadata();
|
|
393
431
|
const config = controller.getConfig();
|
|
394
432
|
const caps = resolveTuiCapabilities({}, config.color);
|
|
@@ -403,6 +441,76 @@ function runInteractive(flags, extras = {}) {
|
|
|
403
441
|
// outcome for the one feature here that runs five agents the user cannot
|
|
404
442
|
// watch (§3.13 / P1-2 / R-19).
|
|
405
443
|
attachTeamEvents(logger, controller);
|
|
444
|
+
// --- The checkpoint writer (D-2 / D-21 / I-10). --------------------------
|
|
445
|
+
//
|
|
446
|
+
// BUILT HERE, BEFORE EVERY EXIT PATH BELOW, because all three of them flush
|
|
447
|
+
// through it and one of them is a `process.on('exit')` handler that can fire
|
|
448
|
+
// while the frame is still on screen.
|
|
449
|
+
//
|
|
450
|
+
// `readView` reads the module-level slot `App` publishes into; with no frame
|
|
451
|
+
// yet (or after unmount) it answers `undefined` and the writer reports
|
|
452
|
+
// `no-identity` instead of writing a guess.
|
|
453
|
+
const checkpointWriter = createSessionWriter({
|
|
454
|
+
controller,
|
|
455
|
+
readView: readSessionView,
|
|
456
|
+
readParentId: () => identity.parentId,
|
|
457
|
+
startPinned: identity.pinned === true,
|
|
458
|
+
});
|
|
459
|
+
// --- The branch port (D-1 / D-2 / D-5 / D-16). ---------------------------
|
|
460
|
+
//
|
|
461
|
+
// BUILT HERE FOR THE SAME REASON THE WRITER IS: it needs the writer (to flush
|
|
462
|
+
// before spawning) and it needs `process.env` / `process.argv`, both of which
|
|
463
|
+
// belong to the host rather than to the view. The App only forwards it.
|
|
464
|
+
//
|
|
465
|
+
// The `checkpoint` closure is a THIN WRAPPER, not the writer itself: the port
|
|
466
|
+
// asks for `force: true` and reports `{ok, message}`, while the writer returns
|
|
467
|
+
// a richer result whose `reason` the launcher must not interpret.
|
|
468
|
+
const branchLauncher = createBranchLauncher({
|
|
469
|
+
checkpoint: (opts) => {
|
|
470
|
+
const result = checkpointWriter.writeNow(opts);
|
|
471
|
+
return result.ok ? { ok: true } : { ok: false, message: result.reason };
|
|
472
|
+
},
|
|
473
|
+
platform: process.platform,
|
|
474
|
+
env: process.env,
|
|
475
|
+
argv: process.argv,
|
|
476
|
+
execPath: process.execPath,
|
|
477
|
+
});
|
|
478
|
+
const sessionBridge = {
|
|
479
|
+
branch: branchLauncher,
|
|
480
|
+
checkpoint: (opts) => {
|
|
481
|
+
const result = checkpointWriter.writeNow(opts ?? {});
|
|
482
|
+
// A PIN THE REPORT DOES NOT KNOW ABOUT IS A LIE ON SCREEN. `writeNow` sets
|
|
483
|
+
// the writer's sticky flag, and `/session` afterwards reads
|
|
484
|
+
// `port.identity()`, which is React state - so without this the file says
|
|
485
|
+
// `pinned: true` while `/session` still answers `pinned no`.
|
|
486
|
+
if (opts?.pin === true && result.ok) {
|
|
487
|
+
identity = { ...identity, pinned: true };
|
|
488
|
+
sessionBridge.onIdentity?.(identity);
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
},
|
|
492
|
+
rotate: (loadedPath) => {
|
|
493
|
+
// CLAIM THE NEW ID FIRST, THEN TELL THE VIEW (D-15). If the claim fails the
|
|
494
|
+
// chip keeps the id whose file it is still writing, which is the only safe
|
|
495
|
+
// half of a failed rotation.
|
|
496
|
+
const next = rotateToChildOf(loadedPath);
|
|
497
|
+
if (!next)
|
|
498
|
+
return undefined;
|
|
499
|
+
identity = next;
|
|
500
|
+
// REPUBLISH SYNCHRONOUSLY, because the writer reads the id from the
|
|
501
|
+
// published slot while it reads `parentId` from `identity` above. Between
|
|
502
|
+
// this line and the next render those two would disagree, and a flush in
|
|
503
|
+
// that window (exit, signal, `/session pin`) would write the conversation
|
|
504
|
+
// into the OLD id's file carrying the NEW `branchOf` - the exact
|
|
505
|
+
// inconsistency I-5 / I-12 forbid (the view's `onIdentity` below is what
|
|
506
|
+
// keeps the chip in step; this is what keeps the WRITER in step).
|
|
507
|
+
const view = readSessionView();
|
|
508
|
+
if (view)
|
|
509
|
+
publishSessionView({ ...view, id: next.id });
|
|
510
|
+
sessionBridge.onIdentity?.(next);
|
|
511
|
+
return next;
|
|
512
|
+
},
|
|
513
|
+
};
|
|
406
514
|
let screen = null;
|
|
407
515
|
let replayed = false;
|
|
408
516
|
// --- Mouse wheel region routing (mouse-wheel-region-routing §4.2). -------
|
|
@@ -624,7 +732,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
624
732
|
// Without it, `kill <pid>`, closing the terminal window (SIGHUP), or a dying
|
|
625
733
|
// parent all leave the user staring at a blank alternate screen with `reset`
|
|
626
734
|
// as their only way out.
|
|
627
|
-
|
|
735
|
+
// THE THIRD PATH, AND NOT A REDUNDANT ONE (R-17 / I-10): `waitUntilExit()` is
|
|
736
|
+
// a microtask that a killed process never reaches, and `failInteractive`
|
|
737
|
+
// reaches neither it nor the signal hook. `flushSync` is idempotent, so the
|
|
738
|
+
// three can coexist without writing three times.
|
|
739
|
+
process.on('exit', () => {
|
|
740
|
+
checkpointWriter.flushSync('exit');
|
|
741
|
+
restore();
|
|
742
|
+
});
|
|
628
743
|
// The signal listeners themselves now belong to `installLogging()`, which
|
|
629
744
|
// registered them before this function ever ran — and a second listener per
|
|
630
745
|
// signal would race the first. This branch owns the SCREEN, so it supplies
|
|
@@ -633,6 +748,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
633
748
|
// the alternate screen), and the signal path restores before exiting.
|
|
634
749
|
setScreenRestore(restore);
|
|
635
750
|
setSignalTerminator((signo) => {
|
|
751
|
+
// BEFORE `restore()`: both are synchronous, but only this one needs the
|
|
752
|
+
// controller and the published view to still be live.
|
|
753
|
+
checkpointWriter.flushSync('signal');
|
|
636
754
|
restore(); // Synchronous: stdout.write on a TTY is sync on every platform.
|
|
637
755
|
process.exit(128 + signo);
|
|
638
756
|
});
|
|
@@ -747,7 +865,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
747
865
|
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
|
|
748
866
|
// Absent when nothing can refuse a paste, so a `--no-paste` tree is
|
|
749
867
|
// byte-identical to a build without this feature.
|
|
750
|
-
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge }),
|
|
868
|
+
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge, session: identity, sessionBridge: sessionBridge, ...(startup.initialEntries.length > 0 ? { initialEntries: startup.initialEntries } : {}), ...(startup.notice ? { sessionNotice: startup.notice } : {}) }),
|
|
751
869
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
752
870
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
753
871
|
{
|
|
@@ -775,6 +893,17 @@ function runInteractive(flags, extras = {}) {
|
|
|
775
893
|
// never armed it this is one read and one string compare. Gating it would mean
|
|
776
894
|
// a user who switches `mode` to `off` between the install and the next launch
|
|
777
895
|
// leaves the guard armed with nothing able to disarm it.
|
|
896
|
+
// THE FIRST WRITE, IMMEDIATELY AFTER `render()` (D-2). This is what makes the
|
|
897
|
+
// id on screen resumable BEFORE the first message: a zero-turn session is a
|
|
898
|
+
// legal session, and `--resume` of one is a legitimate thing to want.
|
|
899
|
+
checkpointWriter.start();
|
|
900
|
+
// ONE SUBSCRIPTION, ON ONE EVENT (I-10). `agent_end` is the only moment a
|
|
901
|
+
// checkpoint is honest: mid-stream the turn has not settled, and every extra
|
|
902
|
+
// event subscription is another timer source to reason about.
|
|
903
|
+
checkpointWriter.attach((onAgentEnd) => controller.subscribe((event) => {
|
|
904
|
+
if (event.type === 'agent_end')
|
|
905
|
+
onAgentEnd();
|
|
906
|
+
}));
|
|
778
907
|
setImmediate(() => { if (!interactiveClosed)
|
|
779
908
|
markBootHealthy(VERSION); });
|
|
780
909
|
if (updateEligible) {
|
|
@@ -819,6 +948,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
819
948
|
.waitUntilExit()
|
|
820
949
|
.then(() => {
|
|
821
950
|
interactiveClosed = true;
|
|
951
|
+
// FIRST LINE, AND BEFORE `controller.dispose()`: this flush needs the live
|
|
952
|
+
// history, the live todos and the published view, all of which the
|
|
953
|
+
// disposers below take apart.
|
|
954
|
+
checkpointWriter.flushSync('exit');
|
|
822
955
|
// Also cover a successful immediate exit when launched via dist/cli.js,
|
|
823
956
|
// which bypasses the launcher's code-zero boot-guard backstop.
|
|
824
957
|
markBootHealthy(VERSION);
|
|
@@ -852,11 +985,57 @@ function runInteractive(flags, extras = {}) {
|
|
|
852
985
|
})
|
|
853
986
|
.catch(failInteractive);
|
|
854
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Build the checkpoint writer for one process (D-2 / D-21).
|
|
990
|
+
*
|
|
991
|
+
* ONE CONSTRUCTION SITE FOR TWO PATHS. The TUI and `-p` differ in exactly one
|
|
992
|
+
* thing - where `readView` gets its transcript - and in nothing else: the same
|
|
993
|
+
* `meta` fields, the same live model, the same todos, the same cwd. Writing the
|
|
994
|
+
* eight deps twice is how `meta.turns` would come to mean two things, which is
|
|
995
|
+
* the drift class D-9 exists to prevent for `applyRestoredSession`.
|
|
996
|
+
*/
|
|
997
|
+
function createSessionWriter(input) {
|
|
998
|
+
const { controller } = input;
|
|
999
|
+
return new SessionCheckpointWriter({
|
|
1000
|
+
readView: input.readView,
|
|
1001
|
+
readHistory: () => controller.getSessionSnapshot(),
|
|
1002
|
+
readTodos: () => controller.getTodoSnapshot()?.items ?? [],
|
|
1003
|
+
// READ AT WRITE TIME: `/model` rebuilds the config, and a checkpoint that
|
|
1004
|
+
// recorded the model from launch would name the wrong one after a switch.
|
|
1005
|
+
readModel: () => {
|
|
1006
|
+
const live = controller.getConfig();
|
|
1007
|
+
return {
|
|
1008
|
+
providerId: live.provider,
|
|
1009
|
+
modelId: live.model,
|
|
1010
|
+
...(live.baseUrl ? { baseUrl: live.baseUrl } : {}),
|
|
1011
|
+
};
|
|
1012
|
+
},
|
|
1013
|
+
readCwd: () => controller.getCwd(),
|
|
1014
|
+
version: VERSION,
|
|
1015
|
+
readParentId: input.readParentId,
|
|
1016
|
+
startPinned: input.startPinned,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
855
1019
|
async function runOneShot(flags, prompt, quiet) {
|
|
1020
|
+
// THE SAME RESOLUTION THE TUI USES (D-9 / D-21). A `-p` run has no Ink, no
|
|
1021
|
+
// `/session` and no `agent_end` subscription, so `--resume` here is one read
|
|
1022
|
+
// and one write - not a different feature.
|
|
1023
|
+
const startup = resolveStartupSession({
|
|
1024
|
+
...(flags.resumeSession === undefined ? {} : { resume: flags.resumeSession }),
|
|
1025
|
+
...(flags.saveSessionAs === undefined ? {} : { saveAs: flags.saveSessionAs }),
|
|
1026
|
+
cwd: process.cwd(),
|
|
1027
|
+
explicitModel: flags.model !== undefined || flags.provider !== undefined,
|
|
1028
|
+
});
|
|
1029
|
+
if (!startup.ok) {
|
|
1030
|
+
process.stderr.write(`${startup.message}\n`);
|
|
1031
|
+
process.exitCode = 2;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
856
1034
|
// Headless: no App is ever rendered, so there is no human to approve a skill
|
|
857
1035
|
// install. The gate must know that up front rather than discovering a null
|
|
858
1036
|
// handler and defaulting to "yes" (§7.4 / AC-13).
|
|
859
1037
|
const { controller } = makeController(flags, { interactive: false });
|
|
1038
|
+
startup.apply(controller);
|
|
860
1039
|
void controller.refreshModelMetadata();
|
|
861
1040
|
const logger = getLogger();
|
|
862
1041
|
logger.info('cli', 'run_oneshot', { quiet });
|
|
@@ -876,6 +1055,27 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
876
1055
|
// `followThrough` is read off the REAL controller here rather than through
|
|
877
1056
|
// `HeadlessController`, which is deliberately the minimal surface a test can
|
|
878
1057
|
// satisfy with an object literal (todo-plan-followthrough §3.6).
|
|
1058
|
+
// THE ONE WRITE `-p` MAKES (D-21), and it is a deliberate trade: `entries` is
|
|
1059
|
+
// empty because there is no transcript, while `messages` is authoritative, so a
|
|
1060
|
+
// later `--resume` inherits the full context and replays no scrollback. Faking
|
|
1061
|
+
// entries would put something on screen that was never displayed.
|
|
1062
|
+
const checkpointWriter = createSessionWriter({
|
|
1063
|
+
controller,
|
|
1064
|
+
readView: () => ({
|
|
1065
|
+
id: startup.identity.id,
|
|
1066
|
+
entries: [],
|
|
1067
|
+
usageTotal: EMPTY_USAGE_TOTAL,
|
|
1068
|
+
}),
|
|
1069
|
+
readParentId: () => startup.identity.parentId,
|
|
1070
|
+
startPinned: startup.identity.pinned === true,
|
|
1071
|
+
});
|
|
1072
|
+
// STDERR, NOT STDOUT (D-21): stdout may be a pipe whose consumer expects the
|
|
1073
|
+
// answer and nothing else, while a human running `-p` by hand still gets told
|
|
1074
|
+
// which session they branched into.
|
|
1075
|
+
if (startup.identity.kind === 'branch' && startup.identity.parentId) {
|
|
1076
|
+
process.stderr.write(`[session] Resumed ${startup.identity.parentId} as a new branch: ` +
|
|
1077
|
+
`${startup.identity.id} (headless: context only, no transcript).\n`);
|
|
1078
|
+
}
|
|
879
1079
|
let code;
|
|
880
1080
|
try {
|
|
881
1081
|
code = await runHeadless(controller, prompt, {
|
|
@@ -884,6 +1084,8 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
884
1084
|
});
|
|
885
1085
|
}
|
|
886
1086
|
finally {
|
|
1087
|
+
// BEFORE `dispose()`: the flush needs the live history and todos.
|
|
1088
|
+
checkpointWriter.flushSync('exit');
|
|
887
1089
|
controller.dispose();
|
|
888
1090
|
}
|
|
889
1091
|
// Flushed HERE and not inside `runHeadless`: that module is deliberately
|
|
@@ -1461,6 +1663,16 @@ function buildProgram() {
|
|
|
1461
1663
|
.version(VERSION, '-v, --version', 'Print the version')
|
|
1462
1664
|
.argument('[prompt]', 'Task prompt (starts the TUI, or a one-shot run with -p / piped stdin)')
|
|
1463
1665
|
.option('-p, --print', 'Headless: stream the answer to stdout, then exit')
|
|
1666
|
+
// ROOT-LEVEL, AND `exec` KEEPS ITS OWN (commander does not inherit parent
|
|
1667
|
+
// options: `copyInheritedSettings` copies output/help configuration only, not
|
|
1668
|
+
// `.option()` definitions), so the two can coexist. T-39 asserts both option
|
|
1669
|
+
// tables structurally rather than by parsing help text.
|
|
1670
|
+
//
|
|
1671
|
+
// `--resume` ALWAYS BRANCHES. It reads the named session and continues it under
|
|
1672
|
+
// a NEW id, so the session it came from is never written again by this process;
|
|
1673
|
+
// `--save` names THIS run's persistent id and pins it against `sessions prune`.
|
|
1674
|
+
.option('--resume <id|path>', 'Resume a saved session as a new branch (TUI or -p)')
|
|
1675
|
+
.option('--save <id>', 'Persist this session under this id (pinned against prune)')
|
|
1464
1676
|
.option('--provider <id>', 'anthropic | openai | google')
|
|
1465
1677
|
.option('--model <id>', 'Model id')
|
|
1466
1678
|
.option('--base-url <url>', 'Override the provider base URL')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/branch` - open a new terminal on this session (tui-branch-terminal-shortcut
|
|
3
|
+
* 3.5 / D-1 / D-2 / D-12 / D-13 / D-21 / D-22).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`, which also
|
|
6
|
+
* means every sentence in `BRANCH_COPY` is plain ASCII (no em dashes, no arrows).
|
|
7
|
+
*
|
|
8
|
+
* WHY A COMMAND AND NOT A KEY HANDLER: `Ctrl+L` dispatches `/branch` exactly the
|
|
9
|
+
* way `Ctrl+I` dispatches `/skill:project-indexer` (`App.tsx` is a documented
|
|
10
|
+
* oversize file, so new logic goes into a module and the key handler keeps doing
|
|
11
|
+
* only guard + dispatch). The side benefit is that `/branch` becomes typeable,
|
|
12
|
+
* testable, listed by `/help`, and inherits `executeSlashInput`'s try/catch - a
|
|
13
|
+
* throwing command becomes one `Command failed: ...` line instead of an unhandled
|
|
14
|
+
* rejection. The cost is ONE transcript line per press (D-21), which `Ctrl+I`
|
|
15
|
+
* already pays today.
|
|
16
|
+
*
|
|
17
|
+
* THIS BODY NEVER TOUCHES SESSION DATA (I-5): no draft read, no queue change, no
|
|
18
|
+
* message edit, no id change, no write to the parent file. It calls `identity()`
|
|
19
|
+
* and `open()` and reports what came back.
|
|
20
|
+
*/
|
|
21
|
+
import type { CommandContext } from './registry.js';
|
|
22
|
+
import type { BranchOpenResult, BranchRequest } from '../session/branch-launch.js';
|
|
23
|
+
/** What `/branch` can do. The one seam a test needs. */
|
|
24
|
+
export interface BranchCommandPort {
|
|
25
|
+
open(request: BranchRequest): Promise<BranchOpenResult>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every sentence `/branch` can print, verbatim.
|
|
29
|
+
*
|
|
30
|
+
* ONE SOURCE, THREE CHANNELS (D-12): success goes to the toast (plus a notice while
|
|
31
|
+
* the agent runs, D-22), failures go to `notify` because a user has to READ a full
|
|
32
|
+
* sentence and possibly copy a command out of it, and cooldown goes to the toast
|
|
33
|
+
* because it is not a failure - it is "you already did this", which expires in two
|
|
34
|
+
* seconds and would be permanent noise in the transcript.
|
|
35
|
+
*
|
|
36
|
+
* `noIdentity` REUSES `/session`'s SENTENCE (R-10). The same state must not read
|
|
37
|
+
* two different ways depending on which key the user pressed; only the consequence
|
|
38
|
+
* clause ("so there is nothing to branch from" / "") differs.
|
|
39
|
+
*
|
|
40
|
+
* There is deliberately NO `stopped` entry (R-9): nothing ever called it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const BRANCH_COPY: {
|
|
43
|
+
readonly noIdentity: string;
|
|
44
|
+
readonly unavailable: "Branching into a new terminal is not available in this build.";
|
|
45
|
+
readonly cooldown: (ms: number) => string;
|
|
46
|
+
readonly opened: (id: string) => string;
|
|
47
|
+
readonly checkpointFailed: (reason: string) => string;
|
|
48
|
+
readonly noLauncher: (manual: string) => string;
|
|
49
|
+
readonly spawnFailed: (reason: string, manual: string) => string;
|
|
50
|
+
readonly lateError: (reason: string) => string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Run `/branch`.
|
|
54
|
+
*
|
|
55
|
+
* ASYNC BY NECESSITY, NOT BY STYLE: "the window opened" and "`wt.exe` does not
|
|
56
|
+
* exist" are distinguishable only after the child emits `'spawn'` or `'error'`,
|
|
57
|
+
* and ENOENT is reported asynchronously. `SlashCommand.run` already allows
|
|
58
|
+
* `Promise<void>`, and `App` calls this as `void executeSlashInput('/branch')`, so
|
|
59
|
+
* the UI never waits.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runBranchCommand(ctx: CommandContext): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/branch` - open a new terminal on this session (tui-branch-terminal-shortcut
|
|
3
|
+
* 3.5 / D-1 / D-2 / D-12 / D-13 / D-21 / D-22).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`, which also
|
|
6
|
+
* means every sentence in `BRANCH_COPY` is plain ASCII (no em dashes, no arrows).
|
|
7
|
+
*
|
|
8
|
+
* WHY A COMMAND AND NOT A KEY HANDLER: `Ctrl+L` dispatches `/branch` exactly the
|
|
9
|
+
* way `Ctrl+I` dispatches `/skill:project-indexer` (`App.tsx` is a documented
|
|
10
|
+
* oversize file, so new logic goes into a module and the key handler keeps doing
|
|
11
|
+
* only guard + dispatch). The side benefit is that `/branch` becomes typeable,
|
|
12
|
+
* testable, listed by `/help`, and inherits `executeSlashInput`'s try/catch - a
|
|
13
|
+
* throwing command becomes one `Command failed: ...` line instead of an unhandled
|
|
14
|
+
* rejection. The cost is ONE transcript line per press (D-21), which `Ctrl+I`
|
|
15
|
+
* already pays today.
|
|
16
|
+
*
|
|
17
|
+
* THIS BODY NEVER TOUCHES SESSION DATA (I-5): no draft read, no queue change, no
|
|
18
|
+
* message edit, no id change, no write to the parent file. It calls `identity()`
|
|
19
|
+
* and `open()` and reports what came back.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Every sentence `/branch` can print, verbatim.
|
|
23
|
+
*
|
|
24
|
+
* ONE SOURCE, THREE CHANNELS (D-12): success goes to the toast (plus a notice while
|
|
25
|
+
* the agent runs, D-22), failures go to `notify` because a user has to READ a full
|
|
26
|
+
* sentence and possibly copy a command out of it, and cooldown goes to the toast
|
|
27
|
+
* because it is not a failure - it is "you already did this", which expires in two
|
|
28
|
+
* seconds and would be permanent noise in the transcript.
|
|
29
|
+
*
|
|
30
|
+
* `noIdentity` REUSES `/session`'s SENTENCE (R-10). The same state must not read
|
|
31
|
+
* two different ways depending on which key the user pressed; only the consequence
|
|
32
|
+
* clause ("so there is nothing to branch from" / "") differs.
|
|
33
|
+
*
|
|
34
|
+
* There is deliberately NO `stopped` entry (R-9): nothing ever called it.
|
|
35
|
+
*/
|
|
36
|
+
export const BRANCH_COPY = {
|
|
37
|
+
noIdentity: 'This session has no id (session recording is off for this build), ' +
|
|
38
|
+
'so there is nothing to branch from.',
|
|
39
|
+
unavailable: 'Branching into a new terminal is not available in this build.',
|
|
40
|
+
cooldown: (ms) => `A branch was just opened - try again in ${ms} ms.`,
|
|
41
|
+
opened: (id) => `New terminal opened - it resumes ${id} as a new branch.`,
|
|
42
|
+
checkpointFailed: (reason) => `Could not checkpoint this session (${reason}), so a new branch would start from ` +
|
|
43
|
+
'stale context. Nothing was opened.',
|
|
44
|
+
noLauncher: (manual) => `No terminal window could be opened here. Run this in another terminal:\n${manual}`,
|
|
45
|
+
spawnFailed: (reason, manual) => `Could not start the terminal (${reason}). Run this in another terminal:\n${manual}`,
|
|
46
|
+
lateError: (reason) => `The new terminal reported an error after opening: ${reason}`,
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Run `/branch`.
|
|
50
|
+
*
|
|
51
|
+
* ASYNC BY NECESSITY, NOT BY STYLE: "the window opened" and "`wt.exe` does not
|
|
52
|
+
* exist" are distinguishable only after the child emits `'spawn'` or `'error'`,
|
|
53
|
+
* and ENOENT is reported asynchronously. `SlashCommand.run` already allows
|
|
54
|
+
* `Promise<void>`, and `App` calls this as `void executeSlashInput('/branch')`, so
|
|
55
|
+
* the UI never waits.
|
|
56
|
+
*/
|
|
57
|
+
export async function runBranchCommand(ctx) {
|
|
58
|
+
// 1. No identity, no branch (D-13). Without an id there is nothing for the new
|
|
59
|
+
// window to resume, and a silent no-op reads as a bug.
|
|
60
|
+
const identity = ctx.session?.identity();
|
|
61
|
+
if (!identity) {
|
|
62
|
+
ctx.notify('warn', BRANCH_COPY.noIdentity);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// 2. A build without the port says so rather than pretending.
|
|
66
|
+
const port = ctx.branch;
|
|
67
|
+
if (!port) {
|
|
68
|
+
ctx.notify('warn', BRANCH_COPY.unavailable);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 3. The cwd is the CONTROLLER's (`/cwd` can change it), never `process.cwd()`,
|
|
72
|
+
// and `--resume` does not restore a cwd - so this value is the only chance
|
|
73
|
+
// the new window has to land in the right directory (D-16).
|
|
74
|
+
const result = await port.open({
|
|
75
|
+
id: identity.id,
|
|
76
|
+
cwd: ctx.controller.getCwd(),
|
|
77
|
+
onLateError: (message) => ctx.notify('warn', message),
|
|
78
|
+
});
|
|
79
|
+
if (result.ok) {
|
|
80
|
+
reportOpened(ctx, identity.id);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
reportFailure(ctx, result);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Success: the toast confirms the action; while the agent runs it ALSO gets one
|
|
87
|
+
* notice line (D-22 / R-5).
|
|
88
|
+
*
|
|
89
|
+
* THE SECOND CHANNEL IS NOT REDUNDANCY. The status bar's feedback slot shows the
|
|
90
|
+
* activity phrase during a run, so a toast at that moment is invisible; a user who
|
|
91
|
+
* sees nothing presses again, gets caught by the cooldown, and sees nothing again.
|
|
92
|
+
* The notice is also where "the child inherited the last settled turn, not the one
|
|
93
|
+
* being generated" becomes visible after the fact.
|
|
94
|
+
*/
|
|
95
|
+
function reportOpened(ctx, id) {
|
|
96
|
+
const text = BRANCH_COPY.opened(id);
|
|
97
|
+
ctx.toast('info', text);
|
|
98
|
+
if (ctx.controller.isRunning())
|
|
99
|
+
ctx.notify('info', text);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Failure: one notice, one sentence, and a command the user can paste (I-4).
|
|
103
|
+
*
|
|
104
|
+
* `cooldown` is the exception that goes to the toast (D-12): it is a time hint
|
|
105
|
+
* about something that just succeeded, not a defect. `checkpoint-failed` and the
|
|
106
|
+
* two spawn failures already carry their COMPLETE sentence - the launcher composes
|
|
107
|
+
* it from `BRANCH_COPY` because only the launcher knows whether the checkpoint
|
|
108
|
+
* failed because the history was unsendable or because this frame has no id yet
|
|
109
|
+
* (R-19), and a body that re-wrapped the text would print an internal reason code.
|
|
110
|
+
*/
|
|
111
|
+
function reportFailure(ctx, result) {
|
|
112
|
+
if (result.reason === 'cooldown') {
|
|
113
|
+
ctx.toast('info', result.message);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
ctx.notify('warn', result.message);
|
|
117
|
+
}
|
|
@@ -15,12 +15,16 @@ import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
|
|
|
15
15
|
import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
16
16
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
17
17
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
18
|
+
import { applyRestoredSession } from '../session/startup-session.js';
|
|
19
|
+
import { runSessionCommand } from './session-command.js';
|
|
20
|
+
import { runBranchCommand } from './branch-command.js';
|
|
18
21
|
import { getLogger } from '../logging/logger.js';
|
|
19
22
|
import { resolveFastTier } from '../fast/resolve.js';
|
|
20
23
|
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
21
24
|
import { registerQueueCommand } from './queue-command.js';
|
|
22
25
|
import { perfCommand } from './perf.js';
|
|
23
26
|
import { runCompactCommand } from '../compaction/command.js';
|
|
27
|
+
import { runClearCommand } from './clear-command.js';
|
|
24
28
|
import { runContextCommand } from '../compaction/context-command.js';
|
|
25
29
|
/**
|
|
26
30
|
* `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
|
|
@@ -957,44 +961,23 @@ const COMMANDS = [
|
|
|
957
961
|
},
|
|
958
962
|
},
|
|
959
963
|
{
|
|
964
|
+
// REGISTRATION ONLY. The body lives in `commands/clear-command.ts`: this file
|
|
965
|
+
// is already past the 1000-line guideline, and `/clear` is no longer a
|
|
966
|
+
// two-line projection reset - it interrupts a run, waits for it, archives the
|
|
967
|
+
// conversation and then clears (clear-command-context-reset D-8).
|
|
960
968
|
name: 'clear',
|
|
961
|
-
description: '
|
|
962
|
-
run:
|
|
963
|
-
// A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
|
|
964
|
-
// reuses reason `'user'`; I-2 and its standing exception are stated on
|
|
965
|
-
// `TodoStore`. Clearing the STORE and not merely the view mirror is what
|
|
966
|
-
// keeps `/save`, `/todo status|continue` and follow-through agreeing with
|
|
967
|
-
// the screen — they read the store directly. `messages` is untouched:
|
|
968
|
-
// clearing the conversation is still `/reset` alone.
|
|
969
|
-
//
|
|
970
|
-
// DELIBERATELY WITHOUT `/todo clear`'s two guards: an `isRunning()` refusal
|
|
971
|
-
// would leave `/clear` unable to clear even the transcript mid-run, and an
|
|
972
|
-
// early return on an empty list would skip the dispatch below entirely.
|
|
973
|
-
// Clearing an empty store is already silent — it emits into a reducer
|
|
974
|
-
// branch that writes two fields that are both already at their defaults.
|
|
975
|
-
//
|
|
976
|
-
// Bounded side effect: the emptied store re-arms `todo_write`'s "a fresh
|
|
977
|
-
// plan has at least two items" gate, so a model that shrinks a plan to one
|
|
978
|
-
// item right after a mid-run `/clear` gets the refusal text and retries.
|
|
979
|
-
ctx.controller.clearTodos();
|
|
980
|
-
ctx.dispatch({ type: 'clearTranscript' });
|
|
981
|
-
},
|
|
969
|
+
description: 'Start a new conversation; "/clear screen" only clears the screen',
|
|
970
|
+
run: runClearCommand,
|
|
982
971
|
},
|
|
983
972
|
{
|
|
973
|
+
// ALIAS BY DESIGN (D-2), AND THIS IS THE WHOLE FIX. Until this change `/clear`
|
|
974
|
+
// cleared the screen while `/reset` cleared the session, so the command a
|
|
975
|
+
// user reaches for in a long conversation was the one that kept the history.
|
|
976
|
+
// One implementation makes that drift structurally impossible rather than
|
|
977
|
+
// merely fixed: there is no second body left to diverge.
|
|
984
978
|
name: 'reset',
|
|
985
|
-
description: '
|
|
986
|
-
run:
|
|
987
|
-
if (ctx.controller.isRunning()) {
|
|
988
|
-
ctx.notify('warn', 'Cannot reset while the agent is running. Wait for it to stop.');
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
ctx.controller.clearMessages();
|
|
992
|
-
ctx.controller.clearAllQueues();
|
|
993
|
-
ctx.dispatch({ type: 'resetConversation' });
|
|
994
|
-
ctx.toast('info', 'Started a new conversation.');
|
|
995
|
-
if (ctx.state.pendingSteering.length)
|
|
996
|
-
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
997
|
-
},
|
|
979
|
+
description: 'Alias of /clear',
|
|
980
|
+
run: runClearCommand,
|
|
998
981
|
},
|
|
999
982
|
{
|
|
1000
983
|
name: 'cwd',
|
|
@@ -1057,15 +1040,38 @@ const COMMANDS = [
|
|
|
1057
1040
|
}
|
|
1058
1041
|
// Loading has validated and normalized every external value. Commit
|
|
1059
1042
|
// synchronously so no new run can start between clearing and restoring.
|
|
1043
|
+
//
|
|
1044
|
+
// THE THREE CONTROLLER CALLS COME FROM THE SHARED PORT (tui-session-id-resume
|
|
1045
|
+
// 3.8 / D-9): the startup `--resume` path and this command must never be
|
|
1046
|
+
// two implementations of "adopt a saved conversation", which is exactly
|
|
1047
|
+
// how `/clear` and `/reset` drifted apart once. `clearAllQueues` and the
|
|
1048
|
+
// `restoreEntries` dispatch stay HERE: the first is indispensable for a
|
|
1049
|
+
// live TUI and a no-op at startup, and the second is a VIEW action with
|
|
1050
|
+
// no controller to hang it on.
|
|
1060
1051
|
ctx.controller.clearAllQueues();
|
|
1061
|
-
ctx.controller
|
|
1062
|
-
// Absence clears the previous conversation's list, including old files.
|
|
1063
|
-
ctx.controller.restoreTodos(session.todos ?? []);
|
|
1052
|
+
applyRestoredSession(ctx.controller, session);
|
|
1064
1053
|
ctx.dispatch({ type: 'restoreEntries', entries: session.entries });
|
|
1065
|
-
|
|
1066
|
-
|
|
1054
|
+
// A NEW ID, AND A ZEROED COUNTER, IN THAT ORDER (D-15 / D-10 / R-9).
|
|
1055
|
+
// Without the rotation the next checkpoint would write THIS conversation
|
|
1056
|
+
// into the previous id's file - silently destroying its disk record - and
|
|
1057
|
+
// without the reset the new id's `meta.usage` would bill the previous
|
|
1058
|
+
// conversation's spend to this one.
|
|
1059
|
+
const rotated = ctx.session?.rotate(path);
|
|
1060
|
+
// GATED ON THE ROTATION, NOT ON THE COMMAND (D-10: the criterion
|
|
1061
|
+
// is 'the id changed'). With no session port - a host without this
|
|
1062
|
+
// feature - or a failed claim the id is unchanged, and restarting
|
|
1063
|
+
// the counter there would misreport this conversation's spend. A
|
|
1064
|
+
// failed claim is also the one case where the NEXT checkpoint writes
|
|
1065
|
+
// another conversation into this id's file, so it is said out loud
|
|
1066
|
+
// rather than left to be discovered later.
|
|
1067
|
+
if (rotated)
|
|
1068
|
+
ctx.dispatch({ type: 'resetUsage' });
|
|
1069
|
+
if (ctx.session && !rotated) {
|
|
1070
|
+
ctx.notify('warn', 'Could not claim a new session id; this conversation keeps the current one.');
|
|
1067
1071
|
}
|
|
1068
|
-
ctx.notify('info',
|
|
1072
|
+
ctx.notify('info', rotated
|
|
1073
|
+
? `Resumed session from ${path} as ${rotated.id}.`
|
|
1074
|
+
: `Resumed session from ${path}`);
|
|
1069
1075
|
if (ctx.state.pendingSteering.length)
|
|
1070
1076
|
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
1071
1077
|
}
|
|
@@ -1074,6 +1080,22 @@ const COMMANDS = [
|
|
|
1074
1080
|
}
|
|
1075
1081
|
},
|
|
1076
1082
|
},
|
|
1083
|
+
{
|
|
1084
|
+
// REGISTRATION ONLY - the body lives in `commands/session-command.ts`, for
|
|
1085
|
+
// the reason `/clear` and `/compact` do: this file is past the 1000-line
|
|
1086
|
+
// guideline and `/session` is a report with three branches, not a one-liner.
|
|
1087
|
+
name: 'session',
|
|
1088
|
+
description: 'Show this session id and file; "/session copy" copies the resume command',
|
|
1089
|
+
run: runSessionCommand,
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
// REGISTRATION ONLY - the body lives in `commands/branch-command.ts`, for the
|
|
1093
|
+
// reason `/session` above does. `Ctrl+L` dispatches this same command, so the
|
|
1094
|
+
// key and the typed word are one code path including the cooldown (D-2).
|
|
1095
|
+
name: 'branch',
|
|
1096
|
+
description: 'Open a new terminal on this session (Ctrl+L); the branch gets its own id',
|
|
1097
|
+
run: (ctx) => runBranchCommand(ctx),
|
|
1098
|
+
},
|
|
1077
1099
|
{
|
|
1078
1100
|
name: 'copy',
|
|
1079
1101
|
description: 'Copy the last answer to the clipboard',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/clear` - start a new conversation - plus its `/reset` alias and the
|
|
3
|
+
* `/clear screen` form that keeps the old "just wipe the screen" meaning
|
|
4
|
+
* (clear-command-context-reset §3.5 / D-1 / D-2 / D-3).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`. The one
|
|
7
|
+
* localized string below is written with `\u` escapes for that reason, exactly
|
|
8
|
+
* as the `/reset` body it replaces did.
|
|
9
|
+
*
|
|
10
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
11
|
+
* reason `/compact` and `/context` do (D-8). `builtins.ts` is 1511 lines against
|
|
12
|
+
* `CLAUDE.md`'s 1000-line cap, and this command is no longer a two-line
|
|
13
|
+
* projection reset - it interrupts a run, waits for it, archives a conversation
|
|
14
|
+
* and then clears. A two-line edit belongs inline; this does not.
|
|
15
|
+
*
|
|
16
|
+
* THE COMMAND'S ONLY JOB IS SEQUENCING AND SPEAKING. Every state change happens
|
|
17
|
+
* on the controller port (`AgentController.resetConversation`, which delegates
|
|
18
|
+
* the ORDER to `agent/conversation-reset.ts`), and every word the user reads is
|
|
19
|
+
* decided here, in one place, in the exact order they will read it.
|
|
20
|
+
*/
|
|
21
|
+
import type { CommandContext } from './registry.js';
|
|
22
|
+
/**
|
|
23
|
+
* Run `/clear` or `/reset`. Both registry entries point HERE (D-2), which is
|
|
24
|
+
* what stops the two from drifting back into "one clears the screen, one clears
|
|
25
|
+
* the session" - the drift that produced the defect this feature fixes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runClearCommand(ctx: CommandContext): Promise<void>;
|