@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `job_wait` and `job_list`, plus the notice tail every tool result carries
|
|
3
|
+
* (long-task-jobs §4.4.2 / §4.4.3 / §4.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/tools/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* WHY TWO NEW TOOLS RATHER THAN A `block` PARAMETER ON `bash_output` (D-10):
|
|
8
|
+
* the tool list IS the discoverable capability surface. A model that can SEE
|
|
9
|
+
* `job_wait` knows that waiting is a legal step in a plan; a hidden `block: 30`
|
|
10
|
+
* parameter is a feature it will never use, and extending `bash_output`'s output
|
|
11
|
+
* would also rewrite assertions that pin its current grammar word for word.
|
|
12
|
+
*
|
|
13
|
+
* EVERY RESULT IS A FIXED GRAMMAR OF AT MOST TWELVE LINES. That is not tidiness:
|
|
14
|
+
* the entire economic argument for this feature is that one wait costs ~150
|
|
15
|
+
* tokens instead of a 200-row tail, so an unbounded result would delete the
|
|
16
|
+
* reason the feature exists.
|
|
17
|
+
*/
|
|
18
|
+
import { defineTool, errorResult, textResult } from '@aragon-agent/core';
|
|
19
|
+
import { formatDuration } from '../agent/usage.js';
|
|
20
|
+
import { JOB_WAIT_MARGIN_MS, PROC_LIMITS } from '../proc/limits.js';
|
|
21
|
+
import { renderJobNotices } from '../proc/notices.js';
|
|
22
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
23
|
+
import { waitForJob } from '../proc/wait.js';
|
|
24
|
+
/**
|
|
25
|
+
* Split a page into "completed rows" and "the carry line".
|
|
26
|
+
*
|
|
27
|
+
* THE PAGE NAMES ITS CARRY: `LogRingPage.carryRow` is the not-yet-terminated line,
|
|
28
|
+
* and `rows` ends with it when it is non-empty. A page from a caller that does not
|
|
29
|
+
* provide it (a test double, `bash_output`) is treated as all-completed, which
|
|
30
|
+
* keeps this a pure refinement rather than a new requirement.
|
|
31
|
+
*/
|
|
32
|
+
function splitCarry(page) {
|
|
33
|
+
const rows = page?.rows ?? [];
|
|
34
|
+
const carry = page?.carryRow ?? '';
|
|
35
|
+
if (carry.length === 0)
|
|
36
|
+
return { rows, carry };
|
|
37
|
+
const last = rows[rows.length - 1];
|
|
38
|
+
return last === carry ? { rows: rows.slice(0, -1), carry } : { rows, carry };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Clamp `timeoutSeconds` into `[jobs.waitSeconds, 60]` and then into the tool
|
|
42
|
+
* ceiling (I-4).
|
|
43
|
+
*
|
|
44
|
+
* TWO CONVERGENCES, BOTH REQUIRED. The first honours `WAIT_SECONDS_RANGE`; the
|
|
45
|
+
* second is what makes it impossible for a wait to outlive the tool that is
|
|
46
|
+
* hosting it, which is the difference between "a bounded wait" and "another
|
|
47
|
+
* `Command timed out after 180000ms`".
|
|
48
|
+
*/
|
|
49
|
+
function clampWaitSeconds(deps, requested) {
|
|
50
|
+
const configured = deps.waitSeconds();
|
|
51
|
+
const asked = requested === undefined || !Number.isFinite(requested) || requested < PROC_LIMITS.waitMinSeconds
|
|
52
|
+
? configured
|
|
53
|
+
: requested;
|
|
54
|
+
const bounded = Math.max(PROC_LIMITS.waitMinSeconds, Math.min(Math.floor(asked), PROC_LIMITS.waitMaxSeconds));
|
|
55
|
+
const ceiling = Math.floor((deps.toolTimeoutMs() - JOB_WAIT_MARGIN_MS) / 1000);
|
|
56
|
+
// `Math.max(1, ...)` rather than `Math.max(waitMinSeconds, ...)`: when an
|
|
57
|
+
// operator sets a tool ceiling below the margin, "the wait fits inside the
|
|
58
|
+
// ceiling" is the invariant that must win. Nothing here ever silently produces
|
|
59
|
+
// a wait longer than the tool call that hosts it.
|
|
60
|
+
return Math.max(1, Math.min(bounded, ceiling));
|
|
61
|
+
}
|
|
62
|
+
/** `JOB t3 task running 12m34s` - the one header grammar for every outcome. */
|
|
63
|
+
function headerLine(job, nowMs) {
|
|
64
|
+
const elapsed = formatDuration((job.endedAt ?? nowMs) - job.startedAt);
|
|
65
|
+
const code = job.exitCode !== null
|
|
66
|
+
? ` code ${job.exitCode}`
|
|
67
|
+
: job.signal !== null
|
|
68
|
+
? ` signal ${job.signal}`
|
|
69
|
+
: '';
|
|
70
|
+
return `JOB ${job.id} ${job.kind} ${job.status}${code} ${elapsed}`;
|
|
71
|
+
}
|
|
72
|
+
/** `[stalled] ...` - only for a live job whose silence passed the threshold. */
|
|
73
|
+
function stalledLine(job, nowMs, stallSeconds) {
|
|
74
|
+
// A FINISHED JOB CANNOT BE STALLED. `snapshot()` derives the flag only for a
|
|
75
|
+
// live job, so this guard is about the OTHER direction: a snapshot captured
|
|
76
|
+
// while the job was quiet and delivered after it ended must not claim silence.
|
|
77
|
+
if (job.stalled !== true || isTerminalStatus(job.status))
|
|
78
|
+
return undefined;
|
|
79
|
+
const quiet = formatDuration(Math.max(0, nowMs - job.lastOutputAt));
|
|
80
|
+
return (`[stalled] no output for ${quiet} (threshold ${stallSeconds}s). Keep waiting, ` +
|
|
81
|
+
`stop it with bash_kill({ job: "${job.id}" }), or read the tail with bash_output.`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Assemble one job's block.
|
|
85
|
+
*
|
|
86
|
+
* THE ROW BLOCK IS THE ONLY ELASTIC PART, and it shrinks first when the fixed
|
|
87
|
+
* lines grow - which is how "at most twelve lines" survives a job that is both
|
|
88
|
+
* stalled and truncated without either sentence being dropped.
|
|
89
|
+
*/
|
|
90
|
+
function renderJobBlock(args) {
|
|
91
|
+
const { job, rows, cursor, since, nowMs } = args;
|
|
92
|
+
const terminal = isTerminalStatus(job.status);
|
|
93
|
+
const fixed = [headerLine(job, nowMs)];
|
|
94
|
+
const progress = job.progress?.text;
|
|
95
|
+
if (progress !== undefined) {
|
|
96
|
+
fixed.push(`[progress] ${progress}${args.progressUnchanged ? ' (unchanged)' : ''}`);
|
|
97
|
+
}
|
|
98
|
+
const stalled = stalledLine(job, nowMs, args.stalledSeconds);
|
|
99
|
+
if (stalled !== undefined)
|
|
100
|
+
fixed.push(stalled);
|
|
101
|
+
if (args.truncatedNote !== undefined)
|
|
102
|
+
fixed.push(args.truncatedNote);
|
|
103
|
+
const tail = [`[cursor ${cursor}]`];
|
|
104
|
+
if (terminal) {
|
|
105
|
+
tail.push(`[done] the job finished. Read more with bash_output({ service: "${job.id}" }) if you need it.`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const quiet = formatDuration(Math.max(0, nowMs - job.lastOutputAt));
|
|
109
|
+
tail.push(`[wait ${args.waitSeconds}s] still running. No output for ${quiet}.`);
|
|
110
|
+
tail.push(`[next] job_wait({ job: "${job.id}", since: ${cursor} }) to keep waiting, ` +
|
|
111
|
+
'or continue with other work.');
|
|
112
|
+
}
|
|
113
|
+
// TWELVE LINES, AND THE ROW BLOCK IS THE ONLY ELASTIC PART. Its header costs a
|
|
114
|
+
// line of its own, so the budget is computed with it already deducted; a stalled
|
|
115
|
+
// or truncated job therefore loses output rows rather than the sentence that
|
|
116
|
+
// says what is wrong.
|
|
117
|
+
const rowLineHint = rows.length > 0 ? 1 : 0;
|
|
118
|
+
const budget = Math.max(0, 12 - fixed.length - tail.length - rowLineHint);
|
|
119
|
+
const shown = rows.slice(-Math.min(PROC_LIMITS.waitTailRows, budget));
|
|
120
|
+
const sinceClause = since === undefined ? '' : ` since ${since}`;
|
|
121
|
+
return [
|
|
122
|
+
...fixed,
|
|
123
|
+
...(shown.length > 0 ? [`[... ${shown.length} row(s)${sinceClause} ...]`] : []),
|
|
124
|
+
...shown.map((row) => ` ${row}`),
|
|
125
|
+
...tail,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
/** The three-state "no such id" answer (RV-P2-7). */
|
|
129
|
+
function unknownJobMessage(deps, requested) {
|
|
130
|
+
// RECLAIMED FIRST, AND THAT ORDER IS THE POINT (RV-P2-7). State two is the one
|
|
131
|
+
// that costs money if it is missed: a model told `No job "t9"` about a
|
|
132
|
+
// forty-minute training run that FINISHED will start it again. The live-id list
|
|
133
|
+
// is only useful once we know the id never named a job this session.
|
|
134
|
+
const reclaimed = deps.terminalRecord?.(requested);
|
|
135
|
+
if (reclaimed) {
|
|
136
|
+
return errorResult(`Job "${requested}" already finished (${reclaimed.kind} ${reclaimed.status}) and was ` +
|
|
137
|
+
'reclaimed; its outcome was delivered in a <job_notices> block. Do not re-run it.');
|
|
138
|
+
}
|
|
139
|
+
const live = deps.supervisor
|
|
140
|
+
.list()
|
|
141
|
+
.filter((job) => !isTerminalStatus(job.status))
|
|
142
|
+
.slice(0, PROC_LIMITS.listRows)
|
|
143
|
+
.map((job) => `${job.id} (${job.status} ${formatDuration(Date.now() - job.startedAt)})`);
|
|
144
|
+
if (live.length > 0) {
|
|
145
|
+
return errorResult(`No job "${requested}". Live: ${live.join(', ')}. ` +
|
|
146
|
+
'Call job_list for detail.');
|
|
147
|
+
}
|
|
148
|
+
return errorResult(`No job "${requested}". Start one with bash({ command, background: true }).`);
|
|
149
|
+
}
|
|
150
|
+
function isLive(job) {
|
|
151
|
+
return !isTerminalStatus(job.status);
|
|
152
|
+
}
|
|
153
|
+
export function makeJobWait(deps) {
|
|
154
|
+
const memory = new Map();
|
|
155
|
+
return defineTool({
|
|
156
|
+
name: 'job_wait',
|
|
157
|
+
label: 'Wait for job',
|
|
158
|
+
description: 'Wait a bounded time for a background job (service or task) and return its ' +
|
|
159
|
+
'status, one line of progress, and only the output since `since`. Returns ' +
|
|
160
|
+
'immediately when the job is already finished. Waiting is bounded (5-60s), ' +
|
|
161
|
+
'so call it again only after deciding what to do.',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
job: { type: 'string', description: '"t3", "s1", or "all".' },
|
|
166
|
+
timeoutSeconds: {
|
|
167
|
+
type: 'number',
|
|
168
|
+
description: '5-60. Omit for the configured default.',
|
|
169
|
+
},
|
|
170
|
+
since: {
|
|
171
|
+
type: 'number',
|
|
172
|
+
description: 'Cursor from a previous call; omit for the smallest useful window.',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ['job'],
|
|
176
|
+
},
|
|
177
|
+
async execute(_id, rawParams, ctx) {
|
|
178
|
+
const params = rawParams;
|
|
179
|
+
const requested = String(params.job);
|
|
180
|
+
const targets = requested === 'all'
|
|
181
|
+
? deps.supervisor.list().filter(isLive).map((job) => job.id)
|
|
182
|
+
: [requested];
|
|
183
|
+
if (requested !== 'all' && !deps.supervisor.get(requested)) {
|
|
184
|
+
return unknownJobMessage(deps, requested);
|
|
185
|
+
}
|
|
186
|
+
if (targets.length === 0) {
|
|
187
|
+
return textResult('No background jobs are running. Start one with ' +
|
|
188
|
+
'bash({ command, background: true }).');
|
|
189
|
+
}
|
|
190
|
+
const waitSeconds = clampWaitSeconds(deps, params.timeoutSeconds);
|
|
191
|
+
const outcome = await waitForJob(deps.supervisor, {
|
|
192
|
+
targets,
|
|
193
|
+
timeoutMs: waitSeconds * 1000,
|
|
194
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
195
|
+
...(params.since !== undefined ? { since: params.since } : {}),
|
|
196
|
+
});
|
|
197
|
+
if (outcome.aborted) {
|
|
198
|
+
return errorResult(`The wait on ${targets.join(', ')} was aborted after ` +
|
|
199
|
+
`${formatDuration(outcome.waitedMs)}. The job was not stopped; ` +
|
|
200
|
+
'call job_wait again when you want to keep waiting.');
|
|
201
|
+
}
|
|
202
|
+
const nowMs = Date.now();
|
|
203
|
+
const stallSeconds = deps.stallSeconds();
|
|
204
|
+
const blocks = [];
|
|
205
|
+
for (const job of outcome.jobs) {
|
|
206
|
+
const page = outcome.pages.get(job.id);
|
|
207
|
+
// THE CARRY LINE IS NOT AN INCREMENT WHEN IT HAS NOT MOVED (RV-P1-5). A
|
|
208
|
+
// carriage-return-rewritten progress bar completes no rows, so the ring
|
|
209
|
+
// hands the same line back on every wait: reprinting it would spend the
|
|
210
|
+
// six-row budget on one unchanging sentence, and counting it as new output
|
|
211
|
+
// would reset the spin guard so a genuinely stalled job never earns its
|
|
212
|
+
// `[guard]` line. It still reports through `[progress] (unchanged)`.
|
|
213
|
+
const { rows: rowsNew, carry } = splitCarry(page);
|
|
214
|
+
const carryUnchanged = carry.length > 0 && carry === memory.get(job.id)?.lastCarry;
|
|
215
|
+
const carryMoved = carry.length > 0 && !carryUnchanged;
|
|
216
|
+
const shownRows = carryMoved ? [...rowsNew, carry] : rowsNew;
|
|
217
|
+
const memoryForJob = memory.get(job.id) ?? { noChange: 0 };
|
|
218
|
+
const previous = memoryForJob.lastProgress;
|
|
219
|
+
const current = job.progress?.text;
|
|
220
|
+
// TWO DIFFERENT QUESTIONS, deliberately kept apart:
|
|
221
|
+
// - `progressUnchanged` is a RENDERING decision (print `(unchanged)`);
|
|
222
|
+
// - `advanced` is the SPIN-GUARD decision, and the first wait of a job is
|
|
223
|
+
// always "advanced" because it establishes the baseline. Counting it
|
|
224
|
+
// otherwise would put the guard line on the sixth wait instead of the
|
|
225
|
+
// seventh (AC-20).
|
|
226
|
+
const hasBaseline = previous !== undefined;
|
|
227
|
+
const progressUnchanged = hasBaseline && current === previous;
|
|
228
|
+
const newRows = shownRows.length;
|
|
229
|
+
const advanced = !hasBaseline || newRows > 0 || !progressUnchanged;
|
|
230
|
+
memoryForJob.noChange = advanced ? 0 : memoryForJob.noChange + 1;
|
|
231
|
+
if (current !== undefined)
|
|
232
|
+
memoryForJob.lastProgress = current;
|
|
233
|
+
if (carry.length > 0)
|
|
234
|
+
memoryForJob.lastCarry = carry;
|
|
235
|
+
memory.set(job.id, memoryForJob);
|
|
236
|
+
// THE SPIN GUARD (R-3): counted per job, reset by ANY change. It never
|
|
237
|
+
// refuses the wait; it prepends one line naming four ways out.
|
|
238
|
+
const guard = memoryForJob.noChange >= deps.maxConsecutiveWaits
|
|
239
|
+
? [
|
|
240
|
+
`[guard] ${memoryForJob.noChange} waits with no change. This job may be stalled. ` +
|
|
241
|
+
'Options: keep waiting, stop it (bash_kill), read the tail (bash_output), or ask the user.',
|
|
242
|
+
]
|
|
243
|
+
: [];
|
|
244
|
+
blocks.push([
|
|
245
|
+
...guard,
|
|
246
|
+
...renderJobBlock({
|
|
247
|
+
job,
|
|
248
|
+
rows: shownRows,
|
|
249
|
+
cursor: page?.cursor ?? job.rowsSeen,
|
|
250
|
+
since: params.since,
|
|
251
|
+
nowMs,
|
|
252
|
+
stalledSeconds: stallSeconds,
|
|
253
|
+
waitSeconds,
|
|
254
|
+
progressUnchanged,
|
|
255
|
+
truncatedNote: page?.truncated === true
|
|
256
|
+
? '[note] older rows were dropped - the tail keeps the last ' +
|
|
257
|
+
`${PROC_LIMITS.serviceTailRows} rows` +
|
|
258
|
+
(job.logPath ? `. Full log: ${job.logPath}` : '.')
|
|
259
|
+
: undefined,
|
|
260
|
+
}),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
const shown = blocks.slice(0, PROC_LIMITS.listRows);
|
|
264
|
+
const lines = shown.flat();
|
|
265
|
+
if (blocks.length > shown.length) {
|
|
266
|
+
lines.push(`[... ${blocks.length - shown.length} more job(s): job_list ...]`);
|
|
267
|
+
}
|
|
268
|
+
return textResult(lines.join('\n'));
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
export function makeJobList(deps) {
|
|
273
|
+
return defineTool({
|
|
274
|
+
name: 'job_list',
|
|
275
|
+
label: 'List jobs',
|
|
276
|
+
description: 'One line per background job: id, kind, status, elapsed, progress. Use it ' +
|
|
277
|
+
'when you need the id you started.',
|
|
278
|
+
parameters: { type: 'object', properties: {} },
|
|
279
|
+
async execute() {
|
|
280
|
+
const jobs = deps.supervisor.list();
|
|
281
|
+
if (jobs.length === 0) {
|
|
282
|
+
return textResult('No background jobs. Start one with bash({ command, background: true }).');
|
|
283
|
+
}
|
|
284
|
+
const nowMs = Date.now();
|
|
285
|
+
// LIVE FIRST, then by start time - the same trade `selectPanelRows` makes
|
|
286
|
+
// for the team panel: what is running is what the reader wants.
|
|
287
|
+
const ordered = [...jobs].sort((a, b) => {
|
|
288
|
+
const liveDelta = Number(isLive(b)) - Number(isLive(a));
|
|
289
|
+
return liveDelta !== 0 ? liveDelta : a.startedAt - b.startedAt;
|
|
290
|
+
});
|
|
291
|
+
const rows = ordered.slice(0, PROC_LIMITS.listRows).map((job) => oneRow(job, nowMs));
|
|
292
|
+
const liveCount = jobs.filter(isLive).length;
|
|
293
|
+
const serviceCount = jobs.filter((job) => job.kind === 'service').length;
|
|
294
|
+
const taskCount = jobs.length - serviceCount;
|
|
295
|
+
const summary = `${liveCount} live of ${jobs.length} (${taskCount} task, ${serviceCount} service). ` +
|
|
296
|
+
'Wait: job_wait({ job: "<id>" }). Full tail: bash_output({ service: "<id>", since: <cursor> }).';
|
|
297
|
+
if (ordered.length > rows.length)
|
|
298
|
+
rows.push(`[+${ordered.length - rows.length} more]`);
|
|
299
|
+
return textResult([...rows, summary].join('\n'));
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** `t3 task running 12m34s 47%` - one line, fixed column count. */
|
|
304
|
+
function oneRow(job, nowMs) {
|
|
305
|
+
const elapsed = formatDuration((job.endedAt ?? nowMs) - job.startedAt);
|
|
306
|
+
const aside = job.progress?.text ?? job.url ?? '';
|
|
307
|
+
return `${job.id} ${job.kind} ${job.status} ${elapsed}${aside ? ` ${aside}` : ''}`;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Append pending `<job_notices>` to every tool result (the PULL channel).
|
|
311
|
+
*
|
|
312
|
+
* SAME SHAPE AS `withMailboxTail`, AND FOR THE SAME REASON: the engine is told
|
|
313
|
+
* nothing, the wrapper rides `{...tool, execute}`, and the notice is delivered on
|
|
314
|
+
* the next tool call whatever that call is. The queue is drained EVEN WHEN the
|
|
315
|
+
* wrapped tool errored - a refused `bash` is still a delivery opportunity.
|
|
316
|
+
*/
|
|
317
|
+
export function withJobNoticeTail(tool, drain) {
|
|
318
|
+
const original = tool.execute;
|
|
319
|
+
return {
|
|
320
|
+
...tool,
|
|
321
|
+
async execute(id, params, ctx) {
|
|
322
|
+
const result = await original(id, params, ctx);
|
|
323
|
+
const block = renderJobNotices(drain());
|
|
324
|
+
if (block.length === 0)
|
|
325
|
+
return result;
|
|
326
|
+
return { ...result, content: [...result.content, { type: 'text', text: block }] };
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* command is still arbitrary.
|
|
18
18
|
*/
|
|
19
19
|
import { type AgentTool } from '@aragon-agent/core';
|
|
20
|
-
import type
|
|
20
|
+
import { type ProcSupervisorPort } from '../proc/types.js';
|
|
21
21
|
export interface ProcToolDeps {
|
|
22
22
|
supervisor: ProcSupervisorPort;
|
|
23
23
|
}
|
package/dist/tools/proc-tools.js
CHANGED
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { defineTool, errorResult, textResult } from '@aragon-agent/core';
|
|
20
20
|
import { PROC_LIMITS } from '../proc/limits.js';
|
|
21
|
-
|
|
21
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
22
|
+
/**
|
|
23
|
+
* One line of status, shared by both tools so the two can never disagree.
|
|
24
|
+
*
|
|
25
|
+
* IT NAMES THE KIND, because `s1` and `t3` are two different things to be holding
|
|
26
|
+
* and the model has to know which one it is looking at to decide whether waiting
|
|
27
|
+
* is even meaningful.
|
|
28
|
+
*/
|
|
22
29
|
function statusLine(service) {
|
|
23
|
-
const bits = [
|
|
30
|
+
const bits = [`${service.kind} ${service.id} ${service.status}`];
|
|
24
31
|
if (service.url)
|
|
25
32
|
bits.push(service.url);
|
|
26
33
|
if (service.exitCode !== null)
|
|
@@ -31,18 +38,30 @@ function statusLine(service) {
|
|
|
31
38
|
bits.push('(may have left a detached child)');
|
|
32
39
|
return `${bits.join(' ')}\n$ ${service.command}`;
|
|
33
40
|
}
|
|
41
|
+
/** `[progress] 47%` and `[stalled] ...` - the two lines worth adding to a tail. */
|
|
42
|
+
function progressLines(service) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
if (service.progress)
|
|
45
|
+
lines.push(`[progress] ${service.progress.text}`);
|
|
46
|
+
if (service.stalled === true) {
|
|
47
|
+
lines.push('[stalled] no output for a while - it has not failed. Keep waiting, stop it ' +
|
|
48
|
+
`with bash_kill({ service: "${service.id}" }), or read the tail.`);
|
|
49
|
+
}
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
34
52
|
export function makeBashOutput(deps) {
|
|
35
53
|
return defineTool({
|
|
36
54
|
name: 'bash_output',
|
|
37
|
-
label: '
|
|
38
|
-
description: 'Read a background
|
|
55
|
+
label: 'Job log',
|
|
56
|
+
description: 'Read a background job log tail and its current status. Pass the ' +
|
|
39
57
|
'`since` cursor returned by a previous call to read only what is new. ' +
|
|
40
|
-
'
|
|
41
|
-
'test watcher
|
|
58
|
+
'Accepts both ids: "s1" for a service and "t3" for a task. Use this to ' +
|
|
59
|
+
'verify a service actually started, and to read a build or test watcher ' +
|
|
60
|
+
'as it runs.',
|
|
42
61
|
parameters: {
|
|
43
62
|
type: 'object',
|
|
44
63
|
properties: {
|
|
45
|
-
service: { type: 'string', description: 'The
|
|
64
|
+
service: { type: 'string', description: 'The job id: "s1" or "t3".' },
|
|
46
65
|
since: {
|
|
47
66
|
type: 'number',
|
|
48
67
|
description: 'Cursor from a previous call; omit to read the whole tail.',
|
|
@@ -54,13 +73,18 @@ export function makeBashOutput(deps) {
|
|
|
54
73
|
const params = rawParams;
|
|
55
74
|
const service = deps.supervisor.get(params.service);
|
|
56
75
|
if (!service) {
|
|
57
|
-
return errorResult(`No
|
|
76
|
+
return errorResult(`No job "${params.service}". Start one with bash({ command, background: true }).`);
|
|
58
77
|
}
|
|
59
78
|
const page = deps.supervisor.read(params.service, params.since);
|
|
60
|
-
const lines = [statusLine(service)];
|
|
79
|
+
const lines = [statusLine(service), ...progressLines(service)];
|
|
61
80
|
if (page?.truncated) {
|
|
81
|
+
// THE PATH IS THE ESCAPE HATCH (long-task-jobs §4.8). Past 200 rows the
|
|
82
|
+
// ring has forgotten the error the model needs, and the full log is on
|
|
83
|
+
// disk; naming it lets `grep`/`read_file` answer a question this buffer
|
|
84
|
+
// no longer can.
|
|
62
85
|
lines.push(`[note] older rows were dropped - the tail keeps the last ` +
|
|
63
|
-
`${PROC_LIMITS.serviceTailRows} rows
|
|
86
|
+
`${PROC_LIMITS.serviceTailRows} rows` +
|
|
87
|
+
(service.logPath ? `. Full log: ${service.logPath}` : '.'));
|
|
64
88
|
}
|
|
65
89
|
if (!page || page.rows.length === 0) {
|
|
66
90
|
lines.push('(no new output)');
|
|
@@ -76,27 +100,35 @@ export function makeBashOutput(deps) {
|
|
|
76
100
|
export function makeBashKill(deps) {
|
|
77
101
|
return defineTool({
|
|
78
102
|
name: 'bash_kill',
|
|
79
|
-
label: 'Stop
|
|
80
|
-
description: 'Stop a background service by id, or every
|
|
81
|
-
'
|
|
82
|
-
'asked
|
|
103
|
+
label: 'Stop job',
|
|
104
|
+
description: 'Stop a background job (service or task) by id, or every job with "all". ' +
|
|
105
|
+
'Stop the jobs you started once you are finished with them, unless the ' +
|
|
106
|
+
'user asked them to stay up.',
|
|
83
107
|
parameters: {
|
|
84
108
|
type: 'object',
|
|
85
109
|
properties: {
|
|
86
|
-
service: { type: 'string', description: 'The
|
|
110
|
+
service: { type: 'string', description: 'The job id ("s1" or "t3"), or "all".' },
|
|
87
111
|
},
|
|
88
112
|
required: ['service'],
|
|
89
113
|
},
|
|
90
114
|
async execute(_id, rawParams) {
|
|
91
115
|
const params = rawParams;
|
|
92
116
|
if (params.service !== 'all' && !deps.supervisor.get(params.service)) {
|
|
93
|
-
return errorResult(`No
|
|
117
|
+
return errorResult(`No job "${params.service}".`);
|
|
94
118
|
}
|
|
95
119
|
const stopped = await deps.supervisor.stop(params.service);
|
|
96
120
|
if (stopped.length === 0)
|
|
97
121
|
return textResult('Nothing to stop.');
|
|
98
122
|
const lines = stopped.map((s) => statusLine(s));
|
|
99
|
-
|
|
123
|
+
// A JOB THAT HAD ALREADY FINISHED IS NOT A STOP (AC-37). `stop()` returns
|
|
124
|
+
// the snapshot either way, so the count has to exclude terminal records or
|
|
125
|
+
// `bash_kill` reports having killed something that ended ten minutes ago.
|
|
126
|
+
const ended = stopped.filter((s) => isTerminalStatus(s.status)).length;
|
|
127
|
+
const killed = stopped.length - ended;
|
|
128
|
+
const summary = killed === 0
|
|
129
|
+
? `Nothing to stop - ${stopped.length === 1 ? 'it had' : 'they had'} already finished.`
|
|
130
|
+
: `Stopped ${killed} job${killed === 1 ? '' : 's'}.`;
|
|
131
|
+
return textResult(`${summary}\n${lines.join('\n')}`);
|
|
100
132
|
},
|
|
101
133
|
});
|
|
102
134
|
}
|
package/dist/ui/ActivityLine.js
CHANGED
|
@@ -10,6 +10,13 @@ export function liveSpinner(reducedMotion, caps) {
|
|
|
10
10
|
}
|
|
11
11
|
/** Explicit execution facts outrank compatibility tool names and rotating phrases. */
|
|
12
12
|
export function resolveActivityLabel(activity, glyphs) {
|
|
13
|
+
// A WAIT OUTRANKS EVERYTHING BELOW IT, and sits ABOVE `runningTool` on purpose:
|
|
14
|
+
// "Running job_wait" names the tool, "Waiting on t3 (up to 30s)" names the
|
|
15
|
+
// situation. It is placed after the phase/outcome branch because a settled run
|
|
16
|
+
// must never claim to be waiting for anything.
|
|
17
|
+
if (activity.phase === undefined && activity.outcome === undefined && activity.wait) {
|
|
18
|
+
return `Waiting on ${activity.wait.jobId} (up to ${activity.wait.upToSeconds}s)`;
|
|
19
|
+
}
|
|
13
20
|
if (activity.phase !== undefined || activity.activeTool !== undefined || activity.outcome !== undefined) {
|
|
14
21
|
return phaseLabels({
|
|
15
22
|
phase: activity.phase ?? (activity.activeTool ? 'tool' : 'idle'),
|
package/dist/ui/App.js
CHANGED
|
@@ -41,6 +41,9 @@ import { AppShell } from './layout/AppShell.js';
|
|
|
41
41
|
import { ScrollbackShell } from './ScrollbackShell.js';
|
|
42
42
|
import { useScrollbackView } from './scrollback/use-scrollback-view.js';
|
|
43
43
|
import { buildExitSummary } from './scrollback/exit-summary.js';
|
|
44
|
+
import { LiveTail } from './scrollback/LiveTail.js';
|
|
45
|
+
import { SCROLLBACK_LIMITS } from './scrollback/limits.js';
|
|
46
|
+
import { TODO_LIMITS } from '../todo/limits.js';
|
|
44
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
45
48
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
46
49
|
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
@@ -2247,11 +2250,27 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2247
2250
|
// there is the exact cost `tui-render-performance` exists to remove. The
|
|
2248
2251
|
// running tool, when there is one, is within a few entries of the end.
|
|
2249
2252
|
let runningTool;
|
|
2253
|
+
// THE WAIT PROJECTION (long-task-jobs §4.9 / AC-28). Derived here, in the same
|
|
2254
|
+
// reverse loop, because both answers come from the same place: which tool is in
|
|
2255
|
+
// flight, and — when that tool is `job_wait` — which job it is waiting for and
|
|
2256
|
+
// for how long. The timeout is read from the CALL'S OWN ARGUMENTS rather than
|
|
2257
|
+
// from the config, so a `timeoutSeconds` the model chose is what the row says.
|
|
2258
|
+
let wait;
|
|
2250
2259
|
if (running) {
|
|
2251
2260
|
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
2252
2261
|
const e = state.entries[i];
|
|
2253
2262
|
if (e.kind === 'tool' && e.status === 'running') {
|
|
2254
2263
|
runningTool = e.name;
|
|
2264
|
+
if (e.name === 'job_wait' && e.args) {
|
|
2265
|
+
const args = e.args;
|
|
2266
|
+
const requested = typeof args.timeoutSeconds === 'number'
|
|
2267
|
+
? args.timeoutSeconds
|
|
2268
|
+
: cfg.jobs.waitSeconds;
|
|
2269
|
+
wait = {
|
|
2270
|
+
jobId: typeof args.job === 'string' ? args.job : 'job',
|
|
2271
|
+
upToSeconds: Math.max(0, Math.round(requested)),
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2255
2274
|
break;
|
|
2256
2275
|
}
|
|
2257
2276
|
}
|
|
@@ -2284,8 +2303,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2284
2303
|
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
2285
2304
|
});
|
|
2286
2305
|
const { visible: railVisible, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
2287
|
-
//
|
|
2288
|
-
|
|
2306
|
+
// The FULL-SCREEN rail only: native mode draws its rail inside the live region's
|
|
2307
|
+
// panel band (`nativeRail` below), because there is no fixed frame to hang a column
|
|
2308
|
+
// on. This name says which of the two it is; the old `showRail` read like a global
|
|
2309
|
+
// switch and that is how native mode ended up with no rail at all.
|
|
2310
|
+
const fullscreenRailVisible = railVisible && !native;
|
|
2289
2311
|
const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
|
|
2290
2312
|
// --- Native scroll (tui-native-terminal-scroll). -----------------------------
|
|
2291
2313
|
//
|
|
@@ -2303,6 +2325,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2303
2325
|
reducedMotion, rows, cols, headerNode: header, openerNode: opener,
|
|
2304
2326
|
overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
|
|
2305
2327
|
statusExpanded, forceInactive: native && guardTripped,
|
|
2328
|
+
// The band's inputs. `teamRowsNatural` is the roster layout computed above, so the
|
|
2329
|
+
// band clamps a number this frame already trusts instead of measuring the roster a
|
|
2330
|
+
// second time (`buildTeamPanelLayout` stays the single authority).
|
|
2331
|
+
itemCount: state.todos?.items.length ?? 0,
|
|
2332
|
+
panelEnabled: cfg.todo.panel,
|
|
2333
|
+
teamRowsNatural: teamLayout.rowCount,
|
|
2306
2334
|
});
|
|
2307
2335
|
scrollbackViewRef.current = scrollbackView;
|
|
2308
2336
|
// The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
|
|
@@ -2384,6 +2412,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2384
2412
|
mode: native ? 'native' : 'fullscreen',
|
|
2385
2413
|
viewportRows: nativeBudget ? nativeBudget.liveRows : viewportBudget,
|
|
2386
2414
|
offset: scrolledLines,
|
|
2415
|
+
// `null` outside native mode, and `/perf` prints `n/a` for it: a band row of
|
|
2416
|
+
// zeroes in full-screen mode would read like a panel that failed to show.
|
|
2417
|
+
band: nativeBudget
|
|
2418
|
+
? { ...nativeBudget.band, contentCols: nativeBudget.contentCols }
|
|
2419
|
+
: null,
|
|
2387
2420
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2388
2421
|
get scrollbar() {
|
|
2389
2422
|
return readScrollbarPerf({
|
|
@@ -2434,6 +2467,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2434
2467
|
columns: viewCols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
|
|
2435
2468
|
context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
|
|
2436
2469
|
elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
|
|
2470
|
+
// The bounded-wait label, when `job_wait` is what is holding the run. Absent
|
|
2471
|
+
// for every other tool, which keeps every existing plan byte-identical.
|
|
2472
|
+
...(wait ? { wait } : {}),
|
|
2437
2473
|
waitingForConfirmation: !!humanRequest || !!confirmState,
|
|
2438
2474
|
stopping: interruptGesture.current.phase === 'stopping',
|
|
2439
2475
|
interruptPhase: interruptGesture.current.phase,
|
|
@@ -2456,10 +2492,25 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2456
2492
|
updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
|
|
2457
2493
|
const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onDraftPresence: onDraftPresence, draftClearNonce: draftClearNonce, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
|
|
2458
2494
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2459
|
-
|
|
2495
|
+
// --- Native panel band (release-gate-and-native-panels §6.3/§6.4) -------------
|
|
2496
|
+
//
|
|
2497
|
+
// SEPARATE NODES, NOT THE TWO ABOVE (RV-P1-7). Those carry `cols={contentCols}` for
|
|
2498
|
+
// the full-screen layout, which has to stay byte-identical (I-3); reusing them here
|
|
2499
|
+
// would silently narrow it. Every number below comes from `budget.band`, and the
|
|
2500
|
+
// number that decides whether a panel mounts IS the number it lays out against - a
|
|
2501
|
+
// panel that mounts on one value and lays out against another renders nothing and
|
|
2502
|
+
// reports nothing (TodoPanel's own I-10 / P1-3 note).
|
|
2503
|
+
const nativeBand = nativeBudget ? nativeBudget.band : null;
|
|
2504
|
+
const nativeRail = state.todos && nativeBand && nativeBand.railRows >= TODO_LIMITS.panelMinRows
|
|
2505
|
+
? (_jsx(TodoPanel, { snapshot: state.todos, width: nativeBand.railCols, rows: nativeBand.railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2506
|
+
// `cols` is the band's left column, not `contentCols`: `TeamPanel.activityBudget` is
|
|
2507
|
+
// continuous in `cols`, so a roster laid out against a column it is not in is exactly
|
|
2508
|
+
// the bug that pushes the elapsed column off an 80-column terminal.
|
|
2509
|
+
const nativeTeam = state.team && nativeBand ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: nativeBand.previewCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2510
|
+
const rail = fullscreenRailVisible && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2460
2511
|
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
|
|
2461
2512
|
const detailsNode = _jsx(BottomStatusRow, { plan: details, columns: viewCols, theme: theme });
|
|
2462
|
-
const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2513
|
+
const statusNode = (_jsx(StatusBar, { columns: viewCols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, ...(wait ? { wait } : {}), runOutcome: state.runOutcome, reducedMotion: reducedMotion, ...(sessionId ? { sessionId } : {}), waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2463
2514
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2464
2515
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2465
2516
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -2475,7 +2526,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2475
2526
|
total: state.team.runs.length,
|
|
2476
2527
|
},
|
|
2477
2528
|
}
|
|
2478
|
-
: {}), ...(state.todos && !
|
|
2529
|
+
: {}), ...(state.todos && !(native
|
|
2530
|
+
? (nativeBudget?.band.railRows ?? 0) > 0
|
|
2531
|
+
: fullscreenRailVisible)
|
|
2479
2532
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2480
2533
|
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2481
2534
|
// Manual compaction starts before its next snapshot is published.
|
|
@@ -2499,7 +2552,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2499
2552
|
}
|
|
2500
2553
|
: {}) }));
|
|
2501
2554
|
if (nativeBudget) {
|
|
2502
|
-
return (_jsx(ScrollbackShell, { budget: nativeBudget, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines,
|
|
2555
|
+
return (_jsx(ScrollbackShell, { budget: nativeBudget, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines,
|
|
2556
|
+
// Real components for the unfinished tail (I-7); the plain rows above stay as
|
|
2557
|
+
// the fallback for a frame with no preview rows.
|
|
2558
|
+
tail: nativeBudget.band.previewRows > 0
|
|
2559
|
+
? (_jsx(LiveTail, { unsettled: scrollbackView.unsettledEntries, maxEntries: SCROLLBACK_LIMITS.liveTailMaxEntries, rows: nativeBudget.band.previewRows, cols: nativeBudget.previewCols, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: cfg.density, nowSec: nowSec, theme: theme, caps: caps }))
|
|
2560
|
+
: undefined, team: nativeTeam, rail: nativeRail, composer: composer, status: statusNode, details: detailsNode, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), theme: theme }));
|
|
2503
2561
|
}
|
|
2504
2562
|
return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: detailsNode, status: statusNode }));
|
|
2505
2563
|
}
|