@aragon-agent/cli 0.5.10 → 0.5.12
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 +24 -0
- package/README.md +5 -3
- package/dist/agent/controller.d.ts +5 -1
- package/dist/agent/controller.js +9 -4
- package/dist/agent/reducer.js +14 -4
- package/dist/cli.js +93 -0
- package/dist/commands/builtins.js +21 -2
- package/dist/commands/perf.d.ts +5 -1
- package/dist/commands/perf.js +43 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.js +17 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +31 -0
- package/dist/config/schema.d.ts +35 -0
- package/dist/config/schema.js +26 -0
- package/dist/todo/store.d.ts +12 -6
- package/dist/todo/store.js +12 -6
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/ActivityLine.d.ts +16 -0
- package/dist/ui/App.js +32 -3
- package/dist/ui/PromptInput.d.ts +10 -0
- package/dist/ui/PromptInput.js +41 -48
- package/dist/ui/Transcript.js +15 -0
- package/dist/ui/editor-reducer.d.ts +62 -0
- package/dist/ui/editor-reducer.js +90 -0
- package/dist/ui/entries/FastCard.js +20 -2
- package/dist/ui/frame-differ.d.ts +95 -0
- package/dist/ui/frame-differ.js +230 -0
- package/dist/ui/overlays/HelpOverlay.js +1 -1
- package/dist/ui/stdout-frame-writer.d.ts +41 -0
- package/dist/ui/stdout-frame-writer.js +102 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ were written.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`/clear` now takes the todo panel with it.** Finish a plan, type `/clear`,
|
|
13
|
+
and the transcript emptied while the `7/7 done` rail stayed exactly where it
|
|
14
|
+
was — on a screen you had just asked to be cleared. It stayed until your next
|
|
15
|
+
real message, which is precisely the pause you take after clearing to decide
|
|
16
|
+
what to ask next, so essentially everyone met it.
|
|
17
|
+
|
|
18
|
+
This was deliberate once. The list is a projection of what the model believes,
|
|
19
|
+
and `/clear` wipes the transcript rather than the conversation, so the model
|
|
20
|
+
still believed in the plan and a panel contradicting the model was judged worse
|
|
21
|
+
than no panel. That reasoning holds for everything the CLI does on its own; it
|
|
22
|
+
does not hold against you typing the command. An explicit instruction was
|
|
23
|
+
always this rule's exception — `/todo clear` has used the same door from the
|
|
24
|
+
start — and `/clear` now goes through it.
|
|
25
|
+
|
|
26
|
+
The list itself is cleared, not just the pixels, so `/save`, `/todo status`,
|
|
27
|
+
`/todo continue` and auto follow-through all agree with the screen: no plan is
|
|
28
|
+
written into a saved session, nothing offers to continue one, and nothing spends
|
|
29
|
+
a follow-through on it. `/reset` is unchanged and still the only command that
|
|
30
|
+
clears the conversation. Nothing is stranded either way — the model's next
|
|
31
|
+
`todo_write` is a full replacement, so the panel comes back the moment there is
|
|
32
|
+
a plan to show.
|
|
33
|
+
|
|
10
34
|
### Added
|
|
11
35
|
|
|
12
36
|
- **An update that will not start is undone automatically.** The updater shipped
|
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ Exit codes: `0` success · `1` agent/runtime error · `2` config/usage error ·
|
|
|
107
107
|
| `/thinking <level>` | Set the thinking level |
|
|
108
108
|
| `/max-tokens [<n>\|auto\|default]` | Output token cap; with no argument, reports the setting **and** the effective cap for the live model |
|
|
109
109
|
| `/tools` | List active tools |
|
|
110
|
-
| `/clear` | Clear the visible transcript |
|
|
110
|
+
| `/clear` | Clear the visible transcript and the todo panel |
|
|
111
111
|
| `/reset` | New conversation |
|
|
112
112
|
| `/cwd [dir]` | Show / change the tool working directory |
|
|
113
113
|
| `/save [file]` | Save the session to JSON |
|
|
@@ -498,9 +498,11 @@ against an empty plan is refused outright. A session that never plans has
|
|
|
498
498
|
byte-identical layout to a build without this feature.
|
|
499
499
|
|
|
500
500
|
**The list is a projection of what the model believes**, so nothing that does
|
|
501
|
-
not also tell the model may change it:
|
|
501
|
+
not also tell the model may change it — except you, saying so:
|
|
502
502
|
|
|
503
|
-
- `/clear`
|
|
503
|
+
- `/clear` and `/todo clear` take the plan off the screen. Neither touches the
|
|
504
|
+
conversation, so the model still knows what it was doing and its next update
|
|
505
|
+
brings the panel straight back.
|
|
504
506
|
- `/reset` clears the conversation, so the plan goes with it.
|
|
505
507
|
- `/save` writes the list and `/resume` restores it. Resuming a session that has
|
|
506
508
|
no list clears the current one, rather than leaving a plan on screen whose
|
|
@@ -339,7 +339,11 @@ export declare class AgentController {
|
|
|
339
339
|
subscribeTodos(listener: TodoEventListener): () => void;
|
|
340
340
|
/** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
|
|
341
341
|
restoreTodos(items: unknown): void;
|
|
342
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* `/todo clear` and `/clear`. A user override of a projection (I-2's standing
|
|
344
|
+
* exception). `/todo` refuses mid-run before it gets here; `/clear` does not,
|
|
345
|
+
* on purpose — refusing there would leave it unable to clear the transcript.
|
|
346
|
+
*/
|
|
343
347
|
clearTodos(): void;
|
|
344
348
|
getRetryConfig(): RetryConfig;
|
|
345
349
|
/**
|
package/dist/agent/controller.js
CHANGED
|
@@ -916,7 +916,11 @@ export class AgentController {
|
|
|
916
916
|
restoreTodos(items) {
|
|
917
917
|
this.todos?.restore(items);
|
|
918
918
|
}
|
|
919
|
-
/**
|
|
919
|
+
/**
|
|
920
|
+
* `/todo clear` and `/clear`. A user override of a projection (I-2's standing
|
|
921
|
+
* exception). `/todo` refuses mid-run before it gets here; `/clear` does not,
|
|
922
|
+
* on purpose — refusing there would leave it unable to clear the transcript.
|
|
923
|
+
*/
|
|
920
924
|
clearTodos() {
|
|
921
925
|
this.todos?.clear('user');
|
|
922
926
|
}
|
|
@@ -1057,9 +1061,10 @@ export class AgentController {
|
|
|
1057
1061
|
// in this conversation" hint would otherwise start lying (P2-9).
|
|
1058
1062
|
this.skills.getRegistry().clearActive();
|
|
1059
1063
|
// `/reset` clears `messages`, so the belief that justifies the panel is gone
|
|
1060
|
-
// with them and the list must go too (I-2). `/clear`
|
|
1061
|
-
// and
|
|
1062
|
-
//
|
|
1064
|
+
// with them and the list must go too (I-2). `/clear` also drops the list, but
|
|
1065
|
+
// through `clearTodos()` and reason `'user'`, which is the distinction that
|
|
1066
|
+
// remains: this path is a belief that no longer exists, that one is a user
|
|
1067
|
+
// overriding a projection of a belief the model still holds.
|
|
1063
1068
|
this.todos?.clear('reset');
|
|
1064
1069
|
}
|
|
1065
1070
|
/** Read-only view of the live system prompt (regression tests, /debug). */
|
package/dist/agent/reducer.js
CHANGED
|
@@ -692,10 +692,20 @@ export function viewReducer(state, action) {
|
|
|
692
692
|
streamingId: undefined,
|
|
693
693
|
expandedToolIds: {},
|
|
694
694
|
teamEntryId: undefined,
|
|
695
|
-
// The
|
|
696
|
-
//
|
|
697
|
-
// the
|
|
698
|
-
//
|
|
695
|
+
// The list goes with the screen it was on. `/clear` is an explicit user
|
|
696
|
+
// instruction, which is I-2's standing exception (`TodoClearReason`
|
|
697
|
+
// `'user'`, the same door `/todo clear` uses) — so the command clears the
|
|
698
|
+
// STORE via `controller.clearTodos()` and this clears the MIRROR, exactly
|
|
699
|
+
// the pairing `resetConversation` below has always had.
|
|
700
|
+
//
|
|
701
|
+
// The mirror write is same-valued with the store's own `todoCleared`
|
|
702
|
+
// dispatch rather than redundant with it: it is what makes this action
|
|
703
|
+
// stand on its own. `builtins.ts` is the only dispatcher of it TODAY, and
|
|
704
|
+
// a second one — a real clear-screen key, a session-restore path — would
|
|
705
|
+
// otherwise bring the bug back in identical form and in silence.
|
|
706
|
+
//
|
|
707
|
+
// The entry id is dropped because the entry it pointed at is gone.
|
|
708
|
+
todos: null,
|
|
699
709
|
todoEntryId: undefined,
|
|
700
710
|
// Same reasoning for the retry card: the entry is gone, so the id must go
|
|
701
711
|
// with it or `retryScheduled` would `mapEntry` an id that no longer
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,9 @@ import { runSkillsCommand } from './skills/cli-commands.js';
|
|
|
34
34
|
import { runHeadless } from './agent/headless.js';
|
|
35
35
|
import { App } from './ui/App.js';
|
|
36
36
|
import { decideRenderMode } from './ui/layout/frame.js';
|
|
37
|
+
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
38
|
+
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
39
|
+
import { setFrameStatsProvider } from './commands/perf.js';
|
|
37
40
|
import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
|
|
38
41
|
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
39
42
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
@@ -140,6 +143,16 @@ function toFlags(opts) {
|
|
|
140
143
|
transcriptRetain: opts.transcriptRetain,
|
|
141
144
|
renderGovernor: opts.renderGovernor,
|
|
142
145
|
maxRenderInterval: opts.maxRenderInterval,
|
|
146
|
+
// Site three of three for `--diff-render` / `--sync-output` — EIGHT
|
|
147
|
+
// features, one trap (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4,
|
|
148
|
+
// retry, fast, update, now frame diffing). A flag needs the `.option()`
|
|
149
|
+
// declaration below, a field on `CliFlags`, AND a line here; miss this one
|
|
150
|
+
// and the flag is inert with no compile error and no runtime error. It bites
|
|
151
|
+
// hardest for `--no-diff-render`: that is the documented escape hatch for a
|
|
152
|
+
// user whose terminal renders the diffed frame wrongly, and a user who has
|
|
153
|
+
// been told to pass it and sees no change has no next move.
|
|
154
|
+
diffRender: opts.diffRender,
|
|
155
|
+
syncOutput: opts.syncOutput,
|
|
143
156
|
// Site three of three for `--update` / `--no-update` — SEVEN features, one
|
|
144
157
|
// trap (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4, retry, fast, now
|
|
145
158
|
// update). A flag needs the `.option()` declaration below, a field on
|
|
@@ -224,6 +237,20 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
224
237
|
});
|
|
225
238
|
return { controller, confirmBridge, humanInputBridge };
|
|
226
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Raised once per session, on the 0->1 edge of the writer's `fallbacks` counter
|
|
242
|
+
* (tui-input-flicker-fix §5.6 / AC-15).
|
|
243
|
+
*
|
|
244
|
+
* The characteristic failure of frame diffing is SILENT — absolute addressing
|
|
245
|
+
* loses its origin and the user sees a subtly wrong screen — so leaving its only
|
|
246
|
+
* signal behind a slash command the affected user has no reason to run is not an
|
|
247
|
+
* acceptable end state for a fix whose whole premise is "the user should not have
|
|
248
|
+
* to notice the renderer". `warn`, not `error`: a single fallback is self-healing
|
|
249
|
+
* by construction (I-6), and it is worth reporting only because the PATTERN is
|
|
250
|
+
* not.
|
|
251
|
+
*/
|
|
252
|
+
const FRAME_FALLBACK_NOTICE = 'Frame diffing lost sync with the terminal and fell back to a full repaint. ' +
|
|
253
|
+
'If the display looks wrong, restart with `--no-diff-render`.';
|
|
227
254
|
/**
|
|
228
255
|
* The interactive TUI — and the ONLY place that may touch the screen.
|
|
229
256
|
*
|
|
@@ -272,6 +299,40 @@ function runInteractive(flags, extras = {}) {
|
|
|
272
299
|
? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
|
|
273
300
|
: null;
|
|
274
301
|
const disposeMouse = () => mouseFilter?.dispose();
|
|
302
|
+
// --- Frame differ (tui-input-flicker-fix §3.2 / §4.2). -------------------
|
|
303
|
+
//
|
|
304
|
+
// FULL-SCREEN ONLY, and that is invariant I-3 rather than caution: inline mode
|
|
305
|
+
// prints settled entries through Ink's `<Static>`, starts wherever the shell's
|
|
306
|
+
// cursor happened to be and scrolls, so row k of a frame is not absolute row k
|
|
307
|
+
// there and never will be. `--no-diff-render` skips construction entirely,
|
|
308
|
+
// which is what makes rung 2 of the fail-safe ladder byte-identical to a
|
|
309
|
+
// pre-fix build rather than merely equivalent.
|
|
310
|
+
//
|
|
311
|
+
// `rows` is a THUNK, not a snapshot: it is the only input to the I-9 geometry
|
|
312
|
+
// guard and it changes under the differ's feet on every resize.
|
|
313
|
+
const frameWriter = mode === 'fullscreen' && config.diffRender
|
|
314
|
+
? wrapStdoutForFrames(process.stdout, createFrameDiffer({
|
|
315
|
+
sync: config.syncOutput && process.env.TERM !== 'dumb',
|
|
316
|
+
rows: () => process.stdout.rows,
|
|
317
|
+
// The console bridge (`App.tsx`, full-screen only) turns this into
|
|
318
|
+
// `dispatch({type:'notice', level:'warn'})`, which is the only legal
|
|
319
|
+
// user-visible channel with the TUI mounted — a direct write would
|
|
320
|
+
// itself be the foreign write being reported. Latched on the 0->1
|
|
321
|
+
// edge inside the differ: a notice per fallback would be a repaint
|
|
322
|
+
// storm triggered by a repaint problem (§5.6).
|
|
323
|
+
onFirstFallback: () => console.warn(FRAME_FALLBACK_NOTICE),
|
|
324
|
+
}))
|
|
325
|
+
: null;
|
|
326
|
+
// `null` when no writer was built, so `/perf` can say "diff render off" rather
|
|
327
|
+
// than printing zeroes that read like a writer doing nothing (§5.4).
|
|
328
|
+
if (frameWriter)
|
|
329
|
+
setFrameStatsProvider(frameWriter.stats);
|
|
330
|
+
const disposeFrameWriter = () => {
|
|
331
|
+
if (!frameWriter)
|
|
332
|
+
return;
|
|
333
|
+
setFrameStatsProvider(null);
|
|
334
|
+
frameWriter.dispose();
|
|
335
|
+
};
|
|
275
336
|
const replayTranscript = () => {
|
|
276
337
|
if (replayed || mode !== 'fullscreen' || !config.exitTranscript)
|
|
277
338
|
return;
|
|
@@ -348,6 +409,13 @@ function runInteractive(flags, extras = {}) {
|
|
|
348
409
|
// I-5: with mouse support off the REAL stdin is handed over unwrapped, so
|
|
349
410
|
// every existing path is provably unchanged.
|
|
350
411
|
stdin: mouseFilter?.stdin ?? process.stdin,
|
|
412
|
+
// SPREAD, not `frameWriter?.stdout ?? process.stdout`. This call passed no
|
|
413
|
+
// `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
|
|
414
|
+
// the key entirely when the writer is off keeps the options object the same
|
|
415
|
+
// SHAPE it has always had, which is what makes AC-5 / AC-6 ("byte-identical
|
|
416
|
+
// with --no-diff-render, inline mode and `aragon -p`") a statement about
|
|
417
|
+
// the code rather than about Ink's defaulting.
|
|
418
|
+
...(frameWriter ? { stdout: frameWriter.stdout } : {}),
|
|
351
419
|
});
|
|
352
420
|
// H1's primary disarm, and this is the earliest moment at which "this build
|
|
353
421
|
// STARTS" is proven: the module graph loaded, config resolved, Ink mounted
|
|
@@ -402,6 +470,11 @@ function runInteractive(flags, extras = {}) {
|
|
|
402
470
|
.then(() => {
|
|
403
471
|
screen?.restore();
|
|
404
472
|
disposeMouse();
|
|
473
|
+
// BOTH BRANCHES, the same discipline `controller.dispose()` and
|
|
474
|
+
// `updateService?.dispose()` already follow. It drops the `'resize'`
|
|
475
|
+
// listener of I-8 and detaches the `/perf` provider so a later `/perf` in a
|
|
476
|
+
// torn-down tree reports honestly instead of reading a dead closure.
|
|
477
|
+
disposeFrameWriter();
|
|
405
478
|
// Orphaned subagents survive `Ctrl+C` otherwise, and a live child holds
|
|
406
479
|
// the event loop open so `aragon` never returns to the shell (R-15). The
|
|
407
480
|
// App's unmount cleanup also calls this; `dispose()` is idempotent.
|
|
@@ -421,6 +494,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
421
494
|
.catch(() => {
|
|
422
495
|
screen?.restore();
|
|
423
496
|
disposeMouse();
|
|
497
|
+
disposeFrameWriter();
|
|
424
498
|
controller.dispose();
|
|
425
499
|
updateService?.dispose();
|
|
426
500
|
});
|
|
@@ -516,6 +590,10 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
516
590
|
'transcriptRetain',
|
|
517
591
|
'renderGovernor',
|
|
518
592
|
'maxRenderIntervalMs',
|
|
593
|
+
// tui-input-flicker-fix §5.1. Listed here AND cased in the switch below, for
|
|
594
|
+
// the reason `transcriptRetain` records three lines up.
|
|
595
|
+
'diffRender',
|
|
596
|
+
'syncOutput',
|
|
519
597
|
// Without these two, `density` and `hints` would be settable only as one-shot
|
|
520
598
|
// flags and never persist -- inconsistent with every other config field.
|
|
521
599
|
'density',
|
|
@@ -687,6 +765,12 @@ function runConfigSet(key, value) {
|
|
|
687
765
|
case 'maxRenderIntervalMs':
|
|
688
766
|
patch.maxRenderIntervalMs = clampMaxRenderInterval(value, DEFAULT_MAX_RENDER_INTERVAL_MS);
|
|
689
767
|
break;
|
|
768
|
+
case 'diffRender':
|
|
769
|
+
patch.diffRender = value === 'true' || value === '1';
|
|
770
|
+
break;
|
|
771
|
+
case 'syncOutput':
|
|
772
|
+
patch.syncOutput = value === 'true' || value === '1';
|
|
773
|
+
break;
|
|
690
774
|
case 'density':
|
|
691
775
|
patch.density = clampDensity(value, 'comfortable');
|
|
692
776
|
break;
|
|
@@ -1037,6 +1121,15 @@ function buildProgram() {
|
|
|
1037
1121
|
.option('--render-governor', 'Adapt the frame interval under load (the default)')
|
|
1038
1122
|
.option('--no-render-governor', 'Pin the frame interval at 33ms however expensive a frame is')
|
|
1039
1123
|
.option('--max-render-interval <ms>', 'Governor ceiling in ms (33-1000, default 320)')
|
|
1124
|
+
// Composer flicker (tui-input-flicker-fix §5.2). Positive form FIRST for the
|
|
1125
|
+
// tri-state reason recorded five times above: both keys are persisted and
|
|
1126
|
+
// default to `true`, so a negative-only declaration would let commander
|
|
1127
|
+
// synthesize `true` and overwrite a stored `false` on every run — turning the
|
|
1128
|
+
// one documented escape hatch for a mis-rendering terminal into a no-op.
|
|
1129
|
+
.option('--diff-render', 'Repaint only changed rows (default, full-screen only)')
|
|
1130
|
+
.option('--no-diff-render', 'Repaint the whole frame every time (pre-fix behavior)')
|
|
1131
|
+
.option('--sync-output', 'Ask the terminal to present each repaint atomically (the default)')
|
|
1132
|
+
.option('--no-sync-output', 'Do not emit DEC 2026 begin/end synchronized update')
|
|
1040
1133
|
.option('--no-color', 'Disable ANSI color')
|
|
1041
1134
|
.option('--quiet', '(print mode) suppress tool/usage lines on stderr')
|
|
1042
1135
|
.action(async (prompt, opts) => {
|
|
@@ -831,8 +831,27 @@ const COMMANDS = [
|
|
|
831
831
|
},
|
|
832
832
|
{
|
|
833
833
|
name: 'clear',
|
|
834
|
-
description: 'Clear the visible transcript',
|
|
835
|
-
run: (ctx) =>
|
|
834
|
+
description: 'Clear the visible transcript and the todo panel',
|
|
835
|
+
run: (ctx) => {
|
|
836
|
+
// A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
|
|
837
|
+
// reuses reason `'user'`; I-2 and its standing exception are stated on
|
|
838
|
+
// `TodoStore`. Clearing the STORE and not merely the view mirror is what
|
|
839
|
+
// keeps `/save`, `/todo status|continue` and follow-through agreeing with
|
|
840
|
+
// the screen — they read the store directly. `messages` is untouched:
|
|
841
|
+
// clearing the conversation is still `/reset` alone.
|
|
842
|
+
//
|
|
843
|
+
// DELIBERATELY WITHOUT `/todo clear`'s two guards: an `isRunning()` refusal
|
|
844
|
+
// would leave `/clear` unable to clear even the transcript mid-run, and an
|
|
845
|
+
// early return on an empty list would skip the dispatch below entirely.
|
|
846
|
+
// Clearing an empty store is already silent — it emits into a reducer
|
|
847
|
+
// branch that writes two fields that are both already at their defaults.
|
|
848
|
+
//
|
|
849
|
+
// Bounded side effect: the emptied store re-arms `todo_write`'s "a fresh
|
|
850
|
+
// plan has at least two items" gate, so a model that shrinks a plan to one
|
|
851
|
+
// item right after a mid-run `/clear` gets the refusal text and retries.
|
|
852
|
+
ctx.controller.clearTodos();
|
|
853
|
+
ctx.dispatch({ type: 'clearTranscript' });
|
|
854
|
+
},
|
|
836
855
|
},
|
|
837
856
|
{
|
|
838
857
|
name: 'reset',
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* live panel, because a performance readout that costs a repaint per second to
|
|
16
16
|
* display would be self-defeating.
|
|
17
17
|
*/
|
|
18
|
+
import type { FrameWriterStats } from '../ui/frame-differ.js';
|
|
18
19
|
import type { SlashCommand } from './registry.js';
|
|
19
20
|
export interface PerfSnapshot {
|
|
20
21
|
/** Governor rung, its resolved interval, and the last measured commit cost. */
|
|
@@ -40,6 +41,9 @@ export interface PerfSnapshot {
|
|
|
40
41
|
export declare function setPerfSnapshotProvider(fn: (() => PerfSnapshot | null) | null): void;
|
|
41
42
|
export declare function readPerfSnapshot(): PerfSnapshot | null;
|
|
42
43
|
export declare function setPerfResetHook(fn: (() => void) | null): void;
|
|
43
|
-
/**
|
|
44
|
+
/** Called by `cli.tsx` after building the writer. Passing `null` detaches. */
|
|
45
|
+
export declare function setFrameStatsProvider(fn: (() => FrameWriterStats) | null): void;
|
|
46
|
+
export declare function readFrameStats(): FrameWriterStats | null;
|
|
47
|
+
/** Format a snapshot as the six-line block documented in §5.4. */
|
|
44
48
|
export declare function formatPerfReport(snap: PerfSnapshot): string;
|
|
45
49
|
export declare const perfCommand: SlashCommand;
|
package/dist/commands/perf.js
CHANGED
|
@@ -30,10 +30,51 @@ let resetGovernor = null;
|
|
|
30
30
|
export function setPerfResetHook(fn) {
|
|
31
31
|
resetGovernor = fn;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Frame-writer counters (tui-input-flicker-fix §5.4).
|
|
35
|
+
*
|
|
36
|
+
* A SECOND MODULE-LEVEL SLOT rather than a prop on `App`, and that is this
|
|
37
|
+
* file's own documented convention rather than a shortcut: the writer is built
|
|
38
|
+
* in `runInteractive`, lives entirely outside React, and its counters never
|
|
39
|
+
* render — so routing them through a 1900-line component's props would buy
|
|
40
|
+
* nothing and cost `App.tsx` plus `app.test.tsx` churn (P1-2).
|
|
41
|
+
*
|
|
42
|
+
* `null` means the writer was never built — `--no-diff-render`, inline mode, or
|
|
43
|
+
* a non-interactive path. `/perf` says so in words, so "the flag is off" and
|
|
44
|
+
* "the writer crashed" never look alike.
|
|
45
|
+
*/
|
|
46
|
+
let frameStats = null;
|
|
47
|
+
/** Called by `cli.tsx` after building the writer. Passing `null` detaches. */
|
|
48
|
+
export function setFrameStatsProvider(fn) {
|
|
49
|
+
frameStats = fn;
|
|
50
|
+
}
|
|
51
|
+
export function readFrameStats() {
|
|
52
|
+
return frameStats ? frameStats() : null;
|
|
53
|
+
}
|
|
33
54
|
function mb(bytes) {
|
|
34
55
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
35
56
|
}
|
|
36
|
-
|
|
57
|
+
function ratio(total, frames) {
|
|
58
|
+
return frames > 0 ? (total / frames).toFixed(1) : '0.0';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The writer line (§5.4), in this function's existing ASCII separator shape.
|
|
62
|
+
*
|
|
63
|
+
* `fallbacks > 0` on a healthy session is the single most useful diagnostic this
|
|
64
|
+
* feature can emit: it means something wrote to stdout behind Ink's back and
|
|
65
|
+
* absolute addressing lost its origin.
|
|
66
|
+
*/
|
|
67
|
+
function formatWriterLine() {
|
|
68
|
+
const stats = readFrameStats();
|
|
69
|
+
if (!stats)
|
|
70
|
+
return 'writer full - diff render off';
|
|
71
|
+
const bytesPerFrame = stats.framesTotal > 0 ? Math.round(stats.bytesWritten / stats.framesTotal) : 0;
|
|
72
|
+
return (`writer diff - frames ${stats.framesTotal} ` +
|
|
73
|
+
`(diffed ${stats.framesDiffed}, full ${stats.framesFull}) - ` +
|
|
74
|
+
`lines/frame ${ratio(stats.linesWritten, stats.framesTotal)} - ` +
|
|
75
|
+
`bytes/frame ${bytesPerFrame} - fallbacks ${stats.fallbacks}`);
|
|
76
|
+
}
|
|
77
|
+
/** Format a snapshot as the six-line block documented in §5.4. */
|
|
37
78
|
export function formatPerfReport(snap) {
|
|
38
79
|
const caches = renderCacheStats();
|
|
39
80
|
const eco = snap.rung > 0 ? ' - eco' : '';
|
|
@@ -49,6 +90,7 @@ export function formatPerfReport(snap) {
|
|
|
49
90
|
`hl ${caches.highlightEntries} (${mb(caches.highlightBytes)}) - ` +
|
|
50
91
|
`lines ${caches.lineEntries}`,
|
|
51
92
|
`mode ${snap.mode} - viewport ${snap.viewportRows} rows - offset ${snap.offset}`,
|
|
93
|
+
formatWriterLine(),
|
|
52
94
|
].join('\n');
|
|
53
95
|
}
|
|
54
96
|
export const perfCommand = {
|
package/dist/config/env.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* ARAGON_LOG_LEVEL,
|
|
14
14
|
* ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
|
|
15
15
|
* ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
|
|
16
|
+
* ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
|
|
16
17
|
* ARAGON_UPDATE.
|
|
17
18
|
*
|
|
18
19
|
* READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
|
package/dist/config/env.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* ARAGON_LOG_LEVEL,
|
|
14
14
|
* ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
|
|
15
15
|
* ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
|
|
16
|
+
* ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
|
|
16
17
|
* ARAGON_UPDATE.
|
|
17
18
|
*
|
|
18
19
|
* READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
|
|
@@ -347,6 +348,22 @@ export function readEnvConfig() {
|
|
|
347
348
|
if (Number.isFinite(parsed) && parsed > 0)
|
|
348
349
|
partial.maxRenderIntervalMs = parsed;
|
|
349
350
|
}
|
|
351
|
+
// ARAGON_DIFF_RENDER=0 restores the pre-fix full-frame repaint;
|
|
352
|
+
// ARAGON_SYNC_OUTPUT=0 drops the DEC 2026 envelope (tui-input-flicker-fix
|
|
353
|
+
// §5.3). Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
|
|
354
|
+
// ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO / ARAGON_RENDER_GOVERNOR
|
|
355
|
+
// are — NOT by `envBool` below, whose NEGATIVE list disagrees with it on values
|
|
356
|
+
// like `ARAGON_DIFF_RENDER=disable`.
|
|
357
|
+
const diffRender = process.env.ARAGON_DIFF_RENDER?.trim().toLowerCase();
|
|
358
|
+
if (diffRender !== undefined && diffRender.length > 0) {
|
|
359
|
+
partial.diffRender =
|
|
360
|
+
diffRender === '1' || diffRender === 'true' || diffRender === 'on' || diffRender === 'yes';
|
|
361
|
+
}
|
|
362
|
+
const syncOutput = process.env.ARAGON_SYNC_OUTPUT?.trim().toLowerCase();
|
|
363
|
+
if (syncOutput !== undefined && syncOutput.length > 0) {
|
|
364
|
+
partial.syncOutput =
|
|
365
|
+
syncOutput === '1' || syncOutput === 'true' || syncOutput === 'on' || syncOutput === 'yes';
|
|
366
|
+
}
|
|
350
367
|
const log = readEnvLogConfig();
|
|
351
368
|
if (log)
|
|
352
369
|
partial.log = log;
|
package/dist/config/load.d.ts
CHANGED
|
@@ -106,6 +106,19 @@ export interface CliFlags {
|
|
|
106
106
|
renderGovernor?: boolean;
|
|
107
107
|
/** `--max-render-interval <ms>` — the governor ceiling for this run (L4). */
|
|
108
108
|
maxRenderInterval?: string | number;
|
|
109
|
+
/**
|
|
110
|
+
* `--diff-render` ⇒ true, `--no-diff-render` ⇒ false, absent ⇒ undefined
|
|
111
|
+
* (tui-input-flicker-fix §5.2).
|
|
112
|
+
*
|
|
113
|
+
* DECLARED AS A PAIR for the reason `--render-governor` records one field up:
|
|
114
|
+
* `diffRender` is PERSISTED and defaults to `true`, so a lone
|
|
115
|
+
* `--no-diff-render` would make commander default `opts.diffRender` to `true`
|
|
116
|
+
* and silently overwrite a stored `false` on every run that passed no flag at
|
|
117
|
+
* all — i.e. the documented escape hatch would quietly un-set itself.
|
|
118
|
+
*/
|
|
119
|
+
diffRender?: boolean;
|
|
120
|
+
/** `--sync-output` ⇒ true, `--no-sync-output` ⇒ false, absent ⇒ undefined (§5.2). */
|
|
121
|
+
syncOutput?: boolean;
|
|
109
122
|
/**
|
|
110
123
|
* `--retry` ⇒ true, `--no-retry` ⇒ false, absent ⇒ undefined
|
|
111
124
|
* (llm-api-retry-backoff §6.2).
|
package/dist/config/load.js
CHANGED
|
@@ -227,6 +227,35 @@ function resolveRenderGovernor(flags, env, file) {
|
|
|
227
227
|
return file.renderGovernor;
|
|
228
228
|
return DEFAULT_CONFIG.renderGovernor;
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolve per-line frame diffing: flag › env › file › default `true`
|
|
232
|
+
* (tui-input-flicker-fix §5.1).
|
|
233
|
+
*
|
|
234
|
+
* Shaped exactly like `resolveRenderGovernor` above, and for the same reason:
|
|
235
|
+
* there are no heuristics for an explicit `true` to override, so a persisted
|
|
236
|
+
* `true` is a value rather than an opinion that has to be told apart from
|
|
237
|
+
* silence. `!== undefined` on the flag is what makes `--no-diff-render` beat a
|
|
238
|
+
* config file that left the key at its default.
|
|
239
|
+
*/
|
|
240
|
+
function resolveDiffRender(flags, env, file) {
|
|
241
|
+
if (flags.diffRender !== undefined)
|
|
242
|
+
return flags.diffRender;
|
|
243
|
+
if (env.diffRender !== undefined)
|
|
244
|
+
return env.diffRender;
|
|
245
|
+
if (file.diffRender !== undefined)
|
|
246
|
+
return file.diffRender;
|
|
247
|
+
return DEFAULT_CONFIG.diffRender;
|
|
248
|
+
}
|
|
249
|
+
/** Same ladder for the DEC 2026 envelope (§5.1). */
|
|
250
|
+
function resolveSyncOutput(flags, env, file) {
|
|
251
|
+
if (flags.syncOutput !== undefined)
|
|
252
|
+
return flags.syncOutput;
|
|
253
|
+
if (env.syncOutput !== undefined)
|
|
254
|
+
return env.syncOutput;
|
|
255
|
+
if (file.syncOutput !== undefined)
|
|
256
|
+
return file.syncOutput;
|
|
257
|
+
return DEFAULT_CONFIG.syncOutput;
|
|
258
|
+
}
|
|
230
259
|
/** Merge the `log` section across all four layers (defaults › file › env › flags). */
|
|
231
260
|
function resolveLogConfig(flags, env, file) {
|
|
232
261
|
const merged = clampLogConfig({
|
|
@@ -421,6 +450,8 @@ export function loadConfig(flags = {}) {
|
|
|
421
450
|
...(requestedRetain < transcriptRetain ? { transcriptRetainRequested: requestedRetain } : {}),
|
|
422
451
|
renderGovernor: resolveRenderGovernor(flags, env.partial, file),
|
|
423
452
|
maxRenderIntervalMs: clampMaxRenderInterval(pick(flags.maxRenderInterval, env.partial.maxRenderIntervalMs, file.maxRenderIntervalMs), DEFAULT_CONFIG.maxRenderIntervalMs),
|
|
453
|
+
diffRender: resolveDiffRender(flags, env.partial, file),
|
|
454
|
+
syncOutput: resolveSyncOutput(flags, env.partial, file),
|
|
424
455
|
confirmTools,
|
|
425
456
|
toolTimeoutMs,
|
|
426
457
|
idleTimeoutMs,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -98,6 +98,23 @@ export declare const DEFAULT_RENDER_GOVERNOR = true;
|
|
|
98
98
|
*/
|
|
99
99
|
export declare const DEFAULT_MAX_RENDER_INTERVAL_MS = 320;
|
|
100
100
|
export declare function clampMaxRenderInterval(v: unknown, fallback: number): number;
|
|
101
|
+
/**
|
|
102
|
+
* Repaint only the frame lines that changed (full-screen only).
|
|
103
|
+
*
|
|
104
|
+
* ON by default, and `--no-diff-render` restores today's byte stream EXACTLY —
|
|
105
|
+
* the interposer is not constructed at all, so a regression on an exotic
|
|
106
|
+
* terminal is one flag away from being neutralised (§3.5 rung 2).
|
|
107
|
+
*/
|
|
108
|
+
export declare const DEFAULT_DIFF_RENDER = true;
|
|
109
|
+
/**
|
|
110
|
+
* Wrap each repaint batch in DEC 2026 begin/end synchronized update.
|
|
111
|
+
*
|
|
112
|
+
* ON by default with NO capability probe (§3.3): terminals that do not implement
|
|
113
|
+
* mode 2026 parse the CSI sequence, find an unknown private mode and discard it,
|
|
114
|
+
* so a false positive costs nothing — and a false negative costs nothing either,
|
|
115
|
+
* because the frame differ already removes the flicker on its own.
|
|
116
|
+
*/
|
|
117
|
+
export declare const DEFAULT_SYNC_OUTPUT = true;
|
|
101
118
|
/** `ask_user` calls allowed per user turn. */
|
|
102
119
|
export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
|
|
103
120
|
/**
|
|
@@ -530,6 +547,10 @@ export interface PersistedConfig {
|
|
|
530
547
|
renderGovernor: boolean;
|
|
531
548
|
/** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
|
|
532
549
|
maxRenderIntervalMs: number;
|
|
550
|
+
/** Repaint only the frame rows that changed (tui-input-flicker-fix §5.1). */
|
|
551
|
+
diffRender: boolean;
|
|
552
|
+
/** Wrap each repaint batch in DEC 2026 synchronized update (§5.1). */
|
|
553
|
+
syncOutput: boolean;
|
|
533
554
|
confirmTools: boolean;
|
|
534
555
|
toolTimeoutMs: number;
|
|
535
556
|
idleTimeoutMs: number;
|
|
@@ -671,6 +692,20 @@ export interface CliConfig {
|
|
|
671
692
|
renderGovernor: boolean;
|
|
672
693
|
/** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
|
|
673
694
|
maxRenderIntervalMs: number;
|
|
695
|
+
/**
|
|
696
|
+
* Repaint only the frame rows that changed (tui-input-flicker-fix §5.1).
|
|
697
|
+
*
|
|
698
|
+
* IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
|
|
699
|
+
* `showThinking` / `liveToolOutput` / `update` each record: `runInteractive`
|
|
700
|
+
* reads `config.diffRender`, where `config` is a `CliConfig`, so a key added to
|
|
701
|
+
* the persisted shape alone would round-trip through `config set`, appear in
|
|
702
|
+
* `config list` — and never reach the one gate that constructs the writer.
|
|
703
|
+
* `--no-diff-render` and `ARAGON_DIFF_RENDER=0` would parse and be discarded,
|
|
704
|
+
* with no compile error and no runtime error.
|
|
705
|
+
*/
|
|
706
|
+
diffRender: boolean;
|
|
707
|
+
/** Wrap each repaint batch in DEC 2026 synchronized update (§5.1). */
|
|
708
|
+
syncOutput: boolean;
|
|
674
709
|
confirmTools: boolean;
|
|
675
710
|
toolTimeoutMs: number;
|
|
676
711
|
idleTimeoutMs: number;
|
package/dist/config/schema.js
CHANGED
|
@@ -145,6 +145,30 @@ export function clampMaxRenderInterval(v, fallback) {
|
|
|
145
145
|
return clampInt(v, fallback, RENDER_INTERVAL_RANGE);
|
|
146
146
|
}
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
148
|
+
// Frame diffing (tui-input-flicker-fix §5.1)
|
|
149
|
+
//
|
|
150
|
+
// TWO TROUBLESHOOTING SWITCHES, in the same class as `historyEnabled`: neither
|
|
151
|
+
// appears on the settings overlay, because a user who needs them is debugging a
|
|
152
|
+
// terminal, not adjusting a preference.
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Repaint only the frame lines that changed (full-screen only).
|
|
156
|
+
*
|
|
157
|
+
* ON by default, and `--no-diff-render` restores today's byte stream EXACTLY —
|
|
158
|
+
* the interposer is not constructed at all, so a regression on an exotic
|
|
159
|
+
* terminal is one flag away from being neutralised (§3.5 rung 2).
|
|
160
|
+
*/
|
|
161
|
+
export const DEFAULT_DIFF_RENDER = true;
|
|
162
|
+
/**
|
|
163
|
+
* Wrap each repaint batch in DEC 2026 begin/end synchronized update.
|
|
164
|
+
*
|
|
165
|
+
* ON by default with NO capability probe (§3.3): terminals that do not implement
|
|
166
|
+
* mode 2026 parse the CSI sequence, find an unknown private mode and discard it,
|
|
167
|
+
* so a false positive costs nothing — and a false negative costs nothing either,
|
|
168
|
+
* because the frame differ already removes the flicker on its own.
|
|
169
|
+
*/
|
|
170
|
+
export const DEFAULT_SYNC_OUTPUT = true;
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
148
172
|
// Plan mode (plan-mode §4.4)
|
|
149
173
|
//
|
|
150
174
|
// THREE FLAT KEYS, not a nested `planMode` object. `store.ts` documents that its
|
|
@@ -669,6 +693,8 @@ export const DEFAULT_CONFIG = {
|
|
|
669
693
|
transcriptRetain: DEFAULT_TRANSCRIPT_RETAIN,
|
|
670
694
|
renderGovernor: DEFAULT_RENDER_GOVERNOR,
|
|
671
695
|
maxRenderIntervalMs: DEFAULT_MAX_RENDER_INTERVAL_MS,
|
|
696
|
+
diffRender: DEFAULT_DIFF_RENDER,
|
|
697
|
+
syncOutput: DEFAULT_SYNC_OUTPUT,
|
|
672
698
|
confirmTools: false,
|
|
673
699
|
toolTimeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
|
|
674
700
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
package/dist/todo/store.d.ts
CHANGED
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
* `TeamRuntime`.
|
|
11
11
|
*
|
|
12
12
|
* INVARIANT I-2 — the list is a PROJECTION OF WHAT THE MODEL BELIEVES, and
|
|
13
|
-
* nothing that does not tell the model may change it
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* nothing that does not tell the model may change it — EXCEPT AN EXPLICIT USER
|
|
14
|
+
* INSTRUCTION, which is the rule's standing exception and is marked
|
|
15
|
+
* `TodoClearReason = 'user'`. Two commands come through that door: `/todo clear`
|
|
16
|
+
* and `/clear`. Neither tells the model anything, and that is fine, because what
|
|
17
|
+
* they leave behind is "panel absent, model still remembers" - the direction I-9
|
|
18
|
+
* below calls safe, and one the model's next full-replacement write heals on its
|
|
19
|
+
* own. `/reset` (reason `'reset'`) remains the only clear caused by the belief
|
|
20
|
+
* itself being destroyed, because it clears `messages`.
|
|
21
|
+
*
|
|
22
|
+
* Every write is a FULL REPLACEMENT and every clear is a FULL REMOVAL - there is
|
|
23
|
+
* no partial update anywhere, which is what makes I-9 ("the panel is never a
|
|
24
|
+
* contradiction of the model") provable rather than argued.
|
|
19
25
|
*/
|
|
20
26
|
import { type NormalizeResult } from './normalize.js';
|
|
21
27
|
import type { TodoClearReason, TodoEventListener, TodoSnapshot } from './types.js';
|