@aragon-agent/cli 0.6.31 → 0.6.32
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 +100 -0
- package/README.md +2957 -2826
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +232 -8
- package/dist/commands/builtins.js +23 -8
- package/dist/commands/perf.d.ts +54 -1
- package/dist/commands/perf.js +28 -4
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +38 -0
- package/dist/config/load.js +50 -1
- package/dist/config/schema.d.ts +73 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +14 -1
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.js +64 -6
- package/dist/ui/ScrollbackShell.d.ts +30 -4
- package/dist/ui/ScrollbackShell.js +8 -3
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +17 -4
- package/dist/ui/screen.js +14 -6
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +32 -3
- package/dist/ui/scrollback/budget.js +43 -6
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/limits.d.ts +49 -0
- package/dist/ui/scrollback/limits.js +49 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +104 -16
- package/dist/ui/scrollback/scrollback-writer.js +368 -82
- package/dist/ui/scrollback/use-scrollback-view.d.ts +11 -0
- package/dist/ui/scrollback/use-scrollback-view.js +37 -6
- package/dist/ui/scrollback/writer-shapes.d.ts +68 -2
- package/dist/ui/scrollback/writer-shapes.js +53 -8
- package/package.json +3 -3
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '.
|
|
|
17
17
|
import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
18
18
|
import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
|
|
19
19
|
import type { TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
20
|
-
import type { ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
20
|
+
import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
21
21
|
import type { RetryPhase } from './retry-view.js';
|
|
22
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
23
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
@@ -247,6 +247,26 @@ export type Entry = {
|
|
|
247
247
|
terminal?: boolean;
|
|
248
248
|
/** The stop reported the pid still alive afterwards (R-2 / P2-6). */
|
|
249
249
|
killIncomplete?: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* WHICH KIND OF JOB THIS CARD DRAWS (long-task-jobs §4.9 / I-2).
|
|
252
|
+
*
|
|
253
|
+
* It changes the NOUN and the status wording only. Renaming the entry kind
|
|
254
|
+
* from `service` to `job` would be a pure regression: ordinary assertions pin
|
|
255
|
+
* `[background] service s1 started` word for word, `bg-command.test.ts` pins
|
|
256
|
+
* `/bg stop`'s `Stopped N services` toast, and there are no snapshot files in
|
|
257
|
+
* this package to absorb the churn. The few strings that DID move to "job"
|
|
258
|
+
* are the model-facing unknown-id messages in `proc-tools.ts`, which name an
|
|
259
|
+
* id space that now holds both kinds.
|
|
260
|
+
*/
|
|
261
|
+
jobKind: JobKind;
|
|
262
|
+
/** Latest extracted progress line; one extra row when present. */
|
|
263
|
+
progress?: string;
|
|
264
|
+
/** When output last arrived, so `stalled` can be evaluated at render time. */
|
|
265
|
+
lastOutputAt: number;
|
|
266
|
+
/** Derived at snapshot time by the supervisor, carried here so it survives a save. */
|
|
267
|
+
stalled?: boolean;
|
|
268
|
+
/** Where the on-disk mirror wrote this job's full log. */
|
|
269
|
+
logPath?: string;
|
|
250
270
|
};
|
|
251
271
|
export interface UsageTotal {
|
|
252
272
|
inputTokens: number;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -1479,6 +1479,7 @@ function serviceFields(service) {
|
|
|
1479
1479
|
return {
|
|
1480
1480
|
kind: 'service',
|
|
1481
1481
|
serviceId: service.id,
|
|
1482
|
+
jobKind: service.kind,
|
|
1482
1483
|
command: service.command,
|
|
1483
1484
|
status: service.status,
|
|
1484
1485
|
...(service.url !== undefined ? { url: service.url } : {}),
|
|
@@ -1490,6 +1491,12 @@ function serviceFields(service) {
|
|
|
1490
1491
|
rows: service.rows,
|
|
1491
1492
|
rowsSeen: service.rowsSeen,
|
|
1492
1493
|
...(service.killIncomplete ? { killIncomplete: true } : {}),
|
|
1494
|
+
// The four fields long-task-jobs adds, in the SAME projection for all three
|
|
1495
|
+
// actions - which is the property this function exists to guarantee.
|
|
1496
|
+
lastOutputAt: service.lastOutputAt,
|
|
1497
|
+
...(service.progress ? { progress: service.progress.text } : {}),
|
|
1498
|
+
...(service.stalled ? { stalled: true } : {}),
|
|
1499
|
+
...(service.logPath !== undefined ? { logPath: service.logPath } : {}),
|
|
1493
1500
|
};
|
|
1494
1501
|
}
|
|
1495
1502
|
/** The LIVE card for a service id, newest first; terminal records are skipped. */
|
|
@@ -102,6 +102,18 @@ export interface SystemPromptParams {
|
|
|
102
102
|
* `bash_kill` would describe tools it does not have.
|
|
103
103
|
*/
|
|
104
104
|
backgroundBlock?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The rendered `<background_jobs>` block, or `''` (long-task-jobs §12 step 13).
|
|
107
|
+
*
|
|
108
|
+
* Spliced conditionally exactly as every block above is, which is what keeps
|
|
109
|
+
* I-3 true: `jobs.enabled: false` produces the pre-feature prompt BYTE FOR BYTE
|
|
110
|
+
* for a fixed tool array.
|
|
111
|
+
*
|
|
112
|
+
* A CHILD'S PROMPT NEVER CARRIES IT: subagents are built with no supervisor and
|
|
113
|
+
* no `job_*` tools, so the block's first sentence would describe a capability
|
|
114
|
+
* the child does not have.
|
|
115
|
+
*/
|
|
116
|
+
jobsBlock?: string;
|
|
105
117
|
}
|
|
106
118
|
/** Describe the active shell so the model does not emit incompatible syntax. */
|
|
107
119
|
export declare function describeShell(): {
|
|
@@ -43,6 +43,7 @@ export function buildSystemPrompt(params) {
|
|
|
43
43
|
const todoBlock = params.todoBlock ?? '';
|
|
44
44
|
const fastBlock = params.fastBlock ?? '';
|
|
45
45
|
const backgroundBlock = params.backgroundBlock ?? '';
|
|
46
|
+
const jobsBlock = params.jobsBlock ?? '';
|
|
46
47
|
const appended = (params.appendSystemPrompt ?? '').trim();
|
|
47
48
|
return [
|
|
48
49
|
'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
|
|
@@ -65,6 +66,7 @@ export function buildSystemPrompt(params) {
|
|
|
65
66
|
...(todoBlock ? ['', todoBlock] : []),
|
|
66
67
|
...(fastBlock ? ['', fastBlock] : []),
|
|
67
68
|
...(backgroundBlock ? ['', backgroundBlock] : []),
|
|
69
|
+
...(jobsBlock ? ['', jobsBlock] : []),
|
|
68
70
|
'',
|
|
69
71
|
'Operating guidance:',
|
|
70
72
|
'- Prefer reading files and inspecting the workspace before making changes.',
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import { formatMigrationNotice, migrateLegacyState } from './config/migrate-lega
|
|
|
25
25
|
import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
|
|
26
26
|
import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
|
|
27
27
|
import { runHistoryCommand } from './config/history-commands.js';
|
|
28
|
-
import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
|
|
28
|
+
import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, JOBS_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyJobsConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
|
|
29
29
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
30
30
|
import { getLogger } from './logging/logger.js';
|
|
31
31
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
@@ -58,7 +58,9 @@ import { createFrameObserver } from './ui/frame-observer.js';
|
|
|
58
58
|
import { enterAltScreen, enterNativeScreen, writeExitTranscript, } from './ui/screen.js';
|
|
59
59
|
import { resolveTerminalPlan } from './ui/terminal-plan.js';
|
|
60
60
|
import { createScrollbackDrain } from './ui/scrollback/drain.js';
|
|
61
|
+
import { SCROLLBACK_LIMITS } from './ui/scrollback/limits.js';
|
|
61
62
|
import { createScrollbackWriter } from './ui/scrollback/scrollback-writer.js';
|
|
63
|
+
import { probeAnchorSpaceBelow, setAnchorProbeDisabled } from './ui/scrollback/cursor-probe.js';
|
|
62
64
|
import { createScrollbackBridge, } from './ui/scrollback/use-scrollback-view.js';
|
|
63
65
|
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
64
66
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
@@ -412,8 +414,16 @@ function forceVtInputForThisConsole() {
|
|
|
412
414
|
* "…" > out.txt`, `aragon config set`, and `aragon --version`, because the
|
|
413
415
|
* interactive and headless paths are disjoint branches: `runInteractive()`
|
|
414
416
|
* renders Ink, `runOneShot()` goes to `runHeadless()`, which never does.
|
|
417
|
+
*
|
|
418
|
+
* `async` FOR EXACTLY ONE `await` (tui-bottom-anchored-live-region P1-3): the
|
|
419
|
+
* cursor-position probe before the first frame. Both call sites are inside async
|
|
420
|
+
* Commander actions and use `void runInteractive(...)` — the session's lifetime is
|
|
421
|
+
* owned by `waitUntilExit()` and the four idempotent restore paths, never by the
|
|
422
|
+
* caller, so nothing awaits it. The `await` sits before the FIRST `stdout.write`
|
|
423
|
+
* and between it and `render()` there is no other suspension point, so making the
|
|
424
|
+
* function async introduces no new interleaving.
|
|
415
425
|
*/
|
|
416
|
-
function runInteractive(flags, extras = {}) {
|
|
426
|
+
async function runInteractive(flags, extras = {}) {
|
|
417
427
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
418
428
|
process.stderr.write('Interactive UI requires a TTY. Use -p "<prompt>", exec, or config list / config set.\n');
|
|
419
429
|
process.exitCode = 2;
|
|
@@ -546,6 +556,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
546
556
|
* asynchronous wait for it.
|
|
547
557
|
*/
|
|
548
558
|
const finalizeNativeScreen = () => {
|
|
559
|
+
// FIRST, before any bytes: the anchor probe must go quiet here (P1-6). A settle
|
|
560
|
+
// timer that survived this point would write `ESC[6n` at a terminal already handed
|
|
561
|
+
// back to the shell and nobody would strip the reply.
|
|
562
|
+
killAnchorProbe();
|
|
549
563
|
const bridge = scrollbackBridge;
|
|
550
564
|
if (!bridge)
|
|
551
565
|
return;
|
|
@@ -651,7 +665,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
651
665
|
// conhost), so no capability gate is possible or needed there.
|
|
652
666
|
const wantEnhancedKeys = plan.enhancedKeys;
|
|
653
667
|
const filter = process.stdin.isTTY
|
|
654
|
-
? tryCreateStdinFilter(process.stdin, {
|
|
668
|
+
? tryCreateStdinFilter(process.stdin, {
|
|
669
|
+
mouse: wantMouse,
|
|
670
|
+
paste: wantPaste,
|
|
671
|
+
enhancedKeys: wantEnhancedKeys,
|
|
672
|
+
// TOLD, never derived (I-A8): a native session strips the terminal's
|
|
673
|
+
// answer to `ESC[6n`, an `app` session is byte-identical to before.
|
|
674
|
+
cursorReport: plan.differ === 'scrollback',
|
|
675
|
+
}, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
655
676
|
: null;
|
|
656
677
|
/**
|
|
657
678
|
* THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
|
|
@@ -665,7 +686,135 @@ function runInteractive(flags, extras = {}) {
|
|
|
665
686
|
const pasteOn = wantPaste && filter !== null;
|
|
666
687
|
/** Same discipline: the translation runs only when a filter fronts Ink. */
|
|
667
688
|
const enhancedKeysOn = wantEnhancedKeys && filter !== null;
|
|
668
|
-
|
|
689
|
+
// --- The bottom anchor's measurement pipeline (tui-bottom-anchored-live-region).
|
|
690
|
+
//
|
|
691
|
+
// Three pieces of host state, and every one of them exists to close a specific
|
|
692
|
+
// failure mode the writer cannot close by itself:
|
|
693
|
+
//
|
|
694
|
+
// `anchorProbeDead` — I-A9 / P1-6. After `finalizeNativeScreen()`,
|
|
695
|
+
// `disposeFrameWriter()`, `filter.dispose()` and the process `exit` hook, a
|
|
696
|
+
// query must never be written again: it would hit a terminal already handed
|
|
697
|
+
// back to the shell, where nothing strips the reply and the user's prompt gains
|
|
698
|
+
// a literal `^[[12;1R`.
|
|
699
|
+
// `anchorReprobeQueued` — P0-3. `invalidate()` is called from inside the writer's
|
|
700
|
+
// `transform`, i.e. inside Ink's own `stdout.write`, so the request is pushed out
|
|
701
|
+
// with `setImmediate` and everything inside that window is coalesced into one
|
|
702
|
+
// query instead of becoming a probe storm on every foreign write.
|
|
703
|
+
// `anchorReprobeTimer` — the 200 ms settle window (§4.6). A drag fires dozens of
|
|
704
|
+
// resize events; one CPR each would fight the input channel for bandwidth and
|
|
705
|
+
// make the region jitter.
|
|
706
|
+
let anchorProbeDead = false;
|
|
707
|
+
let anchorReprobeQueued = false;
|
|
708
|
+
let anchorReprobeTimer = null;
|
|
709
|
+
/** Stop measuring for the rest of the process. Idempotent; safe from any exit path. */
|
|
710
|
+
function killAnchorProbe() {
|
|
711
|
+
anchorProbeDead = true;
|
|
712
|
+
anchorReprobeQueued = false;
|
|
713
|
+
if (anchorReprobeTimer) {
|
|
714
|
+
clearTimeout(anchorReprobeTimer);
|
|
715
|
+
anchorReprobeTimer = null;
|
|
716
|
+
}
|
|
717
|
+
// The module-level switch is what makes a probe that is already in flight, or one
|
|
718
|
+
// whose timer fires during teardown, write zero bytes.
|
|
719
|
+
setAnchorProbeDisabled(true);
|
|
720
|
+
}
|
|
721
|
+
/** Ask for one measurement soon; coalesced, deferred out of Ink's write, and dead-safe. */
|
|
722
|
+
function requestAnchorProbe() {
|
|
723
|
+
if (anchorProbeDead || anchorReprobeQueued)
|
|
724
|
+
return;
|
|
725
|
+
anchorReprobeQueued = true;
|
|
726
|
+
setImmediate(() => {
|
|
727
|
+
anchorReprobeQueued = false;
|
|
728
|
+
if (anchorProbeDead)
|
|
729
|
+
return;
|
|
730
|
+
if (anchorReprobeTimer)
|
|
731
|
+
clearTimeout(anchorReprobeTimer);
|
|
732
|
+
anchorReprobeTimer = setTimeout(() => {
|
|
733
|
+
anchorReprobeTimer = null;
|
|
734
|
+
void runAnchorProbe();
|
|
735
|
+
}, SCROLLBACK_LIMITS.anchorReprobeDelayMs);
|
|
736
|
+
anchorReprobeTimer.unref?.();
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* One query, then consume the answer. Never throws and never fails the session: a
|
|
741
|
+
* terminal that does not answer simply keeps whatever anchor it had.
|
|
742
|
+
*/
|
|
743
|
+
async function runAnchorProbe() {
|
|
744
|
+
const bridge = scrollbackBridge;
|
|
745
|
+
if (anchorProbeDead || !bridge || !filter)
|
|
746
|
+
return;
|
|
747
|
+
filter.setExpectingCursorReport(true);
|
|
748
|
+
let spaceBelow = null;
|
|
749
|
+
try {
|
|
750
|
+
spaceBelow = await probeAnchorSpaceBelow({
|
|
751
|
+
stdin: process.stdin,
|
|
752
|
+
stdout: process.stdout,
|
|
753
|
+
rows: () => process.stdout.rows,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
758
|
+
rows: process.stdout.rows ?? null, reason: String(error),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
filter.setExpectingCursorReport(false);
|
|
763
|
+
}
|
|
764
|
+
if (anchorProbeDead || spaceBelow === null)
|
|
765
|
+
return;
|
|
766
|
+
// `setAnchorSpaceBelow` records the re-probe count; the repaint is deferred for
|
|
767
|
+
// the same reason `requestRepaint` is (this can run while Ink is writing).
|
|
768
|
+
bridge.writer.setAnchorSpaceBelow(spaceBelow, 'probe');
|
|
769
|
+
logger.info('cli', 'scroll_anchor', {
|
|
770
|
+
source: 'probe', spaceBelow, rows: process.stdout.rows ?? null,
|
|
771
|
+
});
|
|
772
|
+
setImmediate(() => {
|
|
773
|
+
if (!anchorProbeDead)
|
|
774
|
+
frameWriter?.repaint();
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* THE ORDER HERE IS THE MECHANISM (P0-1 / I-A10). In canonical mode a terminal's
|
|
779
|
+
* reply to `ESC[6n` is line-buffered until a newline AND echoed, so the probe would
|
|
780
|
+
* time out on every Linux/macOS session while printing `^[[12;1R` on the screen.
|
|
781
|
+
* Raising raw first is also the direction `forceVtInputForThisConsole()` already
|
|
782
|
+
* goes (it raises the mode before Ink's own `setRawMode(true)` so libuv does not
|
|
783
|
+
* rewrite the console mode); a second raise is a no-op on both platforms.
|
|
784
|
+
*
|
|
785
|
+
* `false` means "no probe", never "no session": the caller falls back to the fill.
|
|
786
|
+
*/
|
|
787
|
+
function raiseRawModeForProbe() {
|
|
788
|
+
if (typeof process.stdin.setRawMode !== 'function')
|
|
789
|
+
return false;
|
|
790
|
+
try {
|
|
791
|
+
process.stdin.setRawMode(true);
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
795
|
+
rows: process.stdout.rows ?? null, reason: `raw mode: ${String(error)}`,
|
|
796
|
+
});
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
// Ink lowers raw mode on unmount, which is the normal restore. A kill, a closed
|
|
800
|
+
// window or a crash never unmounts, so the same restore `forceVtInputForThisConsole`
|
|
801
|
+
// registers is registered here too; it is idempotent and cheap.
|
|
802
|
+
process.on('exit', () => {
|
|
803
|
+
try {
|
|
804
|
+
process.stdin.setRawMode(false);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
/* Stream already torn down - nothing left to restore. */
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
const disposeStdinFilter = () => {
|
|
813
|
+
// Before the filter goes away: a probe settle timer must not fire into a stream
|
|
814
|
+
// nobody is stripping any more.
|
|
815
|
+
killAnchorProbe();
|
|
816
|
+
filter?.dispose();
|
|
817
|
+
};
|
|
669
818
|
// --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
|
|
670
819
|
//
|
|
671
820
|
// Selection owns its controller, decoration and notice. Button motion follows
|
|
@@ -698,6 +847,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
698
847
|
// Deferred: static rows can be queued from inside a React commit, and the
|
|
699
848
|
// repaint writes to the stream Ink is in the middle of writing to.
|
|
700
849
|
requestRepaint: () => { setImmediate(() => frameWriter?.repaint()); },
|
|
850
|
+
// `invalidate()` calls this from inside `transform` (geometry lost), so the
|
|
851
|
+
// deferral is not optional here either (P0-3). The host coalesces it into one
|
|
852
|
+
// query per settle window and stops it for good once the screen is handed back.
|
|
853
|
+
requestAnchorProbe,
|
|
701
854
|
});
|
|
702
855
|
scrollbackBridge = createScrollbackBridge({
|
|
703
856
|
writer, drain, requestFlush: () => { setImmediate(() => frameWriter?.repaint()); },
|
|
@@ -757,6 +910,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
757
910
|
stdout: frameWriter?.stdout ?? process.stdout, terminal: process.stdout, scrollbar,
|
|
758
911
|
});
|
|
759
912
|
const disposeFrameWriter = () => {
|
|
913
|
+
// Same switch as `finalizeNativeScreen`: this is one of the four teardown paths.
|
|
914
|
+
killAnchorProbe();
|
|
760
915
|
frameObserver?.dispose();
|
|
761
916
|
pointerRouter?.dispose();
|
|
762
917
|
scrollbar.controller?.dispose();
|
|
@@ -798,6 +953,49 @@ function runInteractive(flags, extras = {}) {
|
|
|
798
953
|
vtInputSupported,
|
|
799
954
|
})
|
|
800
955
|
: NOOP_KEYBOARD_ENHANCEMENT;
|
|
956
|
+
// --- Measure the bottom of the screen, THEN build the first frame (§4.4 / §8.1).
|
|
957
|
+
//
|
|
958
|
+
// ORDER, ALL OF IT LOAD-BEARING:
|
|
959
|
+
// 1. the filter exists (it is the only thing that strips the reply — D5);
|
|
960
|
+
// 2. raw mode is raised BEFORE the query (P0-1 / I-A10);
|
|
961
|
+
// 3. the probe is awaited, and any failure is `null`;
|
|
962
|
+
// 4. `enterNativeScreen` writes the one-off fill only when the probe failed;
|
|
963
|
+
// 5. the measurement is handed to the writer before the first batch.
|
|
964
|
+
//
|
|
965
|
+
// This is the only `await` in the whole startup path and it is bounded by
|
|
966
|
+
// `anchorProbeTimeoutMs` (150 ms), which only a terminal that cannot answer pays.
|
|
967
|
+
let anchorSpaceBelow = null;
|
|
968
|
+
if (native && filter && scrollbackBridge) {
|
|
969
|
+
if (raiseRawModeForProbe()) {
|
|
970
|
+
filter.setExpectingCursorReport(true);
|
|
971
|
+
try {
|
|
972
|
+
anchorSpaceBelow = await probeAnchorSpaceBelow({
|
|
973
|
+
stdin: process.stdin,
|
|
974
|
+
stdout: process.stdout,
|
|
975
|
+
rows: () => process.stdout.rows,
|
|
976
|
+
// The first frame is waiting on this: hold the event loop, or a session with
|
|
977
|
+
// nothing else referenced yet can exit before it draws anything.
|
|
978
|
+
holdProcess: true,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
catch (error) {
|
|
982
|
+
// The probe never throws by contract; this is the belt to that braces, and it
|
|
983
|
+
// must not be able to fail the launch.
|
|
984
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
985
|
+
rows: process.stdout.rows ?? null, reason: String(error),
|
|
986
|
+
});
|
|
987
|
+
anchorSpaceBelow = null;
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
filter.setExpectingCursorReport(false);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
logger.info('cli', anchorSpaceBelow === null ? 'scroll_anchor_probe_failed' : 'scroll_anchor', {
|
|
994
|
+
source: anchorSpaceBelow === null ? 'fill' : 'probe',
|
|
995
|
+
spaceBelow: anchorSpaceBelow,
|
|
996
|
+
rows: process.stdout.rows ?? null,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
801
999
|
{
|
|
802
1000
|
// `mouse` here means "a mouse-parsing filter is installed", NOT "the user
|
|
803
1001
|
// wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
|
|
@@ -815,8 +1013,18 @@ function runInteractive(flags, extras = {}) {
|
|
|
815
1013
|
motion: mouseOn,
|
|
816
1014
|
bracketedPaste: pasteOn,
|
|
817
1015
|
})
|
|
818
|
-
// Native scroll: no alternate screen, no mouse (D6).
|
|
819
|
-
|
|
1016
|
+
// Native scroll: no alternate screen, no mouse (D6). DEC 2004, plus the
|
|
1017
|
+
// one-off fill when the cursor could not be measured — before anything else,
|
|
1018
|
+
// so the rows it scrolls away are the shell's, never this session's.
|
|
1019
|
+
: enterNativeScreen(process.stdout, {
|
|
1020
|
+
bracketedPaste: pasteOn,
|
|
1021
|
+
anchorFillRows: anchorSpaceBelow === null && scrollbackBridge
|
|
1022
|
+
? Math.max(0, (process.stdout.rows ?? 24) - 1)
|
|
1023
|
+
: 0,
|
|
1024
|
+
});
|
|
1025
|
+
// The measurement reaches the writer BEFORE the first batch, so frame one is
|
|
1026
|
+
// already pinned. `fill` says "we know where the bottom is because we went there".
|
|
1027
|
+
scrollbackBridge?.writer.setAnchorSpaceBelow(anchorSpaceBelow ?? 0, anchorSpaceBelow === null ? 'fill' : 'probe');
|
|
820
1028
|
// Input-encoding modes unwind FIRST, for the same reason the mouse
|
|
821
1029
|
// reporting does inside `screen.restore()`: the terminal must stop
|
|
822
1030
|
// sending enhanced encodings before anything else tears the reader
|
|
@@ -1327,6 +1535,11 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
1327
1535
|
// a script at all: `config list` enumerates the section dynamically and so
|
|
1328
1536
|
// shows these keys whether they are here or not, while `config set` does not.
|
|
1329
1537
|
...COMPACTION_CONFIG_SET_KEYS,
|
|
1538
|
+
// `jobs.*` keys are handled by `applyJobsConfigSet`, same reasoning again — and
|
|
1539
|
+
// this spread is exactly why `JOBS_CONFIG_SET_KEYS ⊆ CONFIG_SET_KEYS` needs its
|
|
1540
|
+
// own assertion: the static guard in `config.test.ts` matches LITERAL keys in
|
|
1541
|
+
// this file, so a spread set is invisible to it (RV-P2-8).
|
|
1542
|
+
...JOBS_CONFIG_SET_KEYS,
|
|
1330
1543
|
]);
|
|
1331
1544
|
/**
|
|
1332
1545
|
* `aragon config set maxTokens <n|auto|default>`.
|
|
@@ -1466,6 +1679,17 @@ function runConfigSetValue(key, value) {
|
|
|
1466
1679
|
process.stdout.write(`Set ${key} = ${String(merged.compaction[field])}\n`);
|
|
1467
1680
|
return;
|
|
1468
1681
|
}
|
|
1682
|
+
const jobsPatch = applyJobsConfigSet(key, value);
|
|
1683
|
+
if (jobsPatch) {
|
|
1684
|
+
// Echoed from the STORED value, so `config set jobs.waitSeconds 1` prints 5
|
|
1685
|
+
// and `jobs.waitSeconds 9999` prints 60 — the rule every section above
|
|
1686
|
+
// states. It matters here too: `waitSeconds` is ALSO converged against the
|
|
1687
|
+
// tool ceiling by I-4, so the stored value is the only honest answer.
|
|
1688
|
+
const merged = updatePersistedConfig(jobsPatch);
|
|
1689
|
+
const field = key.slice('jobs.'.length);
|
|
1690
|
+
process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1469
1693
|
const patch = {};
|
|
1470
1694
|
switch (key) {
|
|
1471
1695
|
case 'provider':
|
|
@@ -1935,14 +2159,14 @@ function buildProgram() {
|
|
|
1935
2159
|
}
|
|
1936
2160
|
return;
|
|
1937
2161
|
}
|
|
1938
|
-
runInteractive(flags, { initialPrompt: prompt });
|
|
2162
|
+
void runInteractive(flags, { initialPrompt: prompt });
|
|
1939
2163
|
});
|
|
1940
2164
|
// aragon config [set <key> <value> | path]
|
|
1941
2165
|
const configCmd = program
|
|
1942
2166
|
.command('config')
|
|
1943
2167
|
.description('Open the settings screen')
|
|
1944
2168
|
.action(() => {
|
|
1945
|
-
runInteractive(toFlags(program.opts()), { initialOverlay: 'settings' });
|
|
2169
|
+
void runInteractive(toFlags(program.opts()), { initialOverlay: 'settings' });
|
|
1946
2170
|
});
|
|
1947
2171
|
configCmd
|
|
1948
2172
|
.command('set <key> <value>')
|
|
@@ -9,6 +9,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
9
9
|
import { isAbsolute, resolve } from 'node:path';
|
|
10
10
|
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
11
11
|
import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
12
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
12
13
|
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
13
14
|
import { mergePendingEntries } from '../agent/queued-messages.js';
|
|
14
15
|
import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
|
|
@@ -1256,7 +1257,7 @@ const COMMANDS = [
|
|
|
1256
1257
|
perfCommand,
|
|
1257
1258
|
{
|
|
1258
1259
|
name: 'bg',
|
|
1259
|
-
description: 'Background
|
|
1260
|
+
description: 'Background jobs: list | logs <id> [n] | stop <id|all> | status',
|
|
1260
1261
|
/**
|
|
1261
1262
|
* THE USER'S HANDLE ON WHAT THE AGENT LEFT RUNNING (§5.3).
|
|
1262
1263
|
*
|
|
@@ -1277,9 +1278,18 @@ const COMMANDS = [
|
|
|
1277
1278
|
const services = controller.listServices();
|
|
1278
1279
|
if (verb === 'status') {
|
|
1279
1280
|
const live = controller.liveServiceCount();
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1281
|
+
if (live === 0) {
|
|
1282
|
+
ctx.notify('info', 'No background jobs running.');
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
// THE KIND SPLIT IS THE INFORMATION (long-task-jobs §4.9): "2 jobs
|
|
1286
|
+
// running" is not the same as "1 task, 1 service". A task will end on its
|
|
1287
|
+
// own; a service will not, and that difference decides whether the user
|
|
1288
|
+
// has to do anything.
|
|
1289
|
+
const running = services.filter((job) => !isTerminalStatus(job.status));
|
|
1290
|
+
const tasks = running.filter((job) => job.kind === 'task').length;
|
|
1291
|
+
ctx.notify('info', `${live} background job${live === 1 ? '' : 's'} running ` +
|
|
1292
|
+
`(${tasks} task, ${running.length - tasks} service).`);
|
|
1283
1293
|
return;
|
|
1284
1294
|
}
|
|
1285
1295
|
if (verb === 'logs') {
|
|
@@ -1289,7 +1299,7 @@ const COMMANDS = [
|
|
|
1289
1299
|
}
|
|
1290
1300
|
const service = controller.getServiceSnapshot(value);
|
|
1291
1301
|
if (!service) {
|
|
1292
|
-
ctx.notify('warn', `No
|
|
1302
|
+
ctx.notify('warn', `No job "${value}".`);
|
|
1293
1303
|
return;
|
|
1294
1304
|
}
|
|
1295
1305
|
const page = controller.readServiceLog(value);
|
|
@@ -1335,12 +1345,17 @@ const COMMANDS = [
|
|
|
1335
1345
|
return;
|
|
1336
1346
|
}
|
|
1337
1347
|
const now = Date.now();
|
|
1348
|
+
// `id kind status elapsed progress|url $ command` - the same column
|
|
1349
|
+
// grammar `job_list` uses for the model, so a user reading `/bg list` and a
|
|
1350
|
+
// model reading `job_list` see the same facts in the same order.
|
|
1338
1351
|
const lines = services.map((svc) => {
|
|
1339
1352
|
const uptime = formatDuration((svc.endedAt ?? now) - svc.startedAt);
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1353
|
+
const aside = svc.progress?.text ?? svc.url;
|
|
1354
|
+
const where = aside ? ` ${aside}` : '';
|
|
1355
|
+
const stalled = svc.stalled === true ? ' [no output]' : '';
|
|
1356
|
+
return `${svc.id} ${svc.kind} ${svc.status} ${uptime}${where}${stalled} $ ${svc.command}`;
|
|
1342
1357
|
});
|
|
1343
|
-
ctx.notify('info', `Background
|
|
1358
|
+
ctx.notify('info', `Background jobs:\n${lines.join('\n')}`);
|
|
1344
1359
|
},
|
|
1345
1360
|
},
|
|
1346
1361
|
{
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { FrameWriterStats } from '../ui/frame-differ.js';
|
|
19
19
|
import type { ScrollbarBridge } from '../ui/scrollbar-controller.js';
|
|
20
|
+
import type { AnchorSource } from '../ui/scrollback/canvas.js';
|
|
20
21
|
import type { SlashCommand } from './registry.js';
|
|
21
22
|
export interface PerfSnapshot {
|
|
22
23
|
/** Governor rung, its resolved interval, and the last measured commit cost. */
|
|
@@ -40,6 +41,32 @@ export interface PerfSnapshot {
|
|
|
40
41
|
offset: number;
|
|
41
42
|
/** Right-edge scrollbar diagnostics; read lazily so it reflects the live bridge. */
|
|
42
43
|
scrollbar?: ScrollbarPerf;
|
|
44
|
+
/**
|
|
45
|
+
* The native panel band, or `null` outside native mode
|
|
46
|
+
* (release-gate-and-native-panels §6.10).
|
|
47
|
+
*
|
|
48
|
+
* REQUIRED, WITH AN EXPLICIT `null`. This feature has six ways to show nothing and
|
|
49
|
+
* four of them are invisible, so "the rail is gone" has to be answerable from one
|
|
50
|
+
* line. An optional field would let a future caller forget it and leave `/perf`
|
|
51
|
+
* silently unable to explain the case the user is reporting.
|
|
52
|
+
*/
|
|
53
|
+
band: BandPerf | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The band's projection, as `/perf` reads it. Counts only - the allocation itself lives
|
|
57
|
+
* in `ui/scrollback/band.ts` (`LiveBand`), and this type exists so the command layer
|
|
58
|
+
* does not have to import a UI module to describe it.
|
|
59
|
+
*/
|
|
60
|
+
export interface BandPerf {
|
|
61
|
+
reason: 'visible' | 'empty' | 'disabled' | 'overlay' | 'narrow' | 'short';
|
|
62
|
+
railRows: number;
|
|
63
|
+
railCols: number;
|
|
64
|
+
teamRows: number;
|
|
65
|
+
previewRows: number;
|
|
66
|
+
contentCols: number;
|
|
67
|
+
previewCols: number;
|
|
68
|
+
heldRailRows: number;
|
|
69
|
+
heldTeamRows: number;
|
|
43
70
|
}
|
|
44
71
|
/**
|
|
45
72
|
* Why the right-edge scrollbar is or is not usable, in one record
|
|
@@ -83,6 +110,7 @@ export declare function readFrameStats(): FrameWriterStats | null;
|
|
|
83
110
|
* and the terminal disagreed about a character's width (I-S13).
|
|
84
111
|
*/
|
|
85
112
|
export interface ScrollbackPerf {
|
|
113
|
+
/** LIVE REGION height (the Ink frame), NOT the canvas — see `anchorCanvasRows`. */
|
|
86
114
|
regionRows: number;
|
|
87
115
|
framesSeed: number;
|
|
88
116
|
framesDiff: number;
|
|
@@ -95,9 +123,34 @@ export interface ScrollbackPerf {
|
|
|
95
123
|
fallbacks: number;
|
|
96
124
|
resizes: number;
|
|
97
125
|
repaints: number;
|
|
126
|
+
/** `probe` = measured by CPR; `fill` = the one-off screen fill; `none` = unmeasured. */
|
|
127
|
+
anchorSource: AnchorSource;
|
|
128
|
+
/**
|
|
129
|
+
* Canvas height of the most recent batch, which is also the next erase distance.
|
|
130
|
+
*
|
|
131
|
+
* A SEPARATE FIELD RATHER THAN A RENAMED `regionRows` (P1-4): `/perf`'s
|
|
132
|
+
* "scrollback region N rows" is the first number anyone reads when the frame
|
|
133
|
+
* looks wrong, and quietly turning it into the canvas height would make a 6-row
|
|
134
|
+
* region report 24 during a short conversation.
|
|
135
|
+
*/
|
|
136
|
+
anchorCanvasRows: number;
|
|
137
|
+
/** Content rows inside that canvas — how much of the conversation is on screen. */
|
|
138
|
+
anchorHeldRows: number;
|
|
139
|
+
/** Blank rows above the content. Non-zero is expected on a short conversation. */
|
|
140
|
+
anchorGapRows: number;
|
|
141
|
+
/** Rows this session pushed into the terminal's own scrollback. */
|
|
142
|
+
anchorScrollRows: number;
|
|
143
|
+
/** Times the geometry was dropped on purpose (column change, shrink, geometry lost). */
|
|
144
|
+
anchorResets: number;
|
|
145
|
+
/** Measurements accepted after the first: the re-probe count, not the resize count. */
|
|
146
|
+
anchorReprobes: number;
|
|
98
147
|
}
|
|
99
148
|
/** Called by `cli.tsx` in native mode. Passing `null` detaches. */
|
|
100
149
|
export declare function setScrollbackStatsProvider(fn: (() => ScrollbackPerf) | null): void;
|
|
101
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
|
|
152
|
+
* line). The `band:` line is the newest one and the only place that answers "why is
|
|
153
|
+
* that panel not on screen".
|
|
154
|
+
*/
|
|
102
155
|
export declare function formatPerfReport(snap: PerfSnapshot): string;
|
|
103
156
|
export declare const perfCommand: SlashCommand;
|
package/dist/commands/perf.js
CHANGED
|
@@ -78,10 +78,14 @@ function formatScrollbackLine() {
|
|
|
78
78
|
if (!stats)
|
|
79
79
|
return [];
|
|
80
80
|
return [
|
|
81
|
-
`scrollback region ${stats.regionRows} rows -
|
|
81
|
+
`scrollback region ${stats.regionRows} rows - canvas ${stats.anchorCanvasRows} rows - ` +
|
|
82
|
+
`content ${stats.anchorHeldRows} rows - gap ${stats.anchorGapRows} rows - ` +
|
|
83
|
+
`anchor bottom via ${stats.anchorSource} - scrolled ${stats.anchorScrollRows} rows - ` +
|
|
84
|
+
`anchor resets ${stats.anchorResets} - reprobes ${stats.anchorReprobes}`,
|
|
85
|
+
` batches seed ${stats.framesSeed}, ` +
|
|
82
86
|
`diff ${stats.framesDiff}, full ${stats.framesFull}, repaint ${stats.repaints} - ` +
|
|
83
|
-
`static rows ${stats.staticRowsWritten} - elided ${stats.elidedEntries}
|
|
84
|
-
|
|
87
|
+
`static rows ${stats.staticRowsWritten} - elided ${stats.elidedEntries} - ` +
|
|
88
|
+
`guardHits ${stats.guardHits} (outside resize ${stats.guardHitsOutsideResize}) - ` +
|
|
85
89
|
`clippedRows ${stats.clippedRows} - fallbacks ${stats.fallbacks} - ` +
|
|
86
90
|
`resizes ${stats.resizes}`,
|
|
87
91
|
];
|
|
@@ -120,7 +124,26 @@ function formatScrollbarLine(bar) {
|
|
|
120
124
|
? `${bar.trackCol}x${bar.trackRows + 4}` : 'the terminal size';
|
|
121
125
|
return `${line}\n waiting for a full frame matching ${size}`;
|
|
122
126
|
}
|
|
123
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* `band:` - who got which rows, and why a panel is not on screen.
|
|
129
|
+
*
|
|
130
|
+
* `rail=<rows>x<cols>` reads `0x0` when the rail is not drawn, which is the only
|
|
131
|
+
* unambiguous "there is no rail" - a conservative width would look like a rail that
|
|
132
|
+
* merely has no height. One line, because `/perf` is a notice entry and a wrapping
|
|
133
|
+
* readout costs the repaint it exists to explain.
|
|
134
|
+
*/
|
|
135
|
+
function formatBandLine(band) {
|
|
136
|
+
if (!band)
|
|
137
|
+
return 'band n/a (full-screen layout)';
|
|
138
|
+
return `band rail=${band.railRows}x${band.railCols} team=${band.teamRows} ` +
|
|
139
|
+
`preview=${band.previewRows} cols=${band.contentCols}/${band.previewCols} ` +
|
|
140
|
+
`reason=${band.reason} held=${band.heldRailRows}/${band.heldTeamRows}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
|
|
144
|
+
* line). The `band:` line is the newest one and the only place that answers "why is
|
|
145
|
+
* that panel not on screen".
|
|
146
|
+
*/
|
|
124
147
|
export function formatPerfReport(snap) {
|
|
125
148
|
const caches = renderCacheStats();
|
|
126
149
|
const eco = snap.rung > 0 ? ' - eco' : '';
|
|
@@ -142,6 +165,7 @@ export function formatPerfReport(snap) {
|
|
|
142
165
|
native
|
|
143
166
|
? `mode native - live region ${snap.viewportRows} rows`
|
|
144
167
|
: `mode ${snap.mode} - viewport ${snap.viewportRows} rows - offset ${snap.offset}`,
|
|
168
|
+
formatBandLine(snap.band),
|
|
145
169
|
formatWriterLine(),
|
|
146
170
|
...formatScrollbackLine(),
|
|
147
171
|
...scrollbar,
|