@aragon-agent/cli 0.6.25 → 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 +89 -0
- package/README.md +165 -13
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +4 -1
- 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 +48 -6
- package/dist/commands/registry.d.ts +28 -0
- 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/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/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/ui/App.d.ts +54 -2
- package/dist/ui/App.js +115 -21
- 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/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 +9 -3
- 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,6 +15,9 @@ 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';
|
|
@@ -1037,15 +1040,38 @@ const COMMANDS = [
|
|
|
1037
1040
|
}
|
|
1038
1041
|
// Loading has validated and normalized every external value. Commit
|
|
1039
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.
|
|
1040
1051
|
ctx.controller.clearAllQueues();
|
|
1041
|
-
ctx.controller
|
|
1042
|
-
// Absence clears the previous conversation's list, including old files.
|
|
1043
|
-
ctx.controller.restoreTodos(session.todos ?? []);
|
|
1052
|
+
applyRestoredSession(ctx.controller, session);
|
|
1044
1053
|
ctx.dispatch({ type: 'restoreEntries', entries: session.entries });
|
|
1045
|
-
|
|
1046
|
-
|
|
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.');
|
|
1047
1071
|
}
|
|
1048
|
-
ctx.notify('info',
|
|
1072
|
+
ctx.notify('info', rotated
|
|
1073
|
+
? `Resumed session from ${path} as ${rotated.id}.`
|
|
1074
|
+
: `Resumed session from ${path}`);
|
|
1049
1075
|
if (ctx.state.pendingSteering.length)
|
|
1050
1076
|
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
1051
1077
|
}
|
|
@@ -1054,6 +1080,22 @@ const COMMANDS = [
|
|
|
1054
1080
|
}
|
|
1055
1081
|
},
|
|
1056
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
|
+
},
|
|
1057
1099
|
{
|
|
1058
1100
|
name: 'copy',
|
|
1059
1101
|
description: 'Copy the last answer to the clipboard',
|
|
@@ -10,6 +10,8 @@ import type { Entry, NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } f
|
|
|
10
10
|
import type { PersistedConfig } from '../config/schema.js';
|
|
11
11
|
import type { FollowThroughBudget } from '../todo/follow-through.js';
|
|
12
12
|
import type { UpdateCommandPort } from '../update/types.js';
|
|
13
|
+
import type { SessionCommandPort } from './session-command.js';
|
|
14
|
+
import type { BranchCommandPort } from './branch-command.js';
|
|
13
15
|
export interface CommandContext {
|
|
14
16
|
/** App-owned shared lock and single feedback path for command and selection copies. */
|
|
15
17
|
requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
|
|
@@ -127,6 +129,32 @@ export interface CommandContext {
|
|
|
127
129
|
* (the same rule the `update/types.js` import above follows).
|
|
128
130
|
*/
|
|
129
131
|
transcriptEntries?: () => Entry[];
|
|
132
|
+
/**
|
|
133
|
+
* The session's read-only identity plus the two actions `/session` and
|
|
134
|
+
* `/resume` need (tui-session-id-resume 3.11 / D-14 / D-15).
|
|
135
|
+
*
|
|
136
|
+
* OPTIONAL, like `followBudget` / `update` / `mouse` / `transcriptEntries`
|
|
137
|
+
* above: every existing `CommandContext` fixture in this package builds this
|
|
138
|
+
* object by literal, and a required field would break all of them at compile
|
|
139
|
+
* time for a feature none of them is about. Absent means the build has no
|
|
140
|
+
* session identity, and `/session` says exactly that rather than pretending.
|
|
141
|
+
*/
|
|
142
|
+
session?: SessionCommandPort;
|
|
143
|
+
/**
|
|
144
|
+
* The `/branch` port: open a new terminal on this session (tui-branch-terminal-
|
|
145
|
+
* shortcut 3.11 / #8).
|
|
146
|
+
*
|
|
147
|
+
* OPTIONAL for the same reason `session` above is: every existing
|
|
148
|
+
* `CommandContext` fixture in this package builds this object by literal, and a
|
|
149
|
+
* required field would break all of them at compile time for a feature none of
|
|
150
|
+
* them is about. Absent means `/branch` says "not available in this build"
|
|
151
|
+
* rather than failing silently.
|
|
152
|
+
*
|
|
153
|
+
* TYPE-ONLY import, following the rule the `session-command.js` import above
|
|
154
|
+
* records: the command needs the SHAPE of the port and of its result, never a
|
|
155
|
+
* value at run time.
|
|
156
|
+
*/
|
|
157
|
+
branch?: BranchCommandPort;
|
|
130
158
|
}
|
|
131
159
|
/** What `/mouse` can see and do. Read at command time, never captured. */
|
|
132
160
|
export interface MouseCommandPort {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/session` - report this session's identity, and the two things a user does
|
|
3
|
+
* with it (tui-session-id-resume §3.11 / D-13 / D-14).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
8
|
+
* reason `/clear` and `/compact` do. `builtins.ts` is already past the
|
|
9
|
+
* 1000-line guideline, and this command is a report, not a one-liner.
|
|
10
|
+
*
|
|
11
|
+
* COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
|
|
12
|
+
* characters off a status bar is the only step of this whole feature that can
|
|
13
|
+
* fail for a human, and `/session copy` removes it: one keystroke, the exact
|
|
14
|
+
* command in the clipboard, no transcription.
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckpointResult } from '../session/checkpoint.js';
|
|
17
|
+
import type { SessionIdentity } from '../session/startup-session.js';
|
|
18
|
+
import type { CommandContext } from './registry.js';
|
|
19
|
+
/**
|
|
20
|
+
* What `/session` can see and do.
|
|
21
|
+
*
|
|
22
|
+
* THREE MEMBERS AND NO MORE. `identity()` is read at command time rather than
|
|
23
|
+
* captured, because `/resume` rotates the id under a live App (D-15) and a
|
|
24
|
+
* captured value would report the previous one for the rest of the session.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionCommandPort {
|
|
27
|
+
/** The current identity, or `undefined` when this build has none. */
|
|
28
|
+
identity(): SessionIdentity | undefined;
|
|
29
|
+
/** Write once now; `pin` also marks the file as never-prunable. */
|
|
30
|
+
checkpoint(opts?: {
|
|
31
|
+
pin?: boolean;
|
|
32
|
+
}): CheckpointResult;
|
|
33
|
+
/** Adopt another conversation and move to a derived id; `undefined` if it cannot. */
|
|
34
|
+
rotate(loadedPath: string): SessionIdentity | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The one path an id maps to.
|
|
38
|
+
*
|
|
39
|
+
* THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
|
|
40
|
+
* the single choke point where an id becomes a file name, and a report that
|
|
41
|
+
* built its own path would be the first thing to disagree with the writer about
|
|
42
|
+
* where a session lives.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sessionFilePath(id: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Run `/session`, `/session copy` or `/session pin`.
|
|
47
|
+
*
|
|
48
|
+
* ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
|
|
49
|
+
* six cards in the transcript and replay them as six fragments on exit. The
|
|
50
|
+
* `file` line is NOT truncated even when it overflows: it is a path the user
|
|
51
|
+
* needs to be able to copy, and it is in the same family as `/save`'s
|
|
52
|
+
* `Saved session to <path>`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runSessionCommand(ctx: CommandContext): void;
|