@aragon-agent/cli 0.5.4 → 0.5.8
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 +747 -577
- package/README.md +1163 -995
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +53 -0
- package/dist/agent/reducer.js +145 -14
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +76 -3
- package/dist/commands/builtins.js +171 -0
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/env.d.ts +3 -1
- package/dist/config/env.js +74 -1
- package/dist/config/load.d.ts +41 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +108 -8
- package/dist/config/schema.js +81 -6
- package/dist/config/store.js +10 -1
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +21 -2
- package/dist/team/subagent.js +11 -0
- package/dist/todo/follow-through.d.ts +117 -0
- package/dist/todo/follow-through.js +186 -0
- package/dist/todo/limits.d.ts +114 -0
- package/dist/todo/limits.js +114 -0
- package/dist/todo/normalize.d.ts +33 -0
- package/dist/todo/normalize.js +117 -0
- package/dist/todo/panel-rows.d.ts +53 -0
- package/dist/todo/panel-rows.js +103 -0
- package/dist/todo/prompt.d.ts +54 -0
- package/dist/todo/prompt.js +76 -0
- package/dist/todo/store.d.ts +86 -0
- package/dist/todo/store.js +183 -0
- package/dist/todo/todo-tool.d.ts +38 -0
- package/dist/todo/todo-tool.js +134 -0
- package/dist/todo/types.d.ts +62 -0
- package/dist/todo/types.js +12 -0
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.js +13 -0
- package/dist/ui/App.js +328 -14
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/StatusBar.d.ts +31 -0
- package/dist/ui/StatusBar.js +7 -2
- package/dist/ui/TodoPanel.d.ts +48 -0
- package/dist/ui/TodoPanel.js +74 -0
- package/dist/ui/TodoStrip.d.ts +36 -0
- package/dist/ui/TodoStrip.js +32 -0
- package/dist/ui/Transcript.d.ts +56 -4
- package/dist/ui/Transcript.js +123 -11
- package/dist/ui/entries/AssistantEntry.d.ts +18 -1
- package/dist/ui/entries/AssistantEntry.js +33 -3
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +22 -1
- package/dist/ui/entries/TodoCard.d.ts +39 -0
- package/dist/ui/entries/TodoCard.js +57 -0
- package/dist/ui/entries/ToolCard.d.ts +13 -1
- package/dist/ui/entries/ToolCard.js +39 -5
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +12 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/AppShell.d.ts +34 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +10 -4
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +204 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -0
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +5 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/package.json +3 -2
package/dist/ui/App.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
-
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
14
|
+
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
15
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
16
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
17
|
import { bumpSubmitCount, setMouseNoticeSeen } from '../config/ui-state.js';
|
|
@@ -29,10 +29,18 @@ import { PromptInput } from './PromptInput.js';
|
|
|
29
29
|
import { Composer } from './Composer.js';
|
|
30
30
|
import { StatusBar } from './StatusBar.js';
|
|
31
31
|
import { TeamPanel } from './TeamPanel.js';
|
|
32
|
+
import { TodoPanel } from './TodoPanel.js';
|
|
33
|
+
import { TodoStrip } from './TodoStrip.js';
|
|
32
34
|
import { ToastStack } from './ToastStack.js';
|
|
33
35
|
import { AppShell } from './layout/AppShell.js';
|
|
34
36
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
37
|
+
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
38
|
+
import { TODO_LIMITS } from '../todo/limits.js';
|
|
39
|
+
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
35
40
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
41
|
+
import { useHeightStore } from './use-height-store.js';
|
|
42
|
+
import { useRenderGovernor } from './use-render-governor.js';
|
|
43
|
+
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
36
44
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
37
45
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
38
46
|
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
@@ -85,13 +93,23 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
85
93
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
86
94
|
};
|
|
87
95
|
}
|
|
88
|
-
/** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
|
|
89
|
-
const COALESCE_MS = 33;
|
|
90
96
|
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
|
|
91
97
|
const { exit } = useApp();
|
|
92
98
|
const { stdout } = useStdout();
|
|
93
99
|
const fullscreen = mode === 'fullscreen';
|
|
94
100
|
const { rows, cols } = useTerminalSize();
|
|
101
|
+
// --- Render governor (tui-render-performance L4). ------------------------
|
|
102
|
+
//
|
|
103
|
+
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
104
|
+
// `performance.now()` during render and reads it again in a layout effect, so
|
|
105
|
+
// the span it measures is only the whole commit if nothing precedes it here.
|
|
106
|
+
const cfg = controller.getConfig();
|
|
107
|
+
const governor = useRenderGovernor({
|
|
108
|
+
enabled: cfg.renderGovernor,
|
|
109
|
+
maxIntervalMs: cfg.maxRenderIntervalMs,
|
|
110
|
+
});
|
|
111
|
+
/** The transcript height cache, owned here and read by `TranscriptList` (L3). */
|
|
112
|
+
const heights = useHeightStore();
|
|
95
113
|
const [state, dispatch] = useReducer(viewReducer, undefined, initialViewState);
|
|
96
114
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
97
115
|
const [confirmState, setConfirmState] = useState(null);
|
|
@@ -119,7 +137,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
119
137
|
// the completion list are memoized against it, so a skill installed mid-session
|
|
120
138
|
// gets its `/<name>` command and its autocomplete entry immediately.
|
|
121
139
|
const [skillsNonce, setSkillsNonce] = useState(0);
|
|
122
|
-
const cfg = controller.getConfig();
|
|
123
140
|
const caps = useMemo(() => {
|
|
124
141
|
const detected = detectCapabilities(process.env, stdout);
|
|
125
142
|
return {
|
|
@@ -144,6 +161,80 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
144
161
|
const pendingHuman = useRef(new Set());
|
|
145
162
|
/** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
|
|
146
163
|
const geometryRef = useRef(UNMEASURED_GEOMETRY);
|
|
164
|
+
/**
|
|
165
|
+
* What `/perf` reports. Assigned during render, exactly like `stateRef` above,
|
|
166
|
+
* because a slash command runs outside the render pass and a value captured in
|
|
167
|
+
* an effect closure would be one commit stale on every readout.
|
|
168
|
+
*/
|
|
169
|
+
const perfRef = useRef(null);
|
|
170
|
+
/**
|
|
171
|
+
* How many entries `TranscriptList` actually mounted this frame.
|
|
172
|
+
*
|
|
173
|
+
* A ref written during the child's render, not state: this is the number
|
|
174
|
+
* `render-budget.test.tsx` gates on (AC-1 / AC-9), and turning it into state
|
|
175
|
+
* would make reading the render budget cost a render.
|
|
176
|
+
*/
|
|
177
|
+
const mountedCount = useRef(0);
|
|
178
|
+
// --- Follow-through (todo-plan-followthrough §3.4). ---------------------
|
|
179
|
+
//
|
|
180
|
+
// THREE REFS AND NO VIEW STATE, DELIBERATELY (§5.2). Every field added to
|
|
181
|
+
// `ViewState` is a field `restoreEntries`, `/clear`, `/reset` and the session
|
|
182
|
+
// round-trip each have to have an opinion about, and none of them has an
|
|
183
|
+
// opinion about a 3-second timer.
|
|
184
|
+
const budgetRef = useRef(emptyBudget());
|
|
185
|
+
const followTimer = useRef(null);
|
|
186
|
+
/** True while the run in flight was started by the grace timer, not the user. */
|
|
187
|
+
const autoContinuationRef = useRef(false);
|
|
188
|
+
/**
|
|
189
|
+
* WHY THE END REASON TRAVELS IN A REF THIS FEATURE OWNS (C-11 / D-17).
|
|
190
|
+
*
|
|
191
|
+
* `ViewState.aborted` / `.errorNoticed` are the DEFINITION of the end reason —
|
|
192
|
+
* `runEnd` reads exactly that pair for its own silent-failure guard — but the
|
|
193
|
+
* reducer is not a safe CHANNEL for it, because the answer is needed inside a
|
|
194
|
+
* controller callback rather than during a render.
|
|
195
|
+
*
|
|
196
|
+
* Reading `stateRef.current` there happens to work under Ink 5 only because
|
|
197
|
+
* Ink mounts a LEGACY root (`ink.js`: `createContainer(rootNode, 0 …)`), so a
|
|
198
|
+
* dispatch from a promise context flushes synchronously. Under a concurrent
|
|
199
|
+
* root — an Ink major bump, or its `experimental` renderer — React 18's
|
|
200
|
+
* automatic batching returns, and a provider stream error emitted as
|
|
201
|
+
* `message_update` immediately before `agent_end` (core throws in the loop and
|
|
202
|
+
* emits `agent_end` from `runLoopWithLifecycle`'s `finally`, microtasks apart)
|
|
203
|
+
* would still read `errorNoticed: false`. That auto-continues into a run that
|
|
204
|
+
* just failed authentication: silent, and billable.
|
|
205
|
+
*
|
|
206
|
+
* So this ref is mutated in the SAME SYNCHRONOUS CALLBACK that dispatches the
|
|
207
|
+
* corresponding action, and the reducer fields stay as the cross-check.
|
|
208
|
+
*/
|
|
209
|
+
const endReasonRef = useRef({
|
|
210
|
+
aborted: false,
|
|
211
|
+
errored: false,
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
215
|
+
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
216
|
+
*
|
|
217
|
+
* Three callers and there must be no fourth: `Esc` in the idle branch, the
|
|
218
|
+
* first line of `submitMessage`, and the subscription effect's cleanup.
|
|
219
|
+
*/
|
|
220
|
+
const cancelFollowThrough = useCallback(() => {
|
|
221
|
+
if (followTimer.current) {
|
|
222
|
+
clearTimeout(followTimer.current);
|
|
223
|
+
followTimer.current = null;
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
226
|
+
/**
|
|
227
|
+
* `submitMessage` as of the LATEST render, for the grace timer.
|
|
228
|
+
*
|
|
229
|
+
* The subscription effect below never re-runs (its deps are stable by
|
|
230
|
+
* construction), so a direct call would close over the mount render's binding
|
|
231
|
+
* forever. That binding happens to be functionally correct today because
|
|
232
|
+
* `submitMessage` reads everything mutable through refs — which is exactly the
|
|
233
|
+
* kind of "happens to be correct" this feature has already had to unwind once
|
|
234
|
+
* (P1-1, P1-6). Assigned during render, the same discipline `stateRef` uses
|
|
235
|
+
* twenty lines up.
|
|
236
|
+
*/
|
|
237
|
+
const submitRef = useRef(() => { });
|
|
147
238
|
/**
|
|
148
239
|
* THE SINGLE SETTLE PATH for a human request. Idempotent per entry, and it
|
|
149
240
|
* clears the overlay as its last act.
|
|
@@ -184,6 +275,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
184
275
|
// --- Streaming coalescer + controller subscription. --------------------
|
|
185
276
|
const pending = useRef([]);
|
|
186
277
|
const flushTimer = useRef(null);
|
|
278
|
+
/** Alias so the subscription effect below reads one stable ref, not a hook. */
|
|
279
|
+
const governorInterval = governor.intervalMs;
|
|
187
280
|
const flushPending = useCallback(() => {
|
|
188
281
|
if (flushTimer.current) {
|
|
189
282
|
clearTimeout(flushTimer.current);
|
|
@@ -200,10 +293,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
200
293
|
const unsubscribe = controller.subscribe((event) => {
|
|
201
294
|
const cost = controller.getModelInfo().cost;
|
|
202
295
|
for (const action of reduceEvent(event, cost)) {
|
|
296
|
+
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
297
|
+
// `ViewState.errorNoticed`, rather than read back from the reducer one
|
|
298
|
+
// commit later (C-11). This is the whole of the batching-independence
|
|
299
|
+
// guarantee: `agent_end` can follow this action in the same tick with no
|
|
300
|
+
// render in between, and AC-40 asserts exactly that sequence.
|
|
301
|
+
if (action.type === 'notice' && action.level === 'error') {
|
|
302
|
+
endReasonRef.current.errored = true;
|
|
303
|
+
}
|
|
203
304
|
if (action.type === 'textDelta' || action.type === 'thinkingDelta') {
|
|
204
305
|
pending.current.push(action);
|
|
205
306
|
if (!flushTimer.current) {
|
|
206
|
-
|
|
307
|
+
// READ FROM THE REF AT ARM TIME (L4). This effect's dependency list
|
|
308
|
+
// never changes by construction, so a captured number would be
|
|
309
|
+
// pinned to the mount render and the ladder would move nothing.
|
|
310
|
+
flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
|
|
207
311
|
}
|
|
208
312
|
}
|
|
209
313
|
else {
|
|
@@ -220,6 +324,69 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
220
324
|
// Without it, an aborted run leaves a plan card on screen whose `a` key
|
|
221
325
|
// silently flips the session to build, with no visible cause.
|
|
222
326
|
cancelPendingHuman();
|
|
327
|
+
// --- Follow-through (todo-plan-followthrough §3.4). ------------------
|
|
328
|
+
//
|
|
329
|
+
// Round 1 printed one notice here and stopped, under a good argument
|
|
330
|
+
// ("auto-continuation is an unbounded cost loop wearing a helpful hat").
|
|
331
|
+
// That judgement is not overturned: the loop is BOUNDED first, by two
|
|
332
|
+
// structural counters, and only then permitted — opt-in, and off by
|
|
333
|
+
// default, so `followThrough: 'notify'` reproduces the old notice byte for
|
|
334
|
+
// byte (AC-1).
|
|
335
|
+
const todos = controller.getTodoSnapshot();
|
|
336
|
+
budgetRef.current = advanceBudget(budgetRef.current, todos, autoContinuationRef.current);
|
|
337
|
+
autoContinuationRef.current = false;
|
|
338
|
+
const decision = decideFollowThrough({
|
|
339
|
+
// FROM THE CONTROLLER, NOT FROM `cfg` (P1-1). This effect's dependency
|
|
340
|
+
// list never changes, so it closes over the `cfg` object from the render
|
|
341
|
+
// in which it mounted — and `setTodoConfig` builds a NEW object, which
|
|
342
|
+
// that closure will never see. `cfg.todo.followThrough` would therefore
|
|
343
|
+
// be pinned to the launch value for the whole session and
|
|
344
|
+
// `/todo follow auto` would report success and do nothing. Note the
|
|
345
|
+
// asymmetry with `showStrip` further down, which is right to use `cfg`
|
|
346
|
+
// because it runs in render scope where `cfg` is re-read every frame.
|
|
347
|
+
mode: controller.getTodoConfig().followThrough,
|
|
348
|
+
snapshot: todos,
|
|
349
|
+
runEnd: { ...endReasonRef.current },
|
|
350
|
+
budget: budgetRef.current,
|
|
351
|
+
interactive: true,
|
|
352
|
+
});
|
|
353
|
+
endReasonRef.current = { aborted: false, errored: false };
|
|
354
|
+
if (decision.kind === 'notify') {
|
|
355
|
+
dispatch({ type: 'notice', level: decision.level, text: decision.text });
|
|
356
|
+
}
|
|
357
|
+
else if (decision.kind === 'continue') {
|
|
358
|
+
dispatch({ type: 'notice', level: 'info', text: decision.notice });
|
|
359
|
+
// NO LIVE COUNTDOWN (D-10). The notice is written once and does not
|
|
360
|
+
// tick: a 10 Hz re-render of the whole frame to animate a number is the
|
|
361
|
+
// repaint cost this package refuses elsewhere, and Ink's two dedupe
|
|
362
|
+
// gates mean the alternative is not free either.
|
|
363
|
+
cancelFollowThrough(); // I-3: at most one armed continuation, ever
|
|
364
|
+
followTimer.current = setTimeout(() => {
|
|
365
|
+
followTimer.current = null;
|
|
366
|
+
// TWO RE-CHECKS AT FIRE TIME, not only at decision time. The grace
|
|
367
|
+
// window is 3 s and the user can type in it (C-6): without the first,
|
|
368
|
+
// a fired continuation would either be swallowed into
|
|
369
|
+
// `controller.steer()` (the quiet failure) or reach
|
|
370
|
+
// `controller.prompt()`, which REJECTS while running.
|
|
371
|
+
if (controller.isRunning())
|
|
372
|
+
return;
|
|
373
|
+
const live = controller.getTodoSnapshot();
|
|
374
|
+
if (!live || live.doneCount >= live.total)
|
|
375
|
+
return; // the list moved
|
|
376
|
+
budgetRef.current = { ...budgetRef.current, used: budgetRef.current.used + 1 };
|
|
377
|
+
autoContinuationRef.current = true;
|
|
378
|
+
// COUNTS ONLY, NEVER ITEM TEXT (D-19), and only at the fire point —
|
|
379
|
+
// the quiet paths must stay quiet.
|
|
380
|
+
getLogger().debug('agent', 'todo_follow', {
|
|
381
|
+
used: budgetRef.current.used,
|
|
382
|
+
noProgressStreak: budgetRef.current.noProgressStreak,
|
|
383
|
+
total: live.total,
|
|
384
|
+
done: live.doneCount,
|
|
385
|
+
mode: 'auto',
|
|
386
|
+
});
|
|
387
|
+
submitRef.current(decision.message, { userInitiated: false });
|
|
388
|
+
}, decision.graceMs);
|
|
389
|
+
}
|
|
223
390
|
// A deferred `plan -> build` becomes real here and nowhere else (§3.2).
|
|
224
391
|
const applied = controller.applyPendingMode();
|
|
225
392
|
if (applied) {
|
|
@@ -233,9 +400,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
233
400
|
});
|
|
234
401
|
return () => {
|
|
235
402
|
flushPending();
|
|
403
|
+
// An armed continuation must not outlive the tree that armed it (AC-21):
|
|
404
|
+
// the timer would fire into an unmounted App and `submitRef.current` would
|
|
405
|
+
// prompt a controller nobody is rendering.
|
|
406
|
+
cancelFollowThrough();
|
|
236
407
|
unsubscribe();
|
|
237
408
|
};
|
|
238
|
-
}, [controller, flushPending, cancelPendingHuman]);
|
|
409
|
+
}, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
239
410
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
240
411
|
//
|
|
241
412
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -292,6 +463,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
292
463
|
controller.dispose();
|
|
293
464
|
};
|
|
294
465
|
}, [controller]);
|
|
466
|
+
// --- Todo event stream (todo-plan-execution §3.8 / §5.2). --------------
|
|
467
|
+
//
|
|
468
|
+
// A THIRD SUBSCRIPTION, for the reason the team one records: `TodoEvent` is
|
|
469
|
+
// deliberately not a member of core's `AgentEvent` union (D-11), so it cannot
|
|
470
|
+
// arrive through `controller.subscribe`.
|
|
471
|
+
//
|
|
472
|
+
// NOT ROUTED THROUGH THE STREAMING COALESCER either. That buffer exists to
|
|
473
|
+
// merge text/thinking deltas; a `todo_write` is one deliberate act per step,
|
|
474
|
+
// and delaying the one surface the user is watching a long task through would
|
|
475
|
+
// be exactly backwards.
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
return controller.subscribeTodos((event) => {
|
|
478
|
+
switch (event.type) {
|
|
479
|
+
case 'updated':
|
|
480
|
+
dispatch({ type: 'todoUpdate', snapshot: event.snapshot });
|
|
481
|
+
break;
|
|
482
|
+
case 'cleared':
|
|
483
|
+
dispatch({ type: 'todoCleared' });
|
|
484
|
+
break;
|
|
485
|
+
case 'rejected':
|
|
486
|
+
// THE COMPENSATING PATH FOR SUPPRESSING THE TOOL CARD (§3.8). Without
|
|
487
|
+
// it, the one case where `todo_write` produces nothing visible would
|
|
488
|
+
// be the case that most needs to be visible.
|
|
489
|
+
dispatch({ type: 'notice', level: 'warn', text: event.reason });
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}, [controller]);
|
|
295
494
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
296
495
|
useEffect(() => {
|
|
297
496
|
if (state.status !== 'running') {
|
|
@@ -342,7 +541,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
342
541
|
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
343
542
|
// is bookkeeping, and the comment on the old persisted key said so.
|
|
344
543
|
onSeen: () => setMouseNoticeSeen(true),
|
|
345
|
-
}
|
|
544
|
+
},
|
|
545
|
+
// Present only when `loadConfig` had to raise the user's own
|
|
546
|
+
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
547
|
+
cfg.transcriptRetainRequested !== undefined
|
|
548
|
+
? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
|
|
549
|
+
: undefined);
|
|
346
550
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
347
551
|
useEffect(() => {
|
|
348
552
|
publishExitSnapshot({
|
|
@@ -351,8 +555,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
351
555
|
provider: cfg.provider,
|
|
352
556
|
model: cfg.model,
|
|
353
557
|
startedAt: startedAt.current,
|
|
558
|
+
// Carried so the exit replay can say what the retain ring removed. A
|
|
559
|
+
// transcript that silently omits entries is worse than one that names the
|
|
560
|
+
// count (K-6).
|
|
561
|
+
droppedEntries: state.droppedEntries,
|
|
562
|
+
});
|
|
563
|
+
}, [state.entries, state.usageTotal, state.droppedEntries, cfg.provider, cfg.model]);
|
|
564
|
+
// --- `/perf` snapshot channel (tui-render-performance §5.4). ------------
|
|
565
|
+
//
|
|
566
|
+
// The deps are the two `useCallback`d members, NOT the handle objects: both
|
|
567
|
+
// hooks return a fresh literal per render, so depending on them would re-run
|
|
568
|
+
// this effect on every frame.
|
|
569
|
+
const governorReset = governor.reset;
|
|
570
|
+
const heightsClear = heights.clear;
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
setPerfSnapshotProvider(() => perfRef.current);
|
|
573
|
+
setPerfResetHook(() => {
|
|
574
|
+
governorReset();
|
|
575
|
+
heightsClear();
|
|
354
576
|
});
|
|
355
|
-
|
|
577
|
+
return () => {
|
|
578
|
+
setPerfSnapshotProvider(null);
|
|
579
|
+
setPerfResetHook(null);
|
|
580
|
+
};
|
|
581
|
+
}, [governorReset, heightsClear]);
|
|
356
582
|
// --- Confirm bridge (confirmTools mode + skill approvals). -------------
|
|
357
583
|
useEffect(() => {
|
|
358
584
|
if (!confirmBridge)
|
|
@@ -495,9 +721,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
495
721
|
toast,
|
|
496
722
|
persistConfig,
|
|
497
723
|
exit: doExit,
|
|
498
|
-
submit: (text) => submitMessage(text),
|
|
724
|
+
submit: (text, opts) => submitMessage(text, opts),
|
|
499
725
|
refreshSkills: () => setSkillsNonce((n) => n + 1),
|
|
500
726
|
applyAgentMode: applyMode,
|
|
727
|
+
// Read at command time, so `/todo status` reports the live count rather than
|
|
728
|
+
// whatever it was when this context was built.
|
|
729
|
+
followBudget: budgetRef.current,
|
|
501
730
|
});
|
|
502
731
|
/**
|
|
503
732
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -527,13 +756,26 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
527
756
|
* `handleSubmit` would re-parse it as input and, for a body that happens to
|
|
528
757
|
* start with `/`, recurse into command dispatch.
|
|
529
758
|
*/
|
|
530
|
-
const submitMessage = (message) => {
|
|
759
|
+
const submitMessage = (message, opts = {}) => {
|
|
760
|
+
// A USER MESSAGE IS A NEW INTENT AND OUTRANKS A QUEUED ONE (AC-19). First
|
|
761
|
+
// line, before the steering branch: a message typed during the grace window
|
|
762
|
+
// must be the one that runs, whichever branch it takes.
|
|
763
|
+
cancelFollowThrough();
|
|
531
764
|
if (stateRef.current.status === 'running') {
|
|
532
765
|
controller.steer(message);
|
|
533
766
|
toast('info', 'Steering queued.');
|
|
534
767
|
return;
|
|
535
768
|
}
|
|
536
|
-
|
|
769
|
+
// `userInitiated: false` skips TWO things and both are wanted: the
|
|
770
|
+
// prompt-history append, and the `submitCount` bump that drives the
|
|
771
|
+
// composer's hint fade. An auto-continuation demonstrates nothing about what
|
|
772
|
+
// the user has learned, so suppressing the fade is correct rather than
|
|
773
|
+
// incidental — which is why the parameter is named for the CAUSE (P2-8).
|
|
774
|
+
if (opts.userInitiated !== false)
|
|
775
|
+
recordPrompt(message);
|
|
776
|
+
// Mirrors where the reducer clears its own copies, in the same action
|
|
777
|
+
// (C-11): a new turn's end reason must not inherit the previous one's.
|
|
778
|
+
endReasonRef.current = { aborted: false, errored: false };
|
|
537
779
|
dispatch({ type: 'submit', text: message });
|
|
538
780
|
const pre = controller.preflight();
|
|
539
781
|
if (!pre.ok) {
|
|
@@ -544,6 +786,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
544
786
|
// Fire-and-forget: events drive the UI. prompt() never rejects.
|
|
545
787
|
void controller.prompt(message);
|
|
546
788
|
};
|
|
789
|
+
submitRef.current = submitMessage;
|
|
547
790
|
const handleSubmit = async (raw) => {
|
|
548
791
|
// Submitting is an unconditional "take me to the newest output" (§4.5).
|
|
549
792
|
setPinToBottomNonce((n) => n + 1);
|
|
@@ -782,8 +1025,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
782
1025
|
}
|
|
783
1026
|
if (stateRef.current.status === 'running') {
|
|
784
1027
|
controller.abort();
|
|
1028
|
+
// Next to `abortMark`, in the same callback, for the reason
|
|
1029
|
+
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
1030
|
+
// silence at `agent_end`: the user's Esc is the statement, and
|
|
1031
|
+
// restating what they interrupted is the CLI arguing.
|
|
1032
|
+
endReasonRef.current.aborted = true;
|
|
785
1033
|
dispatch({ type: 'abortMark' });
|
|
786
1034
|
toast('info', 'Run aborted.');
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
1038
|
+
// to take (R-4). OVERLAY PRECEDENCE IS UNCHANGED AND DELIBERATE (P2-9):
|
|
1039
|
+
// the block above returns while an overlay is open, so with one open the
|
|
1040
|
+
// first Esc closes the overlay and the second cancels the continuation.
|
|
1041
|
+
// Reaching past it would make "close this dialog" silently also mean
|
|
1042
|
+
// "abandon the plan".
|
|
1043
|
+
if (followTimer.current) {
|
|
1044
|
+
cancelFollowThrough();
|
|
1045
|
+
toast('info', 'Auto-continue cancelled.');
|
|
787
1046
|
}
|
|
788
1047
|
return;
|
|
789
1048
|
}
|
|
@@ -802,6 +1061,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
802
1061
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
803
1062
|
const viewportBudget = computeViewportRows(rows);
|
|
804
1063
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1064
|
+
// --- The todo rail (todo-plan-execution §3.9). --------------------------
|
|
1065
|
+
//
|
|
1066
|
+
// ONE NUMBER FOR THE GATE AND FOR THE LAYOUT (I-10). `railRows` is both what
|
|
1067
|
+
// decides whether the panel mounts and what the panel lays out against; a
|
|
1068
|
+
// panel that mounts on one number and renders against another shows nothing
|
|
1069
|
+
// and reports nothing. `todoRailRows` is NOT `viewportBudget`: the live team
|
|
1070
|
+
// roster costs up to 8 rows inside `AppShell`'s measured bottom box that the
|
|
1071
|
+
// budget knows nothing about (P1-3).
|
|
1072
|
+
const railRows = todoRailRows(viewportBudget, state.team !== null);
|
|
1073
|
+
const railWidth = todoRailWidth(cols);
|
|
1074
|
+
const showRail = fullscreen && // non-goal 4: inline mode has no fixed frame
|
|
1075
|
+
cfg.todo.panel && // `/todo panel off`, persisted
|
|
1076
|
+
overlay === null && // a modal owns the screen (D-7)
|
|
1077
|
+
state.todos !== null && // no list -> no furniture, zero layout cost
|
|
1078
|
+
railWidth > 0 && // too narrow (under 80 columns)
|
|
1079
|
+
railRows >= TODO_LIMITS.panelMinRows; // too short to say anything useful
|
|
1080
|
+
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1081
|
+
//
|
|
1082
|
+
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
1083
|
+
// three of them mean "do not draw this". `cfg.todo.panel` is included because
|
|
1084
|
+
// `--no-todo-panel` means "keep the planning, drop the display" and a strip is
|
|
1085
|
+
// a display. Overlays are NOT consulted: inline mode's overlays are part of
|
|
1086
|
+
// the document flow, not a modal that owns the screen.
|
|
1087
|
+
//
|
|
1088
|
+
// `cfg` is the right read here and `controller.getTodoConfig()` would be
|
|
1089
|
+
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1090
|
+
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1091
|
+
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1092
|
+
// Every consumer INSIDE the viewport slot switches to this; the header,
|
|
1093
|
+
// composer, status bar, toasts and every overlay keep the full frame width
|
|
1094
|
+
// (C-7). Overlays keep it because `showRail` already includes `overlay ===
|
|
1095
|
+
// null`: splitting the screen with a status panel behind a modal is both worse
|
|
1096
|
+
// design and a second source of width-contract bugs in six components.
|
|
1097
|
+
const contentCols = cols - (showRail ? railWidth : 0);
|
|
805
1098
|
const overlayNode = overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
|
|
806
1099
|
provider: cfg.provider,
|
|
807
1100
|
model: cfg.model,
|
|
@@ -834,15 +1127,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
834
1127
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
835
1128
|
// first block of content, so they scroll away instead of being yanked out of
|
|
836
1129
|
// the header the moment the first message lands.
|
|
837
|
-
const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget,
|
|
1130
|
+
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;
|
|
838
1131
|
const density = cfg.density;
|
|
839
|
-
const
|
|
1132
|
+
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1133
|
+
// Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
|
|
1134
|
+
// `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
|
|
1135
|
+
perfRef.current = {
|
|
1136
|
+
rung: governor.rung,
|
|
1137
|
+
intervalMs: governor.intervalMs.current,
|
|
1138
|
+
lastCommitMs: governor.lastCommitMs(),
|
|
1139
|
+
governorEnabled: cfg.renderGovernor,
|
|
1140
|
+
totalEntries: state.entries.length,
|
|
1141
|
+
retain: cfg.transcriptRetain ?? DEFAULT_TRANSCRIPT_RETAIN,
|
|
1142
|
+
droppedEntries: state.droppedEntries,
|
|
1143
|
+
mountedEntries: mountedCount.current,
|
|
1144
|
+
heightsMeasured: heights.stats().measured,
|
|
1145
|
+
heightsEstimated: heights.stats().estimated,
|
|
1146
|
+
cols: contentCols,
|
|
1147
|
+
mode: fullscreen ? 'fullscreen' : 'inline',
|
|
1148
|
+
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1149
|
+
offset: scrolledLines,
|
|
1150
|
+
};
|
|
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] }));
|
|
840
1152
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
841
1153
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
842
1154
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
843
1155
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
844
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, historyIntent: historyIntent, 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' }) }) }));
|
|
845
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), 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, ...(state.team
|
|
1157
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, 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(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), 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
|
|
846
1158
|
? {
|
|
847
1159
|
teamActive: {
|
|
848
1160
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -850,5 +1162,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
850
1162
|
total: state.team.runs.length,
|
|
851
1163
|
},
|
|
852
1164
|
}
|
|
1165
|
+
: {}), ...(state.todos && !showRail
|
|
1166
|
+
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
853
1167
|
: {}) }) }));
|
|
854
1168
|
}
|
package/dist/ui/Markdown.d.ts
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
* *italic* / `code` / ~~strike~~. Anything else renders as plain text. Still
|
|
8
8
|
* intentionally small — not a CommonMark implementation.
|
|
9
9
|
*
|
|
10
|
-
* The grammar lives in `markdown-blocks.ts`; this file only renders.
|
|
10
|
+
* The grammar lives in `markdown-blocks.ts`; this file only renders. That was
|
|
11
|
+
* true of the file header long before it was true of the file
|
|
12
|
+
* (tui-render-performance R2): the block grammar used to run here, in the render
|
|
13
|
+
* body, on every frame. Both expensive halves are now memoised on the input
|
|
14
|
+
* string — `parseMarkdownCached` for the AST, `highlightCached` for the ANSI —
|
|
15
|
+
* while theme and glyphs stay render-time, so `/theme` still takes effect
|
|
16
|
+
* immediately (K-5).
|
|
11
17
|
*/
|
|
12
18
|
import React from 'react';
|
|
13
19
|
import type { Theme } from './theme.js';
|
package/dist/ui/Markdown.js
CHANGED
|
@@ -1,54 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { highlight } from 'cli-highlight';
|
|
4
3
|
import { pickGlyphs } from './glyphs.js';
|
|
5
4
|
import { railBorderProps } from './layout/Gutter.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { highlightCached, parseMarkdownCached } from './render-cache.js';
|
|
6
|
+
import { formatTable, isRule, parseHeading, parseInline, } from './markdown-blocks.js';
|
|
8
7
|
/** Width of the `---` rule. Fixed so it cannot force a wrap on narrow frames. */
|
|
9
8
|
const RULE_WIDTH = 24;
|
|
10
9
|
export function Markdown({ text, theme, caps }) {
|
|
11
10
|
const glyphs = pickGlyphs(caps);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
i += 1;
|
|
23
|
-
while (i < lines.length && !FENCE.test((lines[i] ?? '').trim())) {
|
|
24
|
-
codeLines.push(lines[i] ?? '');
|
|
25
|
-
i += 1;
|
|
26
|
-
}
|
|
27
|
-
i += 1; // skip closing fence
|
|
28
|
-
blocks.push(_jsx(CodeBlock, { code: codeLines.join('\n'), lang: lang, theme: theme, glyphs: glyphs }, `b${key++}`));
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
// A pipe row followed by a divider row starts a table; consume the run.
|
|
32
|
-
const headerCells = parseTableRow(line);
|
|
33
|
-
if (headerCells && i + 1 < lines.length && isTableDivider(lines[i + 1] ?? '')) {
|
|
34
|
-
const align = tableAlignments(lines[i + 1] ?? '');
|
|
35
|
-
const rows = [headerCells];
|
|
36
|
-
i += 2;
|
|
37
|
-
while (i < lines.length) {
|
|
38
|
-
const cells = parseTableRow(lines[i] ?? '');
|
|
39
|
-
if (!cells)
|
|
40
|
-
break;
|
|
41
|
-
rows.push(cells);
|
|
42
|
-
i += 1;
|
|
43
|
-
}
|
|
44
|
-
const formatted = formatTable(rows, align);
|
|
45
|
-
blocks.push(_jsx(Box, { flexDirection: "column", children: formatted.map((row, r) => (_jsx(Text, { wrap: "truncate", bold: r === 0, color: r === 0 ? theme.accent : undefined, children: row }, r))) }, `b${key++}`));
|
|
46
|
-
continue;
|
|
11
|
+
const blocks = parseMarkdownCached(text);
|
|
12
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => renderBlock(block, i, theme, glyphs)) }));
|
|
13
|
+
}
|
|
14
|
+
function renderBlock(block, key, theme, glyphs) {
|
|
15
|
+
switch (block.kind) {
|
|
16
|
+
case 'code':
|
|
17
|
+
return (_jsx(CodeBlock, { code: block.code, lang: block.lang, theme: theme, glyphs: glyphs }, `b${key}`));
|
|
18
|
+
case 'table': {
|
|
19
|
+
const formatted = formatTable(block.rows, block.align);
|
|
20
|
+
return (_jsx(Box, { flexDirection: "column", children: formatted.map((row, r) => (_jsx(Text, { wrap: "truncate", bold: r === 0, color: r === 0 ? theme.accent : undefined, children: row }, r))) }, `b${key}`));
|
|
47
21
|
}
|
|
48
|
-
|
|
49
|
-
|
|
22
|
+
default:
|
|
23
|
+
return _jsx(InlineLine, { line: block.text, theme: theme, glyphs: glyphs }, `b${key}`);
|
|
50
24
|
}
|
|
51
|
-
return _jsx(Box, { flexDirection: "column", children: blocks });
|
|
52
25
|
}
|
|
53
26
|
/**
|
|
54
27
|
* Fenced code. The round border is gone (§4.3): nested inside a tool card it
|
|
@@ -57,13 +30,7 @@ export function Markdown({ text, theme, caps }) {
|
|
|
57
30
|
* code" boundary for 1 column and 0 rows.
|
|
58
31
|
*/
|
|
59
32
|
function CodeBlock({ code, lang, theme, glyphs, }) {
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
rendered = highlight(code, { language: lang || undefined, ignoreIllegals: true });
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
rendered = code;
|
|
66
|
-
}
|
|
33
|
+
const rendered = highlightCached(code, lang);
|
|
67
34
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [lang.length > 0 && (_jsx(Text, { color: theme.chip.fg, backgroundColor: theme.chip.bg, children: ` ${lang} ` })), _jsx(Text, { children: rendered })] }));
|
|
68
35
|
}
|
|
69
36
|
/** Six heading levels used to render identically; now they carry three weights. */
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -70,6 +70,37 @@ interface StatusBarProps {
|
|
|
70
70
|
running: number;
|
|
71
71
|
total: number;
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Live todo counts (todo-plan-execution §6.3), or absent.
|
|
75
|
+
*
|
|
76
|
+
* `App` sends this ONLY when a list exists AND the rail does not: with the
|
|
77
|
+
* rail on screen the numbers are already there in a larger typeface, and
|
|
78
|
+
* duplicating them costs the context gauge columns it cannot spare. This is
|
|
79
|
+
* therefore the INLINE-MODE and NARROW-TERMINAL fallback for R-d, which is
|
|
80
|
+
* exactly why it is here rather than being folded into the rail's own gate.
|
|
81
|
+
*
|
|
82
|
+
* Degrades below `TODO_LIMITS.statusCompactCols` from `todo 2/7` to `[2/7]`,
|
|
83
|
+
* the same treatment and the same justification as the team counter's
|
|
84
|
+
* `agents 3/5` -> `[3]`. See the note on `agentMode` for why that rule does
|
|
85
|
+
* NOT apply to the mode word.
|
|
86
|
+
*/
|
|
87
|
+
todoActive?: {
|
|
88
|
+
done: number;
|
|
89
|
+
total: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Render-governor rung (tui-render-performance L4). `> 0` renders a muted
|
|
93
|
+
* `eco` chip.
|
|
94
|
+
*
|
|
95
|
+
* I-L4-2 — THE GOVERNOR IS USER-VISIBLE, and this is where. A user who cannot
|
|
96
|
+
* see why the stream got chunkier will conclude the model got slower, and
|
|
97
|
+
* degrading silently is the one thing a claim of robustness cannot afford.
|
|
98
|
+
* It sits in the RIGHT cluster next to the other derived numbers and is
|
|
99
|
+
* suppressed on a narrow terminal, which is the `teamActive` treatment rather
|
|
100
|
+
* than the `agentMode` one: the mode must never be unreportable, whereas an
|
|
101
|
+
* eco chip on a 50-column terminal costs the context gauge more than it says.
|
|
102
|
+
*/
|
|
103
|
+
ecoRung?: number;
|
|
73
104
|
}
|
|
74
105
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
75
106
|
export {};
|