@aragon-agent/cli 0.5.11 → 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 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` wipes the transcript but not the conversation, so the plan stays.
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
- /** `/todo clear`. A user override of a projection; refused mid-run by `/todo`. */
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
  /**
@@ -916,7 +916,11 @@ export class AgentController {
916
916
  restoreTodos(items) {
917
917
  this.todos?.restore(items);
918
918
  }
919
- /** `/todo clear`. A user override of a projection; refused mid-run by `/todo`. */
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` reaches the transcript
1061
- // and NOT this method, which is the whole distinction: it tells the model
1062
- // nothing, so it may not change the list.
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). */
@@ -692,10 +692,20 @@ export function viewReducer(state, action) {
692
692
  streamingId: undefined,
693
693
  expandedToolIds: {},
694
694
  teamEntryId: undefined,
695
- // The entry it pointed at is gone, so the id goes with it. `todos` is
696
- // DELIBERATELY UNTOUCHED (I-2): `/clear` wipes the transcript and not
697
- // the conversation, so the model still believes in the plan, and a panel
698
- // that disagrees with the model is worse than no panel.
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
@@ -831,8 +831,27 @@ const COMMANDS = [
831
831
  },
832
832
  {
833
833
  name: 'clear',
834
- description: 'Clear the visible transcript',
835
- run: (ctx) => ctx.dispatch({ type: 'clearTranscript' }),
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',
@@ -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. `/clear` wipes the
14
- * transcript but not the conversation, so it must NOT reach this class;
15
- * `/reset` clears `messages`, so it must. Every write is a FULL REPLACEMENT and
16
- * every clear is a FULL REMOVAL - there is no partial update anywhere, which is
17
- * what makes I-9 ("the panel is never a contradiction of the model") provable
18
- * rather than argued.
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';
@@ -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. `/clear` wipes the
14
- * transcript but not the conversation, so it must NOT reach this class;
15
- * `/reset` clears `messages`, so it must. Every write is a FULL REPLACEMENT and
16
- * every clear is a FULL REMOVAL - there is no partial update anywhere, which is
17
- * what makes I-9 ("the panel is never a contradiction of the model") provable
18
- * rather than argued.
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 { getLogger } from '../logging/logger.js';
21
27
  import { TODO_LIMITS } from './limits.js';
@@ -42,7 +42,8 @@ export interface TodoSnapshot {
42
42
  /**
43
43
  * Why a list went away.
44
44
  *
45
- * `user` - `/todo clear`.
45
+ * `user` - `/todo clear`, or `/clear`. Both are an explicit user instruction,
46
+ * which is I-2's standing exception; neither touches `messages`.
46
47
  * `reset` - `/reset`, or a `/resume` whose file carried no list (§3.13).
47
48
  * `turn` - every item was completed when the next user turn started.
48
49
  * `stale` - unfinished, and unwritten for more than `TODO_LIMITS.staleTurns`
@@ -21,6 +21,22 @@
21
21
  *
22
22
  * What is left is the one thing the status bar cannot say: THAT THE SILENCE IS
23
23
  * WORK. The bar shows state; this row shows life.
24
+ *
25
+ * THIS ROW OWNS THE ONLY ANIMATED SPINNER WHILE IT IS MOUNTED
26
+ * (single-spinner-while-running D-1). Seven other sites can animate — the
27
+ * streaming assistant marker, the tool badge, the team/fast/retry cards, and the
28
+ * two panels' rows — and each was independently right to. Emergently they put up
29
+ * to five braille animations on adjacent rows, each on its own 80 ms timer: one
30
+ * piece of information rendered five times and out of phase, which is the same
31
+ * duplication the paragraph above legislates against for text. So `App` widens
32
+ * the `reducedMotion` it hands those seven to `cfg.reducedMotion || (running &&
33
+ * !overlay)`, and they all fall back to the static glyphs they already had.
34
+ *
35
+ * THIS COMPONENT KEEPS THE RAW CONFIG FLAG, and that asymmetry is deliberate
36
+ * (D-5). It reads the flag TWICE: once for the spinner below, and once for
37
+ * `pickActivityPhrase`'s rotation. Handing it the widened value would freeze the
38
+ * phrase at the first word of every run — a second, unrelated regression, and
39
+ * one nobody would connect to a change about icons.
24
40
  */
25
41
  import React from 'react';
26
42
  import type { Theme } from './theme.js';
package/dist/ui/App.js CHANGED
@@ -1422,6 +1422,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1422
1422
  const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, contentCols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
1423
1423
  const density = cfg.density;
1424
1424
  const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
1425
+ // --- One spinner per run (single-spinner-while-running D-1 .. D-5). ------
1426
+ //
1427
+ // THE MOUNT CONDITION, NAMED ONCE AND USED TWICE (D-4). This const both gates
1428
+ // the `<ActivityLine>` element below and feeds the derivation on the next
1429
+ // line, so "the line is up" and "every other site is still" can never
1430
+ // disagree. Two copies of the same boolean expression is exactly how that
1431
+ // invariant becomes false in six months, silently and in only one branch.
1432
+ //
1433
+ // NOT `running` ALONE (D-3). While an overlay owns the screen the activity row
1434
+ // is suppressed, so keying suppression off `running` would leave the frame
1435
+ // with ZERO life signals: the bar shows state, this row shows life. Never
1436
+ // trade one duplicate for one absence.
1437
+ const activityVisible = running && !overlayNode;
1438
+ // The FOUR VIEW CONSUMERS (transcript x 2, team panel, todo panel) take this;
1439
+ // the activity line keeps the raw config flag (D-5), because it reads that
1440
+ // flag a second time to gate phrase rotation and the widened value would
1441
+ // freeze the phrase at the first word of every run.
1442
+ //
1443
+ // So below `App` the prop now means "this frame does not animate here" rather
1444
+ // than "the user asked for reduced motion". `cfg.reducedMotion` (`:279`) stays
1445
+ // the sole config source, and `spinner-census.test.ts` pins this wiring —
1446
+ // threading the raw flag into a fifth consumer is the one silent way back to
1447
+ // two spinners (R-2).
1448
+ const viewReducedMotion = reducedMotion || activityVisible;
1425
1449
  // Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
1426
1450
  // `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
1427
1451
  perfRef.current = {
@@ -1440,20 +1464,25 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1440
1464
  viewportRows: fullscreen ? viewportBudget : rows,
1441
1465
  offset: scrolledLines,
1442
1466
  };
1443
- const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
1467
+ const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
1444
1468
  // The completion popup lives inside the bottom chrome and can add up to 9
1445
1469
  // rows; unbounded, it pushes the status bar past the frame height where
1446
1470
  // `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
1447
1471
  const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
1448
1472
  const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
1449
- return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: state.status === 'running', reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
1473
+ return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
1450
1474
  // `!overlayNode` suppresses the line while an overlay owns the screen:
1451
1475
  // the full-screen branch replaces the whole viewport with the overlay,
1452
1476
  // and a working line under a settings screen is noise attached to a
1453
1477
  // surface the run is not visible on. (Inline overlays are part of the
1454
1478
  // document flow, so there the suppression is conservative rather than
1455
1479
  // necessary — P2-5.)
1456
- activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
1480
+ //
1481
+ // THE CONDITION IS `activityVisible`, THE SAME CONST THE SUPPRESSION
1482
+ // SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
1483
+ // drift apart, and the drift is silent in whichever branch is not
1484
+ // edited.
1485
+ activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
1457
1486
  // PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
1458
1487
  // (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
1459
1488
  // prop for truthiness, and a React element is truthy however it
@@ -138,6 +138,21 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
138
138
  // While text streams, the spinner IS the role marker. Braille dots are
139
139
  // both an animation and a Unicode-only glyph, so reduced motion and an
140
140
  // ASCII terminal fall back to the same static marker (P2-11 / A-10).
141
+ //
142
+ // `reducedMotion` ARRIVES PRE-WIDENED FROM `App`: it is now
143
+ // `cfg.reducedMotion || activityVisible`, so this marker is static for the
144
+ // whole of every run and the activity row above the composer carries the
145
+ // one animation (single-spinner-while-running D-2). Nothing here changes —
146
+ // the expression already meant "do not animate here".
147
+ //
148
+ // THAT IS SAFE FOR `<Static>` BY CONSTRUCTION, not by luck: a settled
149
+ // prefix is printed once and can never be re-printed, so a boolean that
150
+ // changed how an already-printed entry renders would tear the history.
151
+ // `computeSettledCount` below breaks on EVERY condition that makes an
152
+ // entry animate (`:84` streaming, `:85` running tool, `:91` active team,
153
+ // `:104` retry, `:109` live fast), so every animating entry is in the live
154
+ // region and every entry in `<Static>` already has `animate === false` on
155
+ // both sides of the flip. Weakening a break clause breaks that proof.
141
156
  const animate = entry.streaming && !reducedMotion && caps.unicode;
142
157
  return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
143
158
  // `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * FastCard — one fast-model review in the transcript (fast-model-tier §6).
4
4
  *
@@ -76,7 +76,25 @@ function FastCardImpl(props) {
76
76
  const duration = durationMs === undefined ? '' : ` ${glyphs.midDot} ${formatDuration(durationMs)}`;
77
77
  const head = `fast review #${reviewIndex} ${glyphs.midDot} ${model} ${glyphs.midDot} turn ${turn}${duration}`;
78
78
  const showBody = status === 'advice' && text !== undefined && text.trim().length > 0;
79
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "fast" }), live && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [' ', _jsx(Spinner, { type: "dots" }), " ", head] })) : (_jsxs(Text, { color: theme.muted, wrap: "truncate", children: [' ', live ? `${glyphs.spinnerStill} ` : '', head] }))] }), showBody ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: textRows(text).map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))) })) : (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: statusLine(props) }) }))] }));
79
+ // COLOUR TRACKS `live`, MOTION TRACKS `animate` — one headline, two separate
80
+ // decisions (single-spinner-while-running C-1 / D-7).
81
+ //
82
+ // These used to be two whole `<Text>` elements, so the still branch also
83
+ // demoted the head to `theme.muted`. That was harmless while stillness meant
84
+ // "the user set reducedMotion", but `App` now widens the prop to "this frame
85
+ // does not animate here" for the whole of every run — and under the old shape
86
+ // that would have greyed out every live fast review for the whole run, losing
87
+ // the status colour along with the motion.
88
+ //
89
+ // Three things are load-bearing and all three are silent if dropped: the
90
+ // leading two-space gutter that separates the head from the bold `fast` label,
91
+ // the single space after the marker (the still form bakes it into its template
92
+ // string), and `head` itself. `wrap="truncate"` now applies to the animated
93
+ // path too, which it did not before: it is what the still branch always did,
94
+ // it is the behaviour this card has in every state that is now common, and an
95
+ // unbounded wrap inside a one-row headline is the worse failure.
96
+ const animate = live && !reducedMotion && caps.unicode;
97
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "fast" }), _jsxs(Text, { color: live ? color : theme.muted, wrap: "truncate", children: [' ', animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), showBody ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: textRows(text).map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))) })) : (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: statusLine(props) }) }))] }));
80
98
  }
81
99
  /**
82
100
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -33,7 +33,7 @@ const COMMANDS = [
33
33
  'Todo planning: status, switches, the rail, follow-through',
34
34
  ],
35
35
  ['/tools', 'List active tools'],
36
- ['/clear', 'Clear the visible transcript'],
36
+ ['/clear', 'Clear the visible transcript and the todo panel'],
37
37
  ['/reset', 'New conversation'],
38
38
  ['/cwd [dir]', 'Show or change the tool working directory'],
39
39
  ['/save [file]', 'Save the session to JSON'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -52,7 +52,7 @@
52
52
  "prepublishOnly": "npm run build"
53
53
  },
54
54
  "dependencies": {
55
- "@aragon-agent/core": "^0.2.11",
55
+ "@aragon-agent/core": "^0.2.12",
56
56
  "chalk": "^5.6.2",
57
57
  "cli-highlight": "^2.0.0",
58
58
  "commander": "^12.1.0",