@llm4ts/runner 2.40.1 → 2.42.0
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/dist/AgentTree.d.ts +70 -8
- package/dist/AgentTree.d.ts.map +1 -1
- package/dist/AgentTree.js +516 -103
- package/dist/AgentTree.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +9 -3
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +13 -0
- package/dist/Terminal.js.map +1 -1
- package/package.json +3 -3
- package/src/AgentTree.ts +709 -126
- package/src/FlowRunner.ts +13 -3
- package/src/Terminal.ts +13 -0
package/dist/AgentTree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Option from "effect/Option";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import {
|
|
3
|
+
import { toolCategory } from "@llm4ts/flow/Activity";
|
|
4
|
+
import { FlowEvent, cloneName } from "@llm4ts/flow/FlowEvents";
|
|
4
5
|
import { estimateCostUsd } from "@llm4ts/flow/PriceList";
|
|
5
6
|
import { formatCount } from "./Terminal.js";
|
|
6
7
|
export const defaultIdleAfterMs = 120_000;
|
|
@@ -24,7 +25,7 @@ export const emptyTree = (options = {}) => ({
|
|
|
24
25
|
executors: [],
|
|
25
26
|
leases: [],
|
|
26
27
|
exclusions: [],
|
|
27
|
-
judge: { executor: undefined, reviews: 0, verdicts: 0 },
|
|
28
|
+
judge: { executor: undefined, reviews: 0, verdicts: 0, borrowed: 0 },
|
|
28
29
|
verdict: undefined,
|
|
29
30
|
judgments: [],
|
|
30
31
|
log: [],
|
|
@@ -33,7 +34,8 @@ export const emptyTree = (options = {}) => ({
|
|
|
33
34
|
ended: undefined,
|
|
34
35
|
time: { model: 0, tools: 0, gates: 0, merge: 0, wait: 0 },
|
|
35
36
|
running: [],
|
|
36
|
-
idleAfterMs: options.idleAfterMs ?? defaultIdleAfterMs
|
|
37
|
+
idleAfterMs: options.idleAfterMs ?? defaultIdleAfterMs,
|
|
38
|
+
lastChanged: undefined
|
|
37
39
|
});
|
|
38
40
|
const costOf = (model, usage) => usage.costUsd ?? (model === undefined ? undefined : estimateCostUsd(model, usage)) ?? 0;
|
|
39
41
|
const logged = (state, entry) => ({
|
|
@@ -46,6 +48,34 @@ const updateLane = (state, id, update) => ({
|
|
|
46
48
|
...state,
|
|
47
49
|
lanes: state.lanes.map((lane) => (lane.id === id ? update(lane) : lane))
|
|
48
50
|
});
|
|
51
|
+
/** A wait label that is a harness pausing itself, not the roster or a gate. */
|
|
52
|
+
const isPause = (label) => /^pi (retry|compaction)$/u.test(label);
|
|
53
|
+
/**
|
|
54
|
+
* A delegate tool that starts a sub-agent: Claude's Agent (Task before
|
|
55
|
+
* 2.1.63), Codex's spawn_agent, Gemini's agents. Codex's wait, send_input,
|
|
56
|
+
* resume_agent and close_agent act on one that exists.
|
|
57
|
+
*/
|
|
58
|
+
const spawningTools = /^(agent|task|spawn_agent|codebase_investigator|generalist|cli_help|browser_agent)$/iu;
|
|
59
|
+
/**
|
|
60
|
+
* A delegate call whose end ends the sub-agent: a synchronous one (Agent,
|
|
61
|
+
* a Gemini agent) or Codex's wait and close; spawn_agent returns at once
|
|
62
|
+
* with the sub-agent still running (ADR 0033).
|
|
63
|
+
*/
|
|
64
|
+
const endsChild = (tool) => !/^(spawn_agent|send_input|resume_agent)$/iu.test(tool);
|
|
65
|
+
const newestOpenChild = (children) => {
|
|
66
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
67
|
+
if (children[index]?.ended === false) {
|
|
68
|
+
return index;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
};
|
|
73
|
+
const endNewestChild = (children) => {
|
|
74
|
+
const index = newestOpenChild(children);
|
|
75
|
+
return index < 0
|
|
76
|
+
? children
|
|
77
|
+
: children.map((child, position) => (position === index ? { ...child, ended: true } : child));
|
|
78
|
+
};
|
|
49
79
|
const withoutLast = (items, item) => {
|
|
50
80
|
const index = items.lastIndexOf(item);
|
|
51
81
|
return index < 0 ? items : [...items.slice(0, index), ...items.slice(index + 1)];
|
|
@@ -62,30 +92,38 @@ const withExecutor = (state, executor) => state.executors.includes(executor)
|
|
|
62
92
|
? state
|
|
63
93
|
: { ...state, executors: [...state.executors, executor] };
|
|
64
94
|
const rosterLog = (state, at, what) => logged(state, { at, source: "roster", who: "roster", what });
|
|
65
|
-
const reduceLease = (state, at, lease) => {
|
|
95
|
+
const reduceLease = (state, at, lease, borrowed) => {
|
|
66
96
|
const held = { ...withExecutor(state, lease.executor), leases: [...state.leases, lease] };
|
|
97
|
+
const who = cloneName(lease.executor, lease.clone);
|
|
98
|
+
const forLabel = lease.label === undefined ? "" : ` · ${lease.label}`;
|
|
67
99
|
switch (lease.role) {
|
|
68
100
|
case "coder": {
|
|
69
101
|
const lane = held.lanes.find((candidate) => candidate.id === lease.label);
|
|
70
102
|
const placed = lane === undefined
|
|
71
103
|
? held
|
|
72
|
-
: updateLane(held, lane.id, (open) => ({
|
|
73
|
-
|
|
104
|
+
: updateLane(held, lane.id, (open) => ({
|
|
105
|
+
...open,
|
|
106
|
+
executor: lease.executor,
|
|
107
|
+
clone: lease.clone
|
|
108
|
+
}));
|
|
109
|
+
return rosterLog(placed, at, `${who} → coder${forLabel}`);
|
|
74
110
|
}
|
|
75
111
|
case "reviewer":
|
|
76
|
-
|
|
112
|
+
case "judge": {
|
|
113
|
+
const seat = {
|
|
77
114
|
...held,
|
|
78
115
|
judge: {
|
|
79
116
|
...held.judge,
|
|
80
|
-
executor: held.judge.executor ?? lease.executor,
|
|
81
|
-
reviews: held.judge.reviews + 1
|
|
117
|
+
executor: lease.role === "judge" ? lease.executor : (held.judge.executor ?? lease.executor),
|
|
118
|
+
reviews: held.judge.reviews + (lease.role === "reviewer" ? 1 : 0),
|
|
119
|
+
verdicts: held.judge.verdicts + (lease.role === "judge" ? 1 : 0),
|
|
120
|
+
borrowed: held.judge.borrowed + (borrowed ? 1 : 0)
|
|
82
121
|
}
|
|
83
122
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
123
|
+
return borrowed
|
|
124
|
+
? rosterLog(seat, at, `${lease.role} borrowed from own executor ${who}${forLabel}`)
|
|
125
|
+
: seat;
|
|
126
|
+
}
|
|
89
127
|
default:
|
|
90
128
|
return held;
|
|
91
129
|
}
|
|
@@ -102,11 +140,14 @@ const reduceEvent = (state, at, event) => {
|
|
|
102
140
|
: { ...state, executors: [...state.executors, executor] };
|
|
103
141
|
const current = lane === undefined
|
|
104
142
|
? named
|
|
105
|
-
:
|
|
106
|
-
...open
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
: {
|
|
144
|
+
...updateLane(named, lane.id, (open) => ({
|
|
145
|
+
...open,
|
|
146
|
+
lastEventAt: at,
|
|
147
|
+
...(executor !== undefined && open.executor !== executor ? { executor } : {})
|
|
148
|
+
})),
|
|
149
|
+
lastChanged: lane.id
|
|
150
|
+
};
|
|
110
151
|
switch (event._tag) {
|
|
111
152
|
case "StageStarted": {
|
|
112
153
|
const story = storyStage.exec(event.stage)?.[1];
|
|
@@ -125,11 +166,18 @@ const reduceEvent = (state, at, event) => {
|
|
|
125
166
|
activity: undefined,
|
|
126
167
|
turns: [],
|
|
127
168
|
gatesMs: 0,
|
|
128
|
-
running: []
|
|
169
|
+
running: [],
|
|
170
|
+
clone: undefined,
|
|
171
|
+
tasks: [],
|
|
172
|
+
task: undefined,
|
|
173
|
+
children: [],
|
|
174
|
+
pause: undefined,
|
|
175
|
+
taskCoders: []
|
|
129
176
|
};
|
|
130
177
|
return {
|
|
131
178
|
...current,
|
|
132
|
-
lanes: [...current.lanes.filter((open) => open.id !== story), fresh]
|
|
179
|
+
lanes: [...current.lanes.filter((open) => open.id !== story), fresh],
|
|
180
|
+
lastChanged: story
|
|
133
181
|
};
|
|
134
182
|
}
|
|
135
183
|
if (lane !== undefined) {
|
|
@@ -138,7 +186,10 @@ const reduceEvent = (state, at, event) => {
|
|
|
138
186
|
stages: [...open.stages, event.stage]
|
|
139
187
|
}));
|
|
140
188
|
}
|
|
141
|
-
|
|
189
|
+
// A lane that is not a story's (a task run aside, ADR 0034) is not the run's either.
|
|
190
|
+
return event.lane === undefined
|
|
191
|
+
? { ...current, stages: [...current.stages, event.stage] }
|
|
192
|
+
: current;
|
|
142
193
|
}
|
|
143
194
|
case "StageCompleted":
|
|
144
195
|
case "StageFailed": {
|
|
@@ -163,7 +214,9 @@ const reduceEvent = (state, at, event) => {
|
|
|
163
214
|
stages: withoutLast(open.stages, event.stage)
|
|
164
215
|
}));
|
|
165
216
|
}
|
|
166
|
-
return
|
|
217
|
+
return event.lane === undefined
|
|
218
|
+
? { ...current, stages: withoutLast(current.stages, event.stage) }
|
|
219
|
+
: current;
|
|
167
220
|
}
|
|
168
221
|
case "Info": {
|
|
169
222
|
// A story's own milestones are `story <id>: …` (resuming, merged,
|
|
@@ -211,7 +264,15 @@ const reduceEvent = (state, at, event) => {
|
|
|
211
264
|
: event.kind === "model" && event.label === "coder"
|
|
212
265
|
? { activity: undefined, turns: [...open.turns, event.ms] }
|
|
213
266
|
: {}),
|
|
214
|
-
gatesMs: open.gatesMs + (event.kind === "gate" ? event.ms : 0)
|
|
267
|
+
gatesMs: open.gatesMs + (event.kind === "gate" ? event.ms : 0),
|
|
268
|
+
// The delegate call ended: its sub-agent is done — unless it was
|
|
269
|
+
// only spawned, and runs on until a wait or close.
|
|
270
|
+
...(event.kind === "tool" && event.category === "delegate" && endsChild(event.label)
|
|
271
|
+
? { children: endNewestChild(open.children) }
|
|
272
|
+
: {}),
|
|
273
|
+
...(event.kind === "wait" && open.pause?.label === event.label
|
|
274
|
+
? { pause: undefined }
|
|
275
|
+
: {})
|
|
215
276
|
}));
|
|
216
277
|
}
|
|
217
278
|
case "Began": {
|
|
@@ -220,7 +281,14 @@ const reduceEvent = (state, at, event) => {
|
|
|
220
281
|
? event.lane === undefined
|
|
221
282
|
? { ...current, running: [...current.running, work] }
|
|
222
283
|
: current
|
|
223
|
-
: updateLane(current, lane.id, (open) => ({
|
|
284
|
+
: updateLane(current, lane.id, (open) => ({
|
|
285
|
+
...open,
|
|
286
|
+
running: [...open.running, work],
|
|
287
|
+
// A harness pause (`pi retry`, `pi compaction`) is the lane's state while it lasts.
|
|
288
|
+
...(event.kind === "wait" && isPause(event.label)
|
|
289
|
+
? { pause: { label: event.label, since: at } }
|
|
290
|
+
: {})
|
|
291
|
+
}));
|
|
224
292
|
}
|
|
225
293
|
case "StoryJudged": {
|
|
226
294
|
const verdict = {
|
|
@@ -246,11 +314,7 @@ const reduceEvent = (state, at, event) => {
|
|
|
246
314
|
]
|
|
247
315
|
};
|
|
248
316
|
case "ExecutorLeased":
|
|
249
|
-
return reduceLease(current, at, {
|
|
250
|
-
executor: event.executor,
|
|
251
|
-
role: event.role,
|
|
252
|
-
label: event.label
|
|
253
|
-
});
|
|
317
|
+
return reduceLease(current, at, { executor: event.executor, role: event.role, label: event.label, clone: event.clone }, event.borrowed === true);
|
|
254
318
|
case "ExecutorReleased": {
|
|
255
319
|
const index = current.leases.findIndex((lease) => lease.executor === event.executor &&
|
|
256
320
|
lease.role === event.role &&
|
|
@@ -277,15 +341,107 @@ const reduceEvent = (state, at, event) => {
|
|
|
277
341
|
}, at, `${event.executor} back · ${event.why}`);
|
|
278
342
|
case "ExecutorHandedOver":
|
|
279
343
|
return rosterLog(current, at, `${event.label ?? "the run"}: ${event.role} leaves ${event.from} · ${event.reason}`);
|
|
280
|
-
case "ToolUse":
|
|
344
|
+
case "ToolUse": {
|
|
345
|
+
if (lane === undefined) {
|
|
346
|
+
return current;
|
|
347
|
+
}
|
|
348
|
+
const text = `${event.tool} ${event.args}`;
|
|
349
|
+
return updateLane(current, lane.id, (open) => {
|
|
350
|
+
// A call inside a sub-agent lands on the newest open child; without
|
|
351
|
+
// one (an older harness, a lost parent) it is the lane's as before.
|
|
352
|
+
const child = event.parent === undefined ? -1 : newestOpenChild(open.children);
|
|
353
|
+
if (child >= 0) {
|
|
354
|
+
return {
|
|
355
|
+
...open,
|
|
356
|
+
children: open.children.map((candidate, index) => index === child ? { ...candidate, lastTool: text } : candidate)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const delegated = toolCategory(event.tool, event.args) === "delegate" && spawningTools.test(event.tool)
|
|
360
|
+
? [
|
|
361
|
+
...open.children,
|
|
362
|
+
{
|
|
363
|
+
id: `${open.id}:${open.children.length + 1}`,
|
|
364
|
+
tool: event.tool,
|
|
365
|
+
args: event.args,
|
|
366
|
+
since: at,
|
|
367
|
+
lastTool: undefined,
|
|
368
|
+
ended: false
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
: open.children;
|
|
372
|
+
return {
|
|
373
|
+
...open,
|
|
374
|
+
activity: { text, since: at, tool: true },
|
|
375
|
+
lastTool: text,
|
|
376
|
+
tools: [...open.tools, text].slice(-expandedTools),
|
|
377
|
+
children: delegated
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
case "TasksPlanned":
|
|
382
|
+
return lane === undefined
|
|
383
|
+
? current
|
|
384
|
+
: updateLane(current, lane.id, (open) => ({
|
|
385
|
+
...open,
|
|
386
|
+
tasks: event.tasks.map((task, position) => ({
|
|
387
|
+
index: position + 1,
|
|
388
|
+
title: task.title,
|
|
389
|
+
done: task.completed,
|
|
390
|
+
...(task.satisfies === undefined ? {} : { satisfies: task.satisfies })
|
|
391
|
+
}))
|
|
392
|
+
}));
|
|
393
|
+
case "TaskStarted": {
|
|
394
|
+
if (lane === undefined) {
|
|
395
|
+
return current;
|
|
396
|
+
}
|
|
397
|
+
const satisfies = event.satisfies === undefined ? {} : { satisfies: event.satisfies };
|
|
398
|
+
const listed = (open) => open.tasks.some((task) => task.index === event.index)
|
|
399
|
+
? open.tasks
|
|
400
|
+
: [
|
|
401
|
+
...open.tasks,
|
|
402
|
+
{ index: event.index, title: event.title, done: false, ...satisfies }
|
|
403
|
+
].sort((left, right) => left.index - right.index);
|
|
404
|
+
if (event.parallel === true) {
|
|
405
|
+
return updateLane(current, lane.id, (open) => ({
|
|
406
|
+
...open,
|
|
407
|
+
tasks: listed(open),
|
|
408
|
+
taskCoders: [
|
|
409
|
+
...open.taskCoders.filter((coder) => coder.index !== event.index),
|
|
410
|
+
{
|
|
411
|
+
index: event.index,
|
|
412
|
+
count: event.count,
|
|
413
|
+
title: event.title,
|
|
414
|
+
executor: event.executor,
|
|
415
|
+
clone: event.clone,
|
|
416
|
+
since: at
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
return updateLane(current, lane.id, (open) => ({
|
|
422
|
+
...open,
|
|
423
|
+
task: { index: event.index, count: event.count, title: event.title, ...satisfies },
|
|
424
|
+
tasks: listed(open),
|
|
425
|
+
// Back with the story's coder: no longer beside it.
|
|
426
|
+
taskCoders: open.taskCoders.filter((coder) => coder.index !== event.index)
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
case "TaskCompleted":
|
|
281
430
|
return lane === undefined
|
|
282
431
|
? current
|
|
283
432
|
: updateLane(current, lane.id, (open) => ({
|
|
284
433
|
...open,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
434
|
+
task: open.task?.index === event.index ? undefined : open.task,
|
|
435
|
+
taskCoders: open.taskCoders.filter((coder) => coder.index !== event.index),
|
|
436
|
+
tasks: open.tasks.map((task) => task.index === event.index ? { ...task, done: true } : task)
|
|
288
437
|
}));
|
|
438
|
+
case "StoryStatusChanged":
|
|
439
|
+
return {
|
|
440
|
+
...current,
|
|
441
|
+
stories: current.stories.some((story) => story.id === event.id)
|
|
442
|
+
? current.stories.map((story) => story.id === event.id ? { ...story, status: event.status } : story)
|
|
443
|
+
: [...current.stories, { id: event.id, status: event.status }]
|
|
444
|
+
};
|
|
289
445
|
case "TokensUsed": {
|
|
290
446
|
const tokens = event.usage.total;
|
|
291
447
|
const costUsd = costOf(event.model, event.usage);
|
|
@@ -445,30 +601,48 @@ export const initialView = {
|
|
|
445
601
|
fullLog: false,
|
|
446
602
|
tail: false,
|
|
447
603
|
tailRole: undefined,
|
|
448
|
-
tailBack: 0
|
|
604
|
+
tailBack: 0,
|
|
605
|
+
scroll: 0
|
|
449
606
|
};
|
|
450
607
|
const runningLanes = (state) => state.lanes.filter((lane) => lane.status === "running");
|
|
608
|
+
/**
|
|
609
|
+
* The lane the detail box is about: the selection, else the lane that moved
|
|
610
|
+
* last, else the first running one — so an unattended screen still shows
|
|
611
|
+
* what is happening (ADR 0033).
|
|
612
|
+
*/
|
|
613
|
+
export const selectedLane = (state, view) => {
|
|
614
|
+
const running = runningLanes(state);
|
|
615
|
+
return (running.find((lane) => lane.id === view.selected) ??
|
|
616
|
+
running.find((lane) => lane.id === state.lastChanged) ??
|
|
617
|
+
running[0]);
|
|
618
|
+
};
|
|
451
619
|
const tailRoles = [undefined, "coder", "reviewer", "judge"];
|
|
452
620
|
const tailPage = 10;
|
|
453
621
|
/** Whose transcript the tail shows: the selected story, or the selected executor. */
|
|
454
622
|
export const tailTargetOf = (view) => view.selected === undefined
|
|
455
623
|
? undefined
|
|
456
|
-
: view.mode === "
|
|
457
|
-
? {
|
|
458
|
-
: {
|
|
624
|
+
: view.mode === "executors"
|
|
625
|
+
? { executor: view.selected }
|
|
626
|
+
: { lane: view.selected };
|
|
459
627
|
/**
|
|
460
628
|
* The view after a key: arrows or 1–9 select a running lane (an executor in
|
|
461
|
-
* the executors view), enter expands
|
|
462
|
-
* cycles the tail's role, escape closes, `e` switches lanes and
|
|
463
|
-
* `l` the full log, `q` quits. Keys it does not
|
|
629
|
+
* the executors view), enter expands its detail, `t` tails its transcript,
|
|
630
|
+
* `r` cycles the tail's role, escape closes, `e` switches lanes and
|
|
631
|
+
* executors, `b` the boards, `l` the full log, `q` quits. Keys it does not
|
|
632
|
+
* know return the same view.
|
|
464
633
|
*/
|
|
465
634
|
export const onTreeKey = (view, key, state) => {
|
|
466
|
-
const choices = view.mode === "
|
|
467
|
-
const
|
|
635
|
+
const choices = view.mode === "executors" ? state.executors : runningLanes(state).map((lane) => lane.id);
|
|
636
|
+
const current = view.mode === "executors" ? view.selected : selectedLane(state, view)?.id;
|
|
637
|
+
const index = choices.findIndex((id) => id === current);
|
|
468
638
|
const select = (next) => {
|
|
469
|
-
const
|
|
470
|
-
|
|
639
|
+
const row = Math.max(0, Math.min(choices.length - 1, next));
|
|
640
|
+
const id = choices[row];
|
|
641
|
+
return id === undefined
|
|
642
|
+
? view
|
|
643
|
+
: { ...view, selected: id, scroll: row < view.scroll ? row : view.scroll };
|
|
471
644
|
};
|
|
645
|
+
const chosen = () => view.mode === "executors" ? view.selected : selectedLane(state, view)?.id;
|
|
472
646
|
switch (key) {
|
|
473
647
|
case "q":
|
|
474
648
|
return "quit";
|
|
@@ -479,13 +653,15 @@ export const onTreeKey = (view, key, state) => {
|
|
|
479
653
|
case "left":
|
|
480
654
|
return select(index < 0 ? 0 : index - 1);
|
|
481
655
|
case "enter":
|
|
482
|
-
return view.
|
|
656
|
+
return view.mode === "executors" || chosen() === undefined
|
|
483
657
|
? view
|
|
484
658
|
: { ...view, expanded: !view.expanded };
|
|
485
|
-
case "t":
|
|
486
|
-
|
|
659
|
+
case "t": {
|
|
660
|
+
const id = chosen();
|
|
661
|
+
return id === undefined
|
|
487
662
|
? view
|
|
488
|
-
: { ...view, tail: !view.tail, expanded: false, tailBack: 0 };
|
|
663
|
+
: { ...view, selected: id, tail: !view.tail, expanded: false, tailBack: 0 };
|
|
664
|
+
}
|
|
489
665
|
case "pageup":
|
|
490
666
|
return view.tail ? { ...view, tailBack: view.tailBack + tailPage } : view;
|
|
491
667
|
case "pagedown":
|
|
@@ -502,9 +678,17 @@ export const onTreeKey = (view, key, state) => {
|
|
|
502
678
|
case "e":
|
|
503
679
|
return {
|
|
504
680
|
...view,
|
|
505
|
-
mode: view.mode === "
|
|
681
|
+
mode: view.mode === "executors" ? "lanes" : "executors",
|
|
506
682
|
selected: undefined,
|
|
507
683
|
expanded: false,
|
|
684
|
+
tail: false,
|
|
685
|
+
scroll: 0
|
|
686
|
+
};
|
|
687
|
+
case "b":
|
|
688
|
+
return {
|
|
689
|
+
...view,
|
|
690
|
+
mode: view.mode === "boards" ? "lanes" : "boards",
|
|
691
|
+
expanded: false,
|
|
508
692
|
tail: false
|
|
509
693
|
};
|
|
510
694
|
case "l":
|
|
@@ -514,7 +698,8 @@ export const onTreeKey = (view, key, state) => {
|
|
|
514
698
|
}
|
|
515
699
|
};
|
|
516
700
|
const railWidth = 26;
|
|
517
|
-
|
|
701
|
+
/** The agent list never shrinks below this many rows for the log's sake. */
|
|
702
|
+
const minListRows = 3;
|
|
518
703
|
const counter = (label, value) => [
|
|
519
704
|
span(`${label.padEnd(15)}${String(value).padStart(5)}`)
|
|
520
705
|
];
|
|
@@ -530,26 +715,8 @@ const railOf = (state) => box(railWidth, "judge", [
|
|
|
530
715
|
],
|
|
531
716
|
[],
|
|
532
717
|
counter("reviews", state.judge.reviews),
|
|
533
|
-
counter("verdicts", state.judge.verdicts)
|
|
534
|
-
])
|
|
535
|
-
const laneBox = (lane, width, now, selected, idleAfterMs) => box(width, "running", [
|
|
536
|
-
[span(selected ? `▸ ${lane.id}` : lane.id, "bold")],
|
|
537
|
-
[span(lane.executor ?? "(leasing)")],
|
|
538
|
-
[span(lane.stages.at(-1) ?? "starting")],
|
|
539
|
-
doingLine(lane, now),
|
|
540
|
-
[
|
|
541
|
-
span(`${elapsed(lane.startedAt, now)} · ${formatCount(lane.tokens)} tok${lane.costUsd > 0 ? ` · ${dollars(lane.costUsd)}` : ""}`)
|
|
542
|
-
],
|
|
543
|
-
...(lane.turns.length === 0 && lane.gatesMs === 0
|
|
544
|
-
? []
|
|
545
|
-
: [
|
|
546
|
-
[
|
|
547
|
-
span(`turns ${lane.turns.length} · avg ${elapsedMs(lane.turns.length === 0
|
|
548
|
-
? 0
|
|
549
|
-
: lane.turns.reduce((total, turn) => total + turn, 0) / lane.turns.length)} · gates ${elapsedMs(lane.gatesMs)}`)
|
|
550
|
-
]
|
|
551
|
-
]),
|
|
552
|
-
statusLine(lane, now, idleAfterMs)
|
|
718
|
+
counter("verdicts", state.judge.verdicts),
|
|
719
|
+
...(state.judge.borrowed === 0 ? [] : [counter("borrowed", state.judge.borrowed)])
|
|
553
720
|
]);
|
|
554
721
|
/** Work under way as a card names it: the command itself, the call, the wait. */
|
|
555
722
|
export const workText = (work) => {
|
|
@@ -678,13 +845,244 @@ const columnsOf = (items, width, draw) => {
|
|
|
678
845
|
const each = Math.floor((width - 2 * (items.length - 1)) / items.length);
|
|
679
846
|
return beside(items.map((item) => draw(item, each)), items.map(() => each));
|
|
680
847
|
};
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
[
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
848
|
+
/** `◐`/`⏸`: the lane's status at a glance, as `statusLine` grades it. */
|
|
849
|
+
const statusMark = (lane, now, idleAfterMs) => {
|
|
850
|
+
const line = statusLine(lane, now, idleAfterMs);
|
|
851
|
+
const text = line[0]?.text ?? "";
|
|
852
|
+
return span(text.startsWith("◐") ? "◐" : "⏸", line[0]?.style);
|
|
853
|
+
};
|
|
854
|
+
/** `3/5 wire session cookie`, or `2/5 tasks` between tasks, or nothing before the plan. */
|
|
855
|
+
const taskText = (lane) => lane.task !== undefined
|
|
856
|
+
? `${lane.task.index}/${lane.task.count} ${lane.task.title}`
|
|
857
|
+
: lane.tasks.length > 0
|
|
858
|
+
? `${lane.tasks.filter((task) => task.done).length}/${lane.tasks.length} tasks`
|
|
859
|
+
: "";
|
|
860
|
+
/** `head` then `tail` in `width`: a long head is cut, the tail (a timer, a count) never is. */
|
|
861
|
+
const headThenTail = (head, tail, width) => {
|
|
862
|
+
const room = width - [...tail.text].length;
|
|
863
|
+
return lengthOf(head) <= room ? [...head, tail] : [...fit(head, Math.max(1, room)), tail];
|
|
864
|
+
};
|
|
865
|
+
/** The one line a running lane is in the agent list (ADR 0033). */
|
|
866
|
+
const laneLine = (lane, width, now, selected, idleAfterMs) => {
|
|
867
|
+
const task = taskText(lane);
|
|
868
|
+
const stage = lane.stages.at(-1) ?? "starting";
|
|
869
|
+
const head = [
|
|
870
|
+
span(selected ? "▸ " : " "),
|
|
871
|
+
statusMark(lane, now, idleAfterMs),
|
|
872
|
+
span(" "),
|
|
873
|
+
span(lane.id, "bold"),
|
|
874
|
+
span(` ${cloneName(lane.executor ?? "(leasing)", lane.clone)}`),
|
|
875
|
+
...(task.length === 0 ? [] : [span(" · "), span(task)]),
|
|
876
|
+
// A task is a stage of its own name: say it once.
|
|
877
|
+
...(stage === lane.task?.title ? [] : [span(" · "), span(stage, "dim")]),
|
|
878
|
+
...(lane.pause === undefined
|
|
879
|
+
? []
|
|
880
|
+
: [span(" · "), span(`⏸ ${lane.pause.label} ${elapsed(lane.pause.since, now)}`, "judge")])
|
|
881
|
+
];
|
|
882
|
+
return headThenTail(head, span(` · ${elapsed(lane.startedAt, now)} · ${formatCount(lane.tokens)} tok`), width);
|
|
883
|
+
};
|
|
884
|
+
/** A sub-agent the lane's harness spawned, indented under it. */
|
|
885
|
+
/** A task running beside the lane's coder, indented under it (ADR 0034). */
|
|
886
|
+
const taskCoderLine = (coder, width, now) => headThenTail([
|
|
887
|
+
span(" └ "),
|
|
888
|
+
span("task coder ", "dim"),
|
|
889
|
+
span(`${cloneName(coder.executor ?? "(leasing)", coder.clone)} · `),
|
|
890
|
+
span(`${coder.index}/${coder.count} ${coder.title}`)
|
|
891
|
+
], span(` · ${elapsed(coder.since, now)}`, "dim"), width);
|
|
892
|
+
const childLine = (child, width, now) => headThenTail([
|
|
893
|
+
span(" └ "),
|
|
894
|
+
span("sub-agent ", "dim"),
|
|
895
|
+
span(`${child.tool} ${child.args}`),
|
|
896
|
+
span(` · ${child.lastTool ?? "starting"}`, "dim")
|
|
897
|
+
], span(` · ${elapsed(child.since, now)}`, "dim"), width);
|
|
898
|
+
/** The list's rows: a running lane, then each sub-agent still running under it. */
|
|
899
|
+
const listRowsOf = (state) => runningLanes(state).reduce((total, lane) => total + 1 + lane.taskCoders.length + lane.children.filter((child) => !child.ended).length, 0);
|
|
900
|
+
/**
|
|
901
|
+
* Every running agent, one row each, scrolled so the selected lane is in
|
|
902
|
+
* view and cut to `rows` with a count of what is out of view (ADR 0033).
|
|
903
|
+
*/
|
|
904
|
+
const agentRows = (state, width, view, selected, rows) => {
|
|
905
|
+
const all = runningLanes(state).flatMap((lane) => [
|
|
906
|
+
{
|
|
907
|
+
lane: lane.id,
|
|
908
|
+
line: laneLine(lane, width, state.now, lane.id === selected?.id, state.idleAfterMs)
|
|
909
|
+
},
|
|
910
|
+
...lane.taskCoders.map((coder) => ({
|
|
911
|
+
lane: lane.id,
|
|
912
|
+
line: taskCoderLine(coder, width, state.now)
|
|
913
|
+
})),
|
|
914
|
+
...lane.children
|
|
915
|
+
.filter((child) => !child.ended)
|
|
916
|
+
.map((child) => ({ lane: lane.id, line: childLine(child, width, state.now) }))
|
|
917
|
+
]);
|
|
918
|
+
if (rows === undefined || all.length <= rows) {
|
|
919
|
+
return all.map((row) => row.line);
|
|
920
|
+
}
|
|
921
|
+
const wanted = all.findIndex((row) => row.lane === selected?.id);
|
|
922
|
+
// The "… n above/more" notes take rows of the budget too.
|
|
923
|
+
const window = (room) => {
|
|
924
|
+
const start = Math.max(0, Math.min(view.scroll, all.length - room));
|
|
925
|
+
return wanted < 0
|
|
926
|
+
? start
|
|
927
|
+
: wanted < start
|
|
928
|
+
? wanted
|
|
929
|
+
: wanted >= start + room
|
|
930
|
+
? wanted - room + 1
|
|
931
|
+
: start;
|
|
932
|
+
};
|
|
933
|
+
let room = Math.max(1, rows - 1);
|
|
934
|
+
let first = window(room);
|
|
935
|
+
if (first > 0 && first + room < all.length) {
|
|
936
|
+
room = Math.max(1, rows - 2);
|
|
937
|
+
first = window(room);
|
|
938
|
+
}
|
|
939
|
+
const shown = all.slice(first, first + room);
|
|
940
|
+
const above = first;
|
|
941
|
+
const below = all.length - first - shown.length;
|
|
942
|
+
return [
|
|
943
|
+
...(above > 0 ? [[span(`… ${above} above`, "dim")]] : []),
|
|
944
|
+
...shown.map((row) => row.line),
|
|
945
|
+
...(below > 0 ? [[span(`… ${below} more`, "dim")]] : [])
|
|
946
|
+
];
|
|
947
|
+
};
|
|
948
|
+
const tick = (task, running) => task.done ? "[x]" : running ? "[▶]" : "[ ]";
|
|
949
|
+
/**
|
|
950
|
+
* The selected lane in full: its task checklist with the criteria each task
|
|
951
|
+
* satisfies, its stage, what it is doing, its tools, a harness pause, and
|
|
952
|
+
* its figures (ADR 0033). Expanded, every stage and tool it ran.
|
|
953
|
+
*/
|
|
954
|
+
const detailBox = (lane, width, now, idleAfterMs, expanded) => {
|
|
955
|
+
const inner = width - 4;
|
|
956
|
+
const checklist = lane.tasks.length === 0
|
|
957
|
+
? [[span("no task plan yet", "dim")]]
|
|
958
|
+
: lane.tasks.map((task) => {
|
|
959
|
+
const beside = lane.taskCoders.find((coder) => coder.index === task.index);
|
|
960
|
+
const running = lane.task?.index === task.index || beside !== undefined;
|
|
961
|
+
const right = task.satisfies === undefined ? "" : `satisfies ${task.satisfies.join(",")}`;
|
|
962
|
+
const left = [
|
|
963
|
+
span(`${tick(task, running)} ${task.index} ${task.title}`, running ? "bold" : undefined),
|
|
964
|
+
...(beside === undefined
|
|
965
|
+
? []
|
|
966
|
+
: [span(` ← ${cloneName(beside.executor ?? "(leasing)", beside.clone)}`, "dim")])
|
|
967
|
+
];
|
|
968
|
+
return right.length === 0
|
|
969
|
+
? left
|
|
970
|
+
: [...fit(left, Math.max(1, inner - [...right].length - 1)), span(` ${right}`, "dim")];
|
|
971
|
+
});
|
|
972
|
+
const stages = expanded
|
|
973
|
+
? lane.stages.map((stage) => [span(stage)])
|
|
974
|
+
: [[span("stage "), span(lane.stages.at(-1) ?? "starting")]];
|
|
975
|
+
const tools = expanded
|
|
976
|
+
? [
|
|
977
|
+
[span(`tools (last ${lane.tools.length})`, "dim")],
|
|
978
|
+
...lane.tools.map((tool) => [span(` ${tool}`, "dim")])
|
|
979
|
+
]
|
|
980
|
+
: lane.tools.length === 0
|
|
981
|
+
? []
|
|
982
|
+
: [[span("tools "), span(lane.tools.slice(-3).join(" · "), "dim")]];
|
|
983
|
+
const title = `${lane.id} · ${cloneName(lane.executor ?? "(leasing)", lane.clone)}${lane.task === undefined ? "" : ` · task ${lane.task.index}/${lane.task.count}`}`;
|
|
984
|
+
return box(width, "running", [
|
|
985
|
+
...checklist,
|
|
986
|
+
...stages,
|
|
987
|
+
doingLine(lane, now),
|
|
988
|
+
...(lane.pause === undefined
|
|
989
|
+
? []
|
|
990
|
+
: [[span(`⏸ ${lane.pause.label} ${elapsed(lane.pause.since, now)}`, "judge")]]),
|
|
991
|
+
...tools,
|
|
992
|
+
[
|
|
993
|
+
span(`${elapsed(lane.startedAt, now)} · ${formatCount(lane.tokens)} tok${lane.costUsd > 0 ? ` · ${dollars(lane.costUsd)}` : ""}`)
|
|
994
|
+
],
|
|
995
|
+
...(lane.turns.length === 0 && lane.gatesMs === 0
|
|
996
|
+
? []
|
|
997
|
+
: [
|
|
998
|
+
[
|
|
999
|
+
span(`turns ${lane.turns.length} · avg ${elapsedMs(lane.turns.length === 0
|
|
1000
|
+
? 0
|
|
1001
|
+
: lane.turns.reduce((total, turn) => total + turn, 0) / lane.turns.length)} · gates ${elapsedMs(lane.gatesMs)}`)
|
|
1002
|
+
]
|
|
1003
|
+
]),
|
|
1004
|
+
statusLine(lane, now, idleAfterMs)
|
|
1005
|
+
], title);
|
|
1006
|
+
};
|
|
1007
|
+
const bar = (done, total, width = 5) => {
|
|
1008
|
+
const filled = total === 0 ? 0 : Math.round((done / total) * width);
|
|
1009
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1010
|
+
};
|
|
1011
|
+
const boardColumns = [
|
|
1012
|
+
["planned", "planned"],
|
|
1013
|
+
["active", "running"],
|
|
1014
|
+
["waiting", "waiting"],
|
|
1015
|
+
["done", "done"],
|
|
1016
|
+
["failed", "failed"]
|
|
1017
|
+
];
|
|
1018
|
+
/**
|
|
1019
|
+
* The boards (ADR 0033): the epic's stories by column, with a running lane's
|
|
1020
|
+
* clone and progress on its card, then the selected lane's tasks by column.
|
|
1021
|
+
*/
|
|
1022
|
+
const boardsOf = (state, width, selected, now) => {
|
|
1023
|
+
const chips = chipsOf(state);
|
|
1024
|
+
// Five columns need ~16 characters each to read; narrower, two rows of columns.
|
|
1025
|
+
const epic = chunked(boardColumns, width >= 5 * 16 + 8 ? 5 : 3).flatMap((group) => columnsOf(group, width, ([name, status], each) => {
|
|
1026
|
+
const members = chips.filter((chip) => chip.status === status);
|
|
1027
|
+
const cards = members.flatMap((chip) => {
|
|
1028
|
+
const lane = state.lanes.find((candidate) => candidate.id === chip.id);
|
|
1029
|
+
if (lane === undefined || lane.status !== "running") {
|
|
1030
|
+
return [[span(chip.id)]];
|
|
1031
|
+
}
|
|
1032
|
+
const done = lane.tasks.filter((task) => task.done).length;
|
|
1033
|
+
const progress = lane.tasks.length === 0
|
|
1034
|
+
? ""
|
|
1035
|
+
: ` ${done}/${lane.tasks.length}${each >= 24 ? ` ${bar(done, lane.tasks.length)}` : ""}`;
|
|
1036
|
+
return [
|
|
1037
|
+
[span(chip.id, "bold")],
|
|
1038
|
+
[span(` ${cloneName(lane.executor ?? "(leasing)", lane.clone)}${progress}`, "dim")]
|
|
1039
|
+
];
|
|
1040
|
+
});
|
|
1041
|
+
return box(each, "running", cards.length === 0 ? [[span("—", "dim")]] : cards, `${name} (${members.length})`);
|
|
1042
|
+
}));
|
|
1043
|
+
if (selected === undefined) {
|
|
1044
|
+
return [[span("epic board", "bold")], ...epic];
|
|
1045
|
+
}
|
|
1046
|
+
const taskColumns = [
|
|
1047
|
+
[
|
|
1048
|
+
"todo",
|
|
1049
|
+
(task) => !task.done &&
|
|
1050
|
+
task.index !== selected.task?.index &&
|
|
1051
|
+
!selected.taskCoders.some((coder) => coder.index === task.index)
|
|
1052
|
+
],
|
|
1053
|
+
[
|
|
1054
|
+
"doing",
|
|
1055
|
+
(task) => !task.done &&
|
|
1056
|
+
(task.index === selected.task?.index ||
|
|
1057
|
+
selected.taskCoders.some((coder) => coder.index === task.index))
|
|
1058
|
+
],
|
|
1059
|
+
["review", () => false],
|
|
1060
|
+
["done", (task) => task.done]
|
|
1061
|
+
];
|
|
1062
|
+
const story = chunked(taskColumns, width >= 4 * 16 + 6 ? 4 : 2).flatMap((group) => columnsOf(group, width, ([name, keep], each) => {
|
|
1063
|
+
const members = selected.tasks.filter(keep);
|
|
1064
|
+
return box(each, "running", members.length === 0
|
|
1065
|
+
? [[span("—", "dim")]]
|
|
1066
|
+
: members.map((task) => [span(`${task.index} ${task.title}`)]), `${name} (${members.length})`);
|
|
1067
|
+
}));
|
|
1068
|
+
return [
|
|
1069
|
+
[span("epic board", "bold")],
|
|
1070
|
+
...epic,
|
|
1071
|
+
[],
|
|
1072
|
+
[
|
|
1073
|
+
span(`story board · ${selected.id} · ${cloneName(selected.executor ?? "(leasing)", selected.clone)}`, "bold"),
|
|
1074
|
+
span(` · ${elapsed(selected.startedAt, now)}`)
|
|
1075
|
+
],
|
|
1076
|
+
...story
|
|
1077
|
+
];
|
|
1078
|
+
};
|
|
1079
|
+
/** `codex ×2 · claude ×1`: how many leases each executor holds now. */
|
|
1080
|
+
const leaseCounts = (state) => state.executors
|
|
1081
|
+
.map((executor) => [executor, state.leases.filter((lease) => lease.executor === executor).length])
|
|
1082
|
+
.filter(([, count]) => count > 0)
|
|
1083
|
+
.map(([executor, count]) => `${executor} ×${count}`)
|
|
1084
|
+
.join(" · ");
|
|
1085
|
+
const chunked = (items, size) => Array.from({ length: Math.ceil(items.length / size) }, (_, index) => items.slice(index * size, index * size + size));
|
|
688
1086
|
const barWidth = 10;
|
|
689
1087
|
const judgmentRows = 3;
|
|
690
1088
|
const meter = (fraction) => {
|
|
@@ -728,8 +1126,8 @@ const tailOf = (view, tail, width) => box(width, "running", tail === undefined |
|
|
|
728
1126
|
]
|
|
729
1127
|
]
|
|
730
1128
|
: tail.map((line) => [span(line)]), `tail · ${view.selected ?? ""} · ${view.tailRole ?? "all"}`);
|
|
731
|
-
const mainOf = (state, width, view, tail) => {
|
|
732
|
-
const running = state
|
|
1129
|
+
const mainOf = (state, width, view, tail, listRows) => {
|
|
1130
|
+
const running = runningLanes(state);
|
|
733
1131
|
const orchestratorWidth = Math.min(46, width);
|
|
734
1132
|
const stage = state.stages.at(-1) ?? (running.length > 0 ? "implement stories" : "idle");
|
|
735
1133
|
// The run's own work under way (its gates, outside any story), innermost.
|
|
@@ -750,27 +1148,29 @@ const mainOf = (state, width, view, tail) => {
|
|
|
750
1148
|
]),
|
|
751
1149
|
[span(`stories ${chipsOf(state).length} elapsed ${elapsed(state.startedAt, state.now)}`)]
|
|
752
1150
|
], state.title);
|
|
753
|
-
const selected =
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
const
|
|
1151
|
+
const selected = selectedLane(state, view);
|
|
1152
|
+
const counts = leaseCounts(state);
|
|
1153
|
+
const agents = centre([span(`agents · ${running.length} running${counts.length === 0 ? "" : ` · ${counts}`}`)], width);
|
|
1154
|
+
const body = view.tail && view.selected !== undefined
|
|
757
1155
|
? tailOf(view, tail, width)
|
|
758
|
-
: view.
|
|
759
|
-
?
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
:
|
|
1156
|
+
: view.mode === "executors"
|
|
1157
|
+
? state.executors.length === 0
|
|
1158
|
+
? [centre([span("no executors yet", "dim")], width)]
|
|
1159
|
+
: chunked(state.executors, 4).flatMap((group) => columnsOf(group, width, (executor, each) => executorBox(state, executor, each, executor === view.selected)))
|
|
1160
|
+
: view.mode === "boards"
|
|
1161
|
+
? boardsOf(state, width, selected, state.now)
|
|
1162
|
+
: running.length === 0 || selected === undefined
|
|
765
1163
|
? [centre([span("no stories in flight", "dim")], width)]
|
|
766
|
-
:
|
|
1164
|
+
: [
|
|
1165
|
+
...agentRows(state, width, view, selected, listRows),
|
|
1166
|
+
...detailBox(selected, width, state.now, state.idleAfterMs, view.expanded)
|
|
1167
|
+
];
|
|
767
1168
|
return [
|
|
768
1169
|
...orchestrator.map((line) => centre(line, width)),
|
|
769
1170
|
centre([span("•", "orchestrator")], width),
|
|
770
1171
|
...judgmentOf(state, width),
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
...lanes,
|
|
1172
|
+
agents,
|
|
1173
|
+
...body,
|
|
774
1174
|
[],
|
|
775
1175
|
...wrapChips(chipsOf(state).map(chipOf), width)
|
|
776
1176
|
];
|
|
@@ -795,7 +1195,7 @@ const timeSplitOf = (time) => {
|
|
|
795
1195
|
];
|
|
796
1196
|
};
|
|
797
1197
|
/** A frame with the session log at `logCount` lines, or without it. */
|
|
798
|
-
const frameOf = (state, options, logCount) => {
|
|
1198
|
+
const frameOf = (state, options, logCount, listRows) => {
|
|
799
1199
|
const { width } = options;
|
|
800
1200
|
const mainWidth = width - railWidth - 2;
|
|
801
1201
|
const chips = chipsOf(state);
|
|
@@ -805,7 +1205,7 @@ const frameOf = (state, options, logCount) => {
|
|
|
805
1205
|
centre([span("LLM4TS AGENT TREE", "bold"), span(" · "), span(state.title, "orchestrator")], width),
|
|
806
1206
|
[span("═".repeat(width), "dim")],
|
|
807
1207
|
[],
|
|
808
|
-
...beside([railOf(state), mainOf(state, mainWidth, options.view, options.tail)], [railWidth, mainWidth]),
|
|
1208
|
+
...beside([railOf(state), mainOf(state, mainWidth, options.view, options.tail, listRows)], [railWidth, mainWidth]),
|
|
809
1209
|
...(logCount === "none"
|
|
810
1210
|
? []
|
|
811
1211
|
: [[], ...box(width, "log", logOf(state, logCount), "session log")]),
|
|
@@ -822,24 +1222,37 @@ const frameOf = (state, options, logCount) => {
|
|
|
822
1222
|
return lines.map((line) => paint(fit(line, width), options.colour));
|
|
823
1223
|
};
|
|
824
1224
|
/**
|
|
825
|
-
* The tree at a width and, when `height` is given, no taller: the
|
|
826
|
-
*
|
|
827
|
-
*
|
|
1225
|
+
* The tree at a width and, when `height` is given, no taller: the agent
|
|
1226
|
+
* list gives way first (down to `minListRows`, with a count of what is out
|
|
1227
|
+
* of view), then the session log — fewer lines, down to one, then none —
|
|
1228
|
+
* and only then is the frame cut, keeping its status lines, so a redraw
|
|
1229
|
+
* never scrolls.
|
|
828
1230
|
*/
|
|
829
1231
|
export const renderTree = (state, options) => {
|
|
830
1232
|
const wanted = options.view.fullLog ? fullLogLines : logLines;
|
|
831
|
-
const full = frameOf(state, options, wanted);
|
|
1233
|
+
const full = frameOf(state, options, wanted, undefined);
|
|
832
1234
|
const { height } = options;
|
|
833
1235
|
if (height === undefined || full.length <= height) {
|
|
834
1236
|
return full;
|
|
835
1237
|
}
|
|
1238
|
+
const total = listRowsOf(state);
|
|
1239
|
+
const floor = Math.min(total, minListRows);
|
|
1240
|
+
const listRows = options.view.mode === "lanes" && total > floor
|
|
1241
|
+
? Math.max(floor, total - (full.length - height))
|
|
1242
|
+
: undefined;
|
|
1243
|
+
if (listRows !== undefined) {
|
|
1244
|
+
const shorter = frameOf(state, options, wanted, listRows);
|
|
1245
|
+
if (shorter.length <= height) {
|
|
1246
|
+
return shorter;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
836
1249
|
for (let count = wanted - 1; count >= 1; count -= 1) {
|
|
837
|
-
const shorter = frameOf(state, options, count);
|
|
1250
|
+
const shorter = frameOf(state, options, count, listRows);
|
|
838
1251
|
if (shorter.length <= height) {
|
|
839
1252
|
return shorter;
|
|
840
1253
|
}
|
|
841
1254
|
}
|
|
842
|
-
return fitToRows(frameOf(state, options, "none"), height);
|
|
1255
|
+
return fitToRows(frameOf(state, options, "none", listRows), height);
|
|
843
1256
|
};
|
|
844
1257
|
/**
|
|
845
1258
|
* A frame cut to a terminal's height: the top of the tree and its two status
|