@aragon-agent/cli 0.5.8 → 0.5.9
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 +279 -0
- package/README.md +1429 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +86 -2
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +308 -2
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -3
- package/dist/config/env.js +103 -3
- package/dist/config/load.d.ts +49 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +212 -2
- package/dist/config/schema.js +207 -1
- package/dist/config/store.js +20 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +1 -4
- package/dist/ui/App.js +273 -37
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
package/dist/ui/App.js
CHANGED
|
@@ -28,10 +28,12 @@ import { Transcript, TranscriptList } from './Transcript.js';
|
|
|
28
28
|
import { PromptInput } from './PromptInput.js';
|
|
29
29
|
import { Composer } from './Composer.js';
|
|
30
30
|
import { StatusBar } from './StatusBar.js';
|
|
31
|
+
import { secondsLeft } from '../agent/retry-view.js';
|
|
31
32
|
import { TeamPanel } from './TeamPanel.js';
|
|
32
33
|
import { TodoPanel } from './TodoPanel.js';
|
|
33
34
|
import { TodoStrip } from './TodoStrip.js';
|
|
34
|
-
import {
|
|
35
|
+
import { ActivityLine } from './ActivityLine.js';
|
|
36
|
+
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
35
37
|
import { AppShell } from './layout/AppShell.js';
|
|
36
38
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
37
39
|
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
@@ -43,14 +45,13 @@ import { useRenderGovernor } from './use-render-governor.js';
|
|
|
43
45
|
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
44
46
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
45
47
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
46
|
-
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
47
48
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
48
49
|
import { installConsoleBridge } from './console-bridge.js';
|
|
49
50
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
50
51
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
51
52
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
52
53
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
53
|
-
import { SettingsScreen } from './overlays/SettingsScreen.js';
|
|
54
|
+
import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
54
55
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
55
56
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
56
57
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
@@ -93,7 +94,14 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
93
94
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
99
|
+
* across renders and React never re-runs it.
|
|
100
|
+
*/
|
|
101
|
+
function seedViewState(thinkingVisible) {
|
|
102
|
+
return initialViewState({ thinkingVisible });
|
|
103
|
+
}
|
|
104
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }) {
|
|
97
105
|
const { exit } = useApp();
|
|
98
106
|
const { stdout } = useStdout();
|
|
99
107
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -110,7 +118,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
110
118
|
});
|
|
111
119
|
/** The transcript height cache, owned here and read by `TranscriptList` (L3). */
|
|
112
120
|
const heights = useHeightStore();
|
|
113
|
-
|
|
121
|
+
// SEEDED FROM CONFIG, not dispatched after mount (§3.1.1): a post-mount
|
|
122
|
+
// `toggleThinking` would let `/resume` flash a restored thinking block on
|
|
123
|
+
// frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
|
|
124
|
+
// function reference.
|
|
125
|
+
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
114
126
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
115
127
|
const [confirmState, setConfirmState] = useState(null);
|
|
116
128
|
/** The request the question / plan overlay is currently rendering. */
|
|
@@ -121,8 +133,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
121
133
|
const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
|
|
122
134
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
123
135
|
const [scrollIntent, setScrollIntent] = useState();
|
|
124
|
-
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
125
|
-
const [historyIntent, setHistoryIntent] = useState();
|
|
126
136
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
127
137
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
128
138
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
@@ -155,12 +165,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
155
165
|
const ctrlCArmed = useRef(false);
|
|
156
166
|
const ctrlCTimer = useRef(null);
|
|
157
167
|
const runBaselineOut = useRef(0);
|
|
168
|
+
/**
|
|
169
|
+
* Epoch ms the current run began — the activity phrase's seed.
|
|
170
|
+
*
|
|
171
|
+
* ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
|
|
172
|
+
* AFTER the render that first sees `status === 'running'`, so a ref it wrote
|
|
173
|
+
* would be one frame late — and the first frame of a run is exactly when the
|
|
174
|
+
* phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
|
|
175
|
+
* render for the same reason: a value derived from a transition is needed by
|
|
176
|
+
* the very render that observes the transition.
|
|
177
|
+
*/
|
|
178
|
+
const runStartedAt = useRef(0);
|
|
158
179
|
const toastTimers = useRef(new Map());
|
|
159
180
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
160
181
|
* bookkeeping for promises, and re-rendering on it would be noise. */
|
|
161
182
|
const pendingHuman = useRef(new Set());
|
|
162
|
-
/** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
|
|
163
|
-
const geometryRef = useRef(UNMEASURED_GEOMETRY);
|
|
164
183
|
/**
|
|
165
184
|
* What `/perf` reports. Assigned during render, exactly like `stateRef` above,
|
|
166
185
|
* because a slash command runs outside the render pass and a value captured in
|
|
@@ -289,10 +308,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
289
308
|
for (const action of merged)
|
|
290
309
|
dispatch(action);
|
|
291
310
|
}, []);
|
|
311
|
+
/**
|
|
312
|
+
* The CLI-local diff side channel (§3.3.7). Memoized once, beside the `cost`
|
|
313
|
+
* lookup below, because `reduceEvent` TAKES a patch rather than reading one:
|
|
314
|
+
* the store is consume-once, so this object must not be rebuilt per event.
|
|
315
|
+
*/
|
|
316
|
+
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
292
317
|
useEffect(() => {
|
|
293
318
|
const unsubscribe = controller.subscribe((event) => {
|
|
294
319
|
const cost = controller.getModelInfo().cost;
|
|
295
|
-
for (const action of reduceEvent(event, cost)) {
|
|
320
|
+
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
296
321
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
297
322
|
// `ViewState.errorNoticed`, rather than read back from the reducer one
|
|
298
323
|
// commit later (C-11). This is the whole of the batching-independence
|
|
@@ -406,7 +431,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
406
431
|
cancelFollowThrough();
|
|
407
432
|
unsubscribe();
|
|
408
433
|
};
|
|
409
|
-
}, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
434
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
410
435
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
411
436
|
//
|
|
412
437
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -440,16 +465,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
440
465
|
dispatch({ type: 'teamUpdate', snapshot });
|
|
441
466
|
break;
|
|
442
467
|
}
|
|
443
|
-
case 'usage':
|
|
468
|
+
case 'usage': {
|
|
444
469
|
// Child spend is REAL SPEND and must reach the status bar, or the
|
|
445
470
|
// session cost readout under-reports by however much the team consumed
|
|
446
471
|
// — the single most misleading possible failure of this feature (R-5).
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
472
|
+
//
|
|
473
|
+
// THE COST TABLE IS SELECTED BY TIER (fast-model-tier §3.6 / R-7).
|
|
474
|
+
// Always using the lead's would over-report a Haiku child under a
|
|
475
|
+
// Sonnet lead by roughly an order of magnitude, which is a different
|
|
476
|
+
// way of being wrong about the same number.
|
|
477
|
+
// FROM THE CONTROLLER, NOT FROM `cfg` (the rule the `agent_end`
|
|
478
|
+
// handler above records): this effect's dependency list never changes,
|
|
479
|
+
// so it closes over the `cfg` object of the render it mounted in, and
|
|
480
|
+
// `setModel` builds a NEW one.
|
|
481
|
+
const live = controller.getConfig();
|
|
482
|
+
const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
|
|
483
|
+
const ref = fastTier && fastTier.ok
|
|
484
|
+
? fastTier.ref
|
|
485
|
+
: { providerId: live.provider, modelId: live.model };
|
|
486
|
+
// Unknown pricing contributes ZERO rather than a fabricated figure
|
|
487
|
+
// (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
|
|
488
|
+
const costDelta = controller.isPricedModel(ref)
|
|
489
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
|
|
490
|
+
: 0;
|
|
491
|
+
dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
|
|
452
492
|
break;
|
|
493
|
+
}
|
|
453
494
|
case 'dispatch_end':
|
|
454
495
|
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
455
496
|
break;
|
|
@@ -491,6 +532,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
491
532
|
}
|
|
492
533
|
});
|
|
493
534
|
}, [controller]);
|
|
535
|
+
// --- Fast-tier event stream (fast-model-tier §5.3). --------------------
|
|
536
|
+
//
|
|
537
|
+
// A FOURTH SUBSCRIPTION, for the reason the team and todo ones record:
|
|
538
|
+
// `FastEvent` is deliberately not a member of core's `AgentEvent` union
|
|
539
|
+
// (D-10), so it cannot arrive through `controller.subscribe`.
|
|
540
|
+
//
|
|
541
|
+
// `subscribeFast` returns a no-op unsubscribe when the tier was off at
|
|
542
|
+
// construction, so this can subscribe unconditionally and an ordinary session
|
|
543
|
+
// pays one closure for the whole run.
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
// Seed the chip from the tier resolved at construction, so a fast-enabled
|
|
546
|
+
// session shows it on the first frame rather than after the first review.
|
|
547
|
+
const initial = controller.getFastStatus();
|
|
548
|
+
if (initial.registered)
|
|
549
|
+
dispatch({ type: 'fastTier', snapshot: initial.snapshot });
|
|
550
|
+
return controller.subscribeFast((event) => {
|
|
551
|
+
switch (event.type) {
|
|
552
|
+
case 'review_start':
|
|
553
|
+
dispatch({
|
|
554
|
+
type: 'fastStart',
|
|
555
|
+
index: event.index,
|
|
556
|
+
turn: event.turn,
|
|
557
|
+
model: controller.getFastStatus().snapshot.model,
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
case 'review_end':
|
|
561
|
+
dispatch({ type: 'fastEnd', review: event.review });
|
|
562
|
+
// The card settled, so the chip's `inFlight` and the session totals
|
|
563
|
+
// both moved. One dispatch rather than two: `fastTier` carries them.
|
|
564
|
+
dispatch({ type: 'fastTier', snapshot: controller.getFastStatus().snapshot });
|
|
565
|
+
break;
|
|
566
|
+
case 'usage': {
|
|
567
|
+
// Review spend is REAL SPEND at the FAST model's price (§3.6 / AC-18).
|
|
568
|
+
const ref = controller.getFastStatus().tier;
|
|
569
|
+
const costDelta = ref.ok && controller.isPricedModel(ref.ref)
|
|
570
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref.ref).cost)
|
|
571
|
+
: 0;
|
|
572
|
+
dispatch({ type: 'fastUsage', usage: event.usage, costDelta });
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case 'tier_changed':
|
|
576
|
+
dispatch({ type: 'fastTier', snapshot: event.snapshot });
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}, [controller]);
|
|
581
|
+
// --- Live tool output (agent-activity-presentation-live §3.3.2). --------
|
|
582
|
+
//
|
|
583
|
+
// A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
|
|
584
|
+
// COALESCER (D-34 / P0-2). The three above each carry a discrete act at human
|
|
585
|
+
// frequency — a roster change, a `todo_write`, a review verdict — and each has
|
|
586
|
+
// a comment refusing the buffer for that reason. This one carries a STREAM at
|
|
587
|
+
// pipe speed: the producer is a child process's stdout, so dispatching per
|
|
588
|
+
// chunk would be one React commit per `read()`.
|
|
589
|
+
//
|
|
590
|
+
// THE EFFECT PUSHES INTO `pending.current` AND ARMS `flushTimer` ITSELF.
|
|
591
|
+
// There is no "buffered path for every other action" to fall into —
|
|
592
|
+
// `App.tsx:436-447` buffers exactly `textDelta` / `thinkingDelta` and
|
|
593
|
+
// dispatches everything else immediately — so writing this the way its three
|
|
594
|
+
// siblings are written leaves `mergeDeltas`'s new clause dead code and R-1
|
|
595
|
+
// with one bound instead of two. AC-38 is the pin, mutation-checked against
|
|
596
|
+
// the direct-dispatch form.
|
|
597
|
+
//
|
|
598
|
+
// `Date.now()` IS READ HERE, in the listener, not in the reducer, which is
|
|
599
|
+
// what keeps `viewReducer` pure.
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
return controller.subscribeToolOutput(({ toolCallId, rows }) => {
|
|
602
|
+
pending.current.push({ type: 'toolOutputDelta', toolCallId, rows, at: Date.now() });
|
|
603
|
+
if (!flushTimer.current) {
|
|
604
|
+
// READ FROM THE REF AT ARM TIME, for the reason `App.tsx:439-441`
|
|
605
|
+
// records: this effect's dependency list never changes, so a captured
|
|
606
|
+
// number would be pinned to the mount render and the governor's ladder
|
|
607
|
+
// would move nothing.
|
|
608
|
+
flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}, [controller, flushPending, governorInterval]);
|
|
494
612
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
495
613
|
useEffect(() => {
|
|
496
614
|
if (state.status !== 'running') {
|
|
@@ -503,6 +621,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
503
621
|
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
504
622
|
return () => clearInterval(id);
|
|
505
623
|
}, [state.status]);
|
|
624
|
+
// --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
|
|
625
|
+
//
|
|
626
|
+
// ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
|
|
627
|
+
// so an idle session, and even a session that is retrying but has already fired
|
|
628
|
+
// its next attempt, has NO timer at all. 1 Hz rather than the elapsed timer's
|
|
629
|
+
// 200 ms because the card shows whole seconds and a five-times-finer tick would
|
|
630
|
+
// buy four identical frames.
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
if (state.retry?.phase !== 'waiting')
|
|
633
|
+
return undefined;
|
|
634
|
+
const id = setInterval(() => dispatch({ type: 'retryTick' }), 1000);
|
|
635
|
+
return () => clearInterval(id);
|
|
636
|
+
}, [state.retry?.phase]);
|
|
506
637
|
// --- Toast auto-dismiss (single effect keyed on the toast list). -------
|
|
507
638
|
useEffect(() => {
|
|
508
639
|
for (const t of state.toasts) {
|
|
@@ -803,6 +934,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
803
934
|
controller.setModel(values.provider, values.model, values.baseUrl || undefined);
|
|
804
935
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
805
936
|
controller.setMaxTokens(cap.applied);
|
|
937
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
|
|
938
|
+
// documents: persisting alone reports success and changes nothing until the
|
|
939
|
+
// next launch. `thinkingVisible` lives in `ViewState`, not in the config the
|
|
940
|
+
// controller holds, so the live half is a dispatch rather than a setter —
|
|
941
|
+
// and it is a no-op when the row already agrees with the current state.
|
|
942
|
+
if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
|
|
943
|
+
dispatch({ type: 'toggleThinking' });
|
|
944
|
+
}
|
|
806
945
|
const key = values.apiKey.trim();
|
|
807
946
|
if (key.length > 0) {
|
|
808
947
|
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
@@ -817,6 +956,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
817
956
|
model: values.model,
|
|
818
957
|
baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
|
|
819
958
|
thinkingLevel: values.thinkingLevel,
|
|
959
|
+
showThinking: values.showThinking === 'on',
|
|
960
|
+
// PERSIST ONLY, with no paired live setter — the store is allocated in
|
|
961
|
+
// `AgentController`'s constructor, so this row takes effect on the next
|
|
962
|
+
// launch. That is what manual row 8 asks for, and it is why the seed above
|
|
963
|
+
// reads `cfg` rather than `ViewState`.
|
|
964
|
+
liveToolOutput: values.liveToolOutput === 'on',
|
|
820
965
|
// Omitted entirely when the field was unusable, so the previous value
|
|
821
966
|
// survives instead of being overwritten with a guess.
|
|
822
967
|
...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
|
|
@@ -827,6 +972,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
827
972
|
// merges it — a shallow merge here would reset `redactSecrets` every time
|
|
828
973
|
// someone touched the level.
|
|
829
974
|
patch.log = { level: values.logLevel };
|
|
975
|
+
// --- Fast tier (fast-model-tier §4.5) ---------------------------------
|
|
976
|
+
//
|
|
977
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
|
|
978
|
+
// panel` documents (P1-2): `App` reads `controller.getConfig()` at render
|
|
979
|
+
// time, so persisting alone reports success and changes nothing until the
|
|
980
|
+
// next launch, while the setter alone forgets by morning.
|
|
981
|
+
//
|
|
982
|
+
// `setFastConfig` also re-resolves the tier, which matters HERE more than
|
|
983
|
+
// anywhere: this same Enter press may have changed the main provider, the
|
|
984
|
+
// main model or the API key, and `fast.provider: ''` inherits from all
|
|
985
|
+
// three (§3.2 rule 8 / RV-3).
|
|
986
|
+
const fastPatch = readFastSettings(values);
|
|
987
|
+
controller.setFastConfig(fastPatch);
|
|
988
|
+
patch.fast = fastPatch;
|
|
830
989
|
persistConfig(patch);
|
|
831
990
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
832
991
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -864,26 +1023,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
864
1023
|
}
|
|
865
1024
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
866
1025
|
};
|
|
867
|
-
// --- Mouse wheel
|
|
868
|
-
// hook (D-13); only the wiring it cannot own stays here.
|
|
869
|
-
// fine — the hook re-reads its options through a ref every
|
|
870
|
-
// `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
871
|
-
// reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1026
|
+
// --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
|
|
1027
|
+
// coalescer live in the hook (D-13); only the wiring it cannot own stays here.
|
|
1028
|
+
// Inline arrows are fine — the hook re-reads its options through a ref every
|
|
1029
|
+
// render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
1030
|
+
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
872
1031
|
const { clearCoalescer } = useWheelRouting({
|
|
873
1032
|
mouseSource,
|
|
874
|
-
geometryRef,
|
|
875
|
-
geometryOverride,
|
|
876
1033
|
getOverlay: () => stateRef.current.overlay,
|
|
877
1034
|
onScroll: scrollBy,
|
|
878
1035
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
879
|
-
onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
|
|
880
1036
|
});
|
|
881
1037
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
882
1038
|
// offset over would open Help mid-list for no reason the user can see.
|
|
883
1039
|
//
|
|
884
1040
|
// `clearCoalescer()` rides along here rather than adding a second subscriber
|
|
885
|
-
// to the same transition (§4.
|
|
886
|
-
// transcript scroll must not survive an overlay
|
|
1041
|
+
// to the same transition (wheel-scrolls-transcript-only §4.3): an
|
|
1042
|
+
// accumulated-but-unflushed transcript scroll must not survive an overlay
|
|
1043
|
+
// opening.
|
|
887
1044
|
useEffect(() => {
|
|
888
1045
|
setOverlayScroll(0);
|
|
889
1046
|
clearCoalescer();
|
|
@@ -991,7 +1148,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
991
1148
|
}
|
|
992
1149
|
}
|
|
993
1150
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
1151
|
+
const revealing = !stateRef.current.thinkingVisible;
|
|
994
1152
|
dispatch({ type: 'toggleThinking' });
|
|
1153
|
+
// THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
|
|
1154
|
+
// Inline mode cannot repaint what `<Static>` has already printed, so the
|
|
1155
|
+
// toast says what it DID rather than what the user hoped. The hide
|
|
1156
|
+
// direction needs no qualifier: hiding applies to everything drawn from
|
|
1157
|
+
// here on, and nothing is claimed about scrollback either way.
|
|
1158
|
+
if (!revealing)
|
|
1159
|
+
toast('info', 'Thinking hidden.');
|
|
1160
|
+
else if (fullscreen)
|
|
1161
|
+
toast('info', 'Thinking shown.');
|
|
1162
|
+
else
|
|
1163
|
+
toast('info', 'Thinking shown for new output.');
|
|
995
1164
|
return;
|
|
996
1165
|
}
|
|
997
1166
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1056,6 +1225,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1056
1225
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
1057
1226
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1058
1227
|
: 0;
|
|
1228
|
+
// --- The activity line's seed (§3.2.3 / P1-7). --------------------------
|
|
1229
|
+
//
|
|
1230
|
+
// RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
|
|
1231
|
+
// word) and the PREVIOUS run's start on every run after, so every run opens on
|
|
1232
|
+
// a wrong phrase for one frame and then jumps — a flicker at the start of
|
|
1233
|
+
// every single turn, which is the one moment this feature exists to make feel
|
|
1234
|
+
// calm.
|
|
1235
|
+
const running = state.status === 'running';
|
|
1236
|
+
if (running && runStartedAt.current === 0)
|
|
1237
|
+
runStartedAt.current = Date.now();
|
|
1238
|
+
if (!running && runStartedAt.current !== 0)
|
|
1239
|
+
runStartedAt.current = 0;
|
|
1240
|
+
// --- The running tool (agent-activity-presentation-live §3.4 / L4). -----
|
|
1241
|
+
//
|
|
1242
|
+
// A REVERSE LOOP WITH AN EARLY EXIT, NOT `.filter(...).pop()` (P2-3).
|
|
1243
|
+
// `state.entries` is bounded by `transcriptRetain`, which DEFAULTS TO 1000,
|
|
1244
|
+
// and this runs in render scope at up to 30 fps — an allocating full scan
|
|
1245
|
+
// there is the exact cost `tui-render-performance` exists to remove. The
|
|
1246
|
+
// running tool, when there is one, is within a few entries of the end.
|
|
1247
|
+
let runningTool;
|
|
1248
|
+
if (running) {
|
|
1249
|
+
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
1250
|
+
const e = state.entries[i];
|
|
1251
|
+
if (e.kind === 'tool' && e.status === 'running') {
|
|
1252
|
+
runningTool = e.name;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// WALL-CLOCK SECONDS, and the 200 ms ticker above is what causes the render
|
|
1258
|
+
// that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
|
|
1259
|
+
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1260
|
+
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1261
|
+
const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
|
|
1059
1262
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1060
1263
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1061
1264
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
@@ -1100,18 +1303,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1100
1303
|
model: cfg.model,
|
|
1101
1304
|
baseUrl: cfg.baseUrl ?? '',
|
|
1102
1305
|
thinkingLevel: cfg.thinkingLevel,
|
|
1306
|
+
// SEEDED AND READ BACK, both halves (§4.3): omitting either is the
|
|
1307
|
+
// documented silent half of adding a row — the field renders empty and
|
|
1308
|
+
// an untouched save writes a value the user never chose.
|
|
1309
|
+
//
|
|
1310
|
+
// FROM `ViewState`, NOT FROM `cfg`. `thinkingVisible` is session state
|
|
1311
|
+
// that `Ctrl+T` also writes, and there is no controller setter to keep
|
|
1312
|
+
// `cfg` in step — so seeding from the config would show the LAUNCH
|
|
1313
|
+
// value under a transcript that has been toggled since, and an
|
|
1314
|
+
// untouched save would then quietly toggle it back.
|
|
1315
|
+
showThinking: state.thinkingVisible ? 'on' : 'off',
|
|
1316
|
+
// FROM `cfg`, unlike its neighbour, and the difference is real: the
|
|
1317
|
+
// live tail is decided at construction (the store is allocated there
|
|
1318
|
+
// and there is no setter), so the config IS the current value and
|
|
1319
|
+
// nothing can have toggled it since launch.
|
|
1320
|
+
liveToolOutput: cfg.liveToolOutput ? 'on' : 'off',
|
|
1103
1321
|
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
1104
1322
|
// is indistinguishable from AUTO and is not a state this can be in.
|
|
1105
1323
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
1106
1324
|
apiKey: '',
|
|
1107
1325
|
logLevel: cfg.log.level,
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1326
|
+
// The four fast rows, seeded through the pure mapper so the screen and
|
|
1327
|
+
// the save handler cannot disagree about what `(inherit)` means.
|
|
1328
|
+
...fastSettingsFrom(cfg.fast),
|
|
1329
|
+
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
|
|
1330
|
+
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1331
|
+
// while the screen is open shows the new value on the next paint.
|
|
1332
|
+
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
1115
1333
|
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
1116
1334
|
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
1117
1335
|
// the height from `frameHeight()` like everything else, because this is the
|
|
@@ -1148,13 +1366,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1148
1366
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1149
1367
|
offset: scrolledLines,
|
|
1150
1368
|
};
|
|
1151
|
-
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, 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, theme: theme, caps: caps }), overlayNode] }));
|
|
1369
|
+
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] }));
|
|
1152
1370
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1153
1371
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1154
1372
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
1155
1373
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
1156
|
-
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,
|
|
1157
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols,
|
|
1374
|
+
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' }) }) }));
|
|
1375
|
+
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,
|
|
1376
|
+
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1377
|
+
// the full-screen branch replaces the whole viewport with the overlay,
|
|
1378
|
+
// and a working line under a settings screen is noise attached to a
|
|
1379
|
+
// surface the run is not visible on. (Inline overlays are part of the
|
|
1380
|
+
// document flow, so there the suppression is conservative rather than
|
|
1381
|
+
// necessary — P2-5.)
|
|
1382
|
+
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1158
1383
|
? {
|
|
1159
1384
|
teamActive: {
|
|
1160
1385
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1164,5 +1389,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1164
1389
|
}
|
|
1165
1390
|
: {}), ...(state.todos && !showRail
|
|
1166
1391
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1392
|
+
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.retry
|
|
1393
|
+
? {
|
|
1394
|
+
retryActive: {
|
|
1395
|
+
attempt: state.retry.attempt,
|
|
1396
|
+
max: state.retry.maxRetries,
|
|
1397
|
+
// Recomputed at RENDER from the absolute `resumeAt`, which is
|
|
1398
|
+
// what keeps the chip and the transcript card showing the same
|
|
1399
|
+
// number without either of them owning a second clock.
|
|
1400
|
+
secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
|
|
1401
|
+
},
|
|
1402
|
+
}
|
|
1167
1403
|
: {}) }) }));
|
|
1168
1404
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bottom status row — the one already-budgeted row above the composer, now
|
|
3
|
+
* shared by two occupants (agent-activity-presentation §3.2.3 / D-17 / P0-1).
|
|
4
|
+
*
|
|
5
|
+
* THE ACTIVITY LINE IS NOT A NEW `AppShell` SLOT, AND THIS FILE IS WHY.
|
|
6
|
+
*
|
|
7
|
+
* `viewportRows()` (`layout/budget.ts`) is the ONE function that says how many
|
|
8
|
+
* rows the viewport gets, and it enumerates exactly `header + toast + composer +
|
|
9
|
+
* status`. In full-screen `AppShell`'s root is `height={frameHeight(rows)}` with
|
|
10
|
+
* `overflow="hidden"`, the bottom chrome box is `flexShrink={0}` and the viewport
|
|
11
|
+
* box is its only `flexShrink={1}` child — so an extra chrome row does not make
|
|
12
|
+
* the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
|
|
13
|
+
* `viewportRows()` keeps returning the old number to `ScrollViewport`,
|
|
14
|
+
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`.
|
|
15
|
+
* The transcript would draw one row shorter than every consumer believes, ONLY
|
|
16
|
+
* WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
|
|
17
|
+
* `agent_end`.
|
|
18
|
+
*
|
|
19
|
+
* `ToastStack.tsx` already legislates this for itself: *"in `fullscreen` this
|
|
20
|
+
* ALWAYS occupies exactly one row — an empty row when there is nothing to say.
|
|
21
|
+
* Rendering conditionally would make the viewport height (and therefore the whole
|
|
22
|
+
* transcript) jump every time a toast appears or expires."* A run starts and ends
|
|
23
|
+
* far more often than a toast appears and expires, so the activity line is the
|
|
24
|
+
* STRONGER case for that rule, not an exception to it.
|
|
25
|
+
*
|
|
26
|
+
* Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
|
|
27
|
+
* UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
|
|
28
|
+
*
|
|
29
|
+
* PRECEDENCE: a toast wins. A transient ack is a RESPONSE TO THE USER, and the
|
|
30
|
+
* row nearest the input belongs to it.
|
|
31
|
+
*/
|
|
32
|
+
import React from 'react';
|
|
33
|
+
import type { Theme } from './theme.js';
|
|
34
|
+
import type { Toast } from '../agent/reducer.js';
|
|
35
|
+
import type { RenderMode } from './layout/frame.js';
|
|
36
|
+
export interface BottomStatusRowProps {
|
|
37
|
+
mode: RenderMode;
|
|
38
|
+
toasts: Toast[];
|
|
39
|
+
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
40
|
+
activity: React.ReactNode;
|
|
41
|
+
theme: Theme;
|
|
42
|
+
}
|
|
43
|
+
export declare function BottomStatusRow({ mode, toasts, activity, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { ToastStack } from './ToastStack.js';
|
|
4
|
+
export function BottomStatusRow({ mode, toasts, activity, theme, }) {
|
|
5
|
+
// A toast wins the row; so does an empty toast stack in full-screen, whose
|
|
6
|
+
// blank `<Text> </Text>` IS the budgeted row and must keep being emitted.
|
|
7
|
+
if (toasts.length > 0 || !activity) {
|
|
8
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
9
|
+
}
|
|
10
|
+
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
11
|
+
// branch is already a conditional 0-3 row stack, and this is simply one more
|
|
12
|
+
// conditional row above it.
|
|
13
|
+
return _jsx(Box, { flexShrink: 0, children: activity });
|
|
14
|
+
}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -35,14 +35,9 @@ export interface ComposerProps {
|
|
|
35
35
|
/** Mirror of the controller's effective mode; drives the chip and the border. */
|
|
36
36
|
agentMode: AgentMode;
|
|
37
37
|
popupMaxRows?: number;
|
|
38
|
-
/** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
|
|
39
|
-
historyIntent?: {
|
|
40
|
-
dir: 'up' | 'down';
|
|
41
|
-
nonce: number;
|
|
42
|
-
};
|
|
43
38
|
theme: Theme;
|
|
44
39
|
caps: TermCapabilities;
|
|
45
40
|
onSubmit: (text: string) => void;
|
|
46
41
|
onHelp?: () => void;
|
|
47
42
|
}
|
|
48
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows,
|
|
43
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
|
|
|
56
56
|
'? help',
|
|
57
57
|
].join(dot);
|
|
58
58
|
}
|
|
59
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows,
|
|
59
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
|
|
60
60
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
61
|
const onDraftChange = useCallback((next) => setHasDraft(next), []);
|
|
62
62
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
73
|
: theme.idleBorder ?? theme.border;
|
|
74
74
|
const glyphs = pickGlyphs(caps);
|
|
75
75
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
77
77
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
78
|
// costs zero rows on a 24-row terminal.
|
|
79
79
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -55,20 +55,6 @@ interface PromptInputProps {
|
|
|
55
55
|
};
|
|
56
56
|
/** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
|
|
57
57
|
onDraftChange?: (hasDraft: boolean) => void;
|
|
58
|
-
/**
|
|
59
|
-
* One prompt-history step, pushed by the wheel router when a notch lands on
|
|
60
|
-
* the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
|
|
61
|
-
* so two notches in the same direction are two steps.
|
|
62
|
-
*
|
|
63
|
-
* Applied through the SAME `verticalOrHistory` / popup-selection branches the
|
|
64
|
-
* keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
|
|
65
|
-
* apart, and the "never clobber an in-progress draft" guard protects the
|
|
66
|
-
* wheel for free.
|
|
67
|
-
*/
|
|
68
|
-
historyIntent?: {
|
|
69
|
-
dir: 'up' | 'down';
|
|
70
|
-
nonce: number;
|
|
71
|
-
};
|
|
72
58
|
}
|
|
73
59
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
74
60
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
@@ -98,5 +84,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
98
84
|
end: number;
|
|
99
85
|
query: string;
|
|
100
86
|
} | null;
|
|
101
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange,
|
|
87
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
102
88
|
export {};
|