jeo-code 0.8.43 → 0.9.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/CHANGELOG.md +13 -19
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/ast-edit-tool.ts +2 -2
- package/src/agent/engine.ts +7 -44
- package/src/agent/job-registry.ts +34 -189
- package/src/agent/job-tool.ts +2 -2
- package/src/agent/lsp-rename-tool.ts +2 -2
- package/src/agent/monitor-registry.ts +233 -0
- package/src/agent/monitor-tool.ts +80 -31
- package/src/agent/subagent-registry.ts +46 -5
- package/src/agent/subagent-tool.ts +3 -3
- package/src/agent/subagents.ts +2 -2
- package/src/agent/task-tool.ts +19 -46
- package/src/agent/tool-schemas.ts +7 -6
- package/src/agent/tools.ts +4 -36
- package/src/commands/launch/stream.ts +1 -16
- package/src/commands/launch.ts +49 -55
- package/src/tui/app.ts +47 -59
package/src/commands/launch.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { parseInThreadConfigCommand } from "../agent/notify/config-commands";
|
|
|
19
19
|
|
|
20
20
|
import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
|
|
21
21
|
import { createJobTool, JOB_TOOL_PROTOCOL_LINE } from "../agent/job-tool";
|
|
22
|
-
import { JobRegistry, type MonitorJobEvent } from "../agent/job-registry";
|
|
23
22
|
import { createMonitorTool, MONITOR_TOOL_PROTOCOL_LINE } from "../agent/monitor-tool";
|
|
23
|
+
import { MonitorRegistry } from "../agent/monitor-registry";
|
|
24
|
+
import { JobRegistry } from "../agent/job-registry";
|
|
24
25
|
import { createIrcTool, IRC_TOOL_PROTOCOL_LINE } from "../agent/irc-tool";
|
|
25
26
|
import { createGoalTool, GOAL_TOOL_PROTOCOL_LINE } from "../agent/goal-tool";
|
|
26
27
|
import { createApproveTool, APPROVE_TOOL_PROTOCOL_LINE } from "../agent/approve-tool";
|
|
@@ -186,7 +187,6 @@ import {
|
|
|
186
187
|
gatedStdout,
|
|
187
188
|
formatTaskSubEvent,
|
|
188
189
|
logTaskSubEvent,
|
|
189
|
-
formatMonitorJobEvent,
|
|
190
190
|
createStreamEvents,
|
|
191
191
|
shouldUseOneShotTui,
|
|
192
192
|
} from "./launch/stream";
|
|
@@ -910,6 +910,33 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
910
910
|
sessionNotifyEndpoint.sendIdentity({ repo: cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd, branch, cwd });
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
+
// Session-scoped registries for DETACHED subagents, background jobs, and monitors (gjc parity).
|
|
914
|
+
// Created ONCE for the whole launch session lifetime (not per-turn) so detached
|
|
915
|
+
// runs persist across turns and remain controllable/inspectable.
|
|
916
|
+
const subagentRegistry = new SubagentRegistry();
|
|
917
|
+
const jobRegistry = new JobRegistry();
|
|
918
|
+
let activeTui: LaunchTui | null = null;
|
|
919
|
+
const monitorRegistry = new MonitorRegistry({
|
|
920
|
+
onLine: (record, line) => {
|
|
921
|
+
const notice = `[monitor ${record.id}] ${line}`;
|
|
922
|
+
if (activeTui) activeTui.events().onNotice?.(notice);
|
|
923
|
+
else console.log(notice);
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
sessionNotifyEndpoint?.attachRegistry(subagentRegistry);
|
|
927
|
+
|
|
928
|
+
let cleanedUp = false;
|
|
929
|
+
const cleanupSession = async (): Promise<void> => {
|
|
930
|
+
if (cleanedUp) return;
|
|
931
|
+
cleanedUp = true;
|
|
932
|
+
subagentRegistry.cancelAll();
|
|
933
|
+
jobRegistry.cancelAll();
|
|
934
|
+
monitorRegistry.cancelAll();
|
|
935
|
+
sessionNotifyEndpoint?.detachRegistry();
|
|
936
|
+
await sessionNotifyEndpoint?.stop();
|
|
937
|
+
await stopSessionNotifyEndpoint(subagentRegistry);
|
|
938
|
+
};
|
|
939
|
+
|
|
913
940
|
// Persist the active per-session model into the session header so `/resume` restores
|
|
914
941
|
// it (each session can carry its own model independent of the global default).
|
|
915
942
|
// Best-effort: a header-rewrite failure must never abort the turn.
|
|
@@ -1215,6 +1242,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1215
1242
|
const { provider: activeProvider } = await describeModel(activeModel, turnConfig);
|
|
1216
1243
|
const turnDirtyCount = branch ? gitDirtyCount(cwd) : undefined;
|
|
1217
1244
|
const tui = useTui ? new LaunchTui({ model: activeModel, provider: activeProvider, sessionId, maxSteps: initialStepLimit, cwd, branch, dirtyCount: turnDirtyCount, thinking: activeThinking, routedTier: routed?.tier }) : null;
|
|
1245
|
+
activeTui = tui;
|
|
1218
1246
|
if (routeCredentialNotice) {
|
|
1219
1247
|
if (tui) tui.events().onNotice?.(routeCredentialNotice);
|
|
1220
1248
|
else console.log(routeCredentialNotice);
|
|
@@ -1272,7 +1300,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1272
1300
|
},
|
|
1273
1301
|
onHardExit: () => {
|
|
1274
1302
|
if (tui) tui.finish("Cancelled.", { ok: false });
|
|
1275
|
-
|
|
1303
|
+
forceExitFromCtrlC();
|
|
1276
1304
|
},
|
|
1277
1305
|
});
|
|
1278
1306
|
let compRes: Awaited<ReturnType<typeof maybeCompact>>;
|
|
@@ -1418,40 +1446,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1418
1446
|
},
|
|
1419
1447
|
onHardExit: () => {
|
|
1420
1448
|
if (tui) tui.finish("Cancelled.", { ok: false });
|
|
1421
|
-
|
|
1449
|
+
forceExitFromCtrlC();
|
|
1422
1450
|
},
|
|
1423
1451
|
});
|
|
1424
1452
|
const ac = harness.controller;
|
|
1425
|
-
// #9: per-turn registry for DETACHED subagents (task{detached:true}); the
|
|
1426
|
-
// `subagent` tool controls them and cancelAll() in finally prevents orphans.
|
|
1427
|
-
const subagentRegistry = new SubagentRegistry();
|
|
1428
|
-
// TUI callbacks are scoped to this registry. cancelAll() may trigger a detached
|
|
1429
|
-
// run's final event after the turn is complete; do not let it paint a freshly
|
|
1430
|
-
// reset later TUI. Non-TUI streams retain direct event logging.
|
|
1431
|
-
let acceptingTuiSubagentEvents = true;
|
|
1432
|
-
const onSubagentEvent = useTui
|
|
1433
|
-
? (e: TaskSubEvent) => {
|
|
1434
|
-
if (acceptingTuiSubagentEvents) tui?.onSubagentEvent(e);
|
|
1435
|
-
}
|
|
1436
|
-
: (e: TaskSubEvent) => logTaskSubEvent(e);
|
|
1437
|
-
const jobRegistry = new JobRegistry();
|
|
1438
|
-
// Monitor output stays bounded and turn-scoped. It deliberately feeds the
|
|
1439
|
-
// engine's observational inbox rather than the steering inbox.
|
|
1440
|
-
const monitorInbox: string[] = [];
|
|
1441
|
-
const MONITOR_INBOX_CAP = 32;
|
|
1442
|
-
let acceptingMonitorEvents = true;
|
|
1443
|
-
const onMonitorEvent = (e: MonitorJobEvent) => {
|
|
1444
|
-
if (!acceptingMonitorEvents) return;
|
|
1445
|
-
if (useTui) tui?.onMonitorEvent(e);
|
|
1446
|
-
else console.log(formatMonitorJobEvent(e));
|
|
1447
|
-
if (e.type !== "line" || monitorInbox.length >= MONITOR_INBOX_CAP) return;
|
|
1448
|
-
const description = e.record.description.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1449
|
-
const line = e.line.slice(0, 2_000);
|
|
1450
|
-
monitorInbox.push(`[monitor ${e.record.id} · ${e.record.category} · ${description}]\n${line}`);
|
|
1451
|
-
};
|
|
1452
|
-
const unsubscribeMonitor = jobRegistry.subscribeMonitor(onMonitorEvent);
|
|
1453
|
-
const drainMonitor = () => monitorInbox.splice(0, monitorInbox.length);
|
|
1454
|
-
sessionNotifyEndpoint?.attachRegistry(subagentRegistry);
|
|
1455
1453
|
currentTurnSteer = { push: line => steerInbox.push(line), flushCard: line => tui?.flushSteerCard(line) };
|
|
1456
1454
|
if (sessionNotifyEndpoint && !notifyRedact) {
|
|
1457
1455
|
const preview = userInput.length > 200 ? `${userInput.slice(0, 200)}…` : userInput;
|
|
@@ -1523,18 +1521,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1523
1521
|
steer: drainSteer,
|
|
1524
1522
|
registry: subagentRegistry,
|
|
1525
1523
|
sessionEndpoint: sessionNotifyEndpoint,
|
|
1526
|
-
onEvent:
|
|
1524
|
+
onEvent: useTui
|
|
1525
|
+
? (e => tui?.onSubagentEvent(e))
|
|
1526
|
+
: (e => logTaskSubEvent(e)),
|
|
1527
1527
|
}),
|
|
1528
1528
|
eval: createEvalTool({
|
|
1529
1529
|
config: { ...turnConfig, defaultModel: activeModel },
|
|
1530
1530
|
signal: ac.signal,
|
|
1531
1531
|
steer: drainSteer,
|
|
1532
|
-
onEvent:
|
|
1532
|
+
onEvent: useTui
|
|
1533
|
+
? (e => tui?.onSubagentEvent(e))
|
|
1534
|
+
: (e => logTaskSubEvent(e)),
|
|
1533
1535
|
}),
|
|
1534
1536
|
todo: createTodoTool({ onChange: items => { turnTodos = items; tui?.setTodos(items); } }),
|
|
1535
1537
|
subagent: createSubagentTool(subagentRegistry),
|
|
1536
1538
|
job: createJobTool(jobRegistry),
|
|
1537
|
-
monitor: createMonitorTool(
|
|
1539
|
+
monitor: createMonitorTool(monitorRegistry),
|
|
1538
1540
|
irc: createIrcTool(subagentRegistry),
|
|
1539
1541
|
goal: createGoalTool(),
|
|
1540
1542
|
approve: createApproveTool(),
|
|
@@ -1640,7 +1642,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1640
1642
|
signal: ac.signal,
|
|
1641
1643
|
sessionKey: cacheKey,
|
|
1642
1644
|
steer: drainSteer,
|
|
1643
|
-
monitor: drainMonitor,
|
|
1644
1645
|
rateLimitFallbackAvailable,
|
|
1645
1646
|
safetyFallbackAvailable,
|
|
1646
1647
|
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
|
|
@@ -1774,7 +1775,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1774
1775
|
signal: ac.signal,
|
|
1775
1776
|
sessionKey: turnSessionKey,
|
|
1776
1777
|
steer: drainSteer,
|
|
1777
|
-
monitor: drainMonitor,
|
|
1778
1778
|
// Deliberately NOT wiring rateLimitFallbackAvailable here: this repair
|
|
1779
1779
|
// retry has no reroute loop around it (unlike runLoopWithModel's
|
|
1780
1780
|
// routeFallbackAttempt loop) — bailing the retry ladder early on a 429
|
|
@@ -1799,14 +1799,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1799
1799
|
await opik.endTurn({ done: result.done, steps: result.steps, output: result.doneReason });
|
|
1800
1800
|
} finally {
|
|
1801
1801
|
harness.dispose();
|
|
1802
|
-
|
|
1803
|
-
acceptingMonitorEvents = false;
|
|
1804
|
-
subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
|
|
1805
|
-
sessionNotifyEndpoint?.detachRegistry(); // this turn's registry is gone — subagent frames stop applying to it
|
|
1802
|
+
activeTui = null;
|
|
1806
1803
|
currentTurnSteer = undefined; // a remote message after this point queues instead of steering a dead turn
|
|
1807
|
-
await stopSessionNotifyEndpoint(subagentRegistry); // tear down the FALLBACK lazy endpoint, if one was started (no-op when sessionNotifyEndpoint owns the registry)
|
|
1808
|
-
jobRegistry.cancelAll(); // background jobs are turn-scoped too — no orphaned processes
|
|
1809
|
-
unsubscribeMonitor();
|
|
1810
1804
|
|
|
1811
1805
|
// Steering typed but never drained (e.g. entered just after the final step)
|
|
1812
1806
|
// must not be lost — fold it into the next prompt draft so it runs next.
|
|
@@ -2060,7 +2054,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2060
2054
|
const harness = createInFlightAbortHarness({
|
|
2061
2055
|
captureEsc: false,
|
|
2062
2056
|
onAbortNotice: msg => console.log(msg),
|
|
2063
|
-
onHardExit: () =>
|
|
2057
|
+
onHardExit: () => forceExitFromCtrlC(),
|
|
2064
2058
|
});
|
|
2065
2059
|
const ac = harness.controller;
|
|
2066
2060
|
|
|
@@ -2273,7 +2267,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2273
2267
|
const harness = createInFlightAbortHarness({
|
|
2274
2268
|
captureEsc: false,
|
|
2275
2269
|
onAbortNotice: msg => console.log(msg),
|
|
2276
|
-
onHardExit: () =>
|
|
2270
|
+
onHardExit: () => forceExitFromCtrlC(),
|
|
2277
2271
|
});
|
|
2278
2272
|
const ac = harness.controller;
|
|
2279
2273
|
|
|
@@ -3171,8 +3165,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3171
3165
|
} catch {
|
|
3172
3166
|
// Best-effort terminal restore; process exit is the contract.
|
|
3173
3167
|
}
|
|
3174
|
-
|
|
3168
|
+
subagentRegistry.cancelAll();
|
|
3169
|
+
jobRegistry.cancelAll();
|
|
3170
|
+
monitorRegistry.cancelAll();
|
|
3171
|
+
sessionNotifyEndpoint?.detachRegistry();
|
|
3175
3172
|
void sessionNotifyEndpoint?.stop();
|
|
3173
|
+
void stopSessionNotifyEndpoint(subagentRegistry);
|
|
3176
3174
|
process.exit(130);
|
|
3177
3175
|
};
|
|
3178
3176
|
// Prompt Ctrl+C: a single press clears a non-empty box; on an empty box it exits.
|
|
@@ -4994,11 +4992,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4994
4992
|
disarmPreview();
|
|
4995
4993
|
out.write("\x1b[?25h\n");
|
|
4996
4994
|
} catch { /* best effort */ }
|
|
4997
|
-
|
|
4998
|
-
// tears down the socket on process death regardless, and a leaked discovery
|
|
4999
|
-
// file is self-healing (the daemon's scanSessions() reaps dead-pid entries
|
|
5000
|
-
// on its next 3s poll — see notify-telegram-daemon.test.ts).
|
|
5001
|
-
void sessionNotifyEndpoint?.stop();
|
|
4995
|
+
await cleanupSession();
|
|
5002
4996
|
process.removeListener("SIGINT", handleCtrlC);
|
|
5003
4997
|
process.stdin.off("data", handleCtrlCByte);
|
|
5004
4998
|
drainPromptListeners();
|
|
@@ -5015,7 +5009,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
5015
5009
|
// gjc-parity resume pointer (logs/gjc-tui-study analysis Gap C): leave the exact
|
|
5016
5010
|
// resume command in scrollback on exit, mirroring the --list handler's convention.
|
|
5017
5011
|
if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
|
|
5018
|
-
await
|
|
5012
|
+
await cleanupSession();
|
|
5019
5013
|
process.removeListener("SIGINT", handleCtrlC);
|
|
5020
5014
|
process.stdin.off("data", handleCtrlCByte);
|
|
5021
5015
|
drainPromptListeners();
|
package/src/tui/app.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { renderFooter, type FooterData } from "./components/footer";
|
|
|
20
20
|
import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat } from "./components/ascii-art";
|
|
21
21
|
import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
|
|
22
22
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
23
|
-
import type { MonitorJobEvent } from "../agent/job-registry";
|
|
24
23
|
import { supportsUnicode } from "./components/capability";
|
|
25
24
|
import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
|
|
26
25
|
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
@@ -275,15 +274,19 @@ export class LaunchTui {
|
|
|
275
274
|
// Cumulative token usage for the live turn (engine onUsage event).
|
|
276
275
|
private turnUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
277
276
|
// True while at least one delegated subagent SLOT is in flight — drives the `(sub)`
|
|
278
|
-
// status marker. Derived from `subagentLiveSlots.size` rather than a
|
|
279
|
-
// so
|
|
277
|
+
// status marker. Derived from `subagentLiveSlots.size` (see below) rather than a
|
|
278
|
+
// plain boolean so a fan-out batch where ONE worker finishes early no longer
|
|
279
|
+
// clears the marker while its siblings are still visibly running.
|
|
280
280
|
private subagentActive = false;
|
|
281
|
-
// Latest nested subagent activity, PER CONCURRENT SLOT
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
// Latest nested subagent activity, PER CONCURRENT SLOT (fan-out `task` batches — both
|
|
282
|
+
// the executor and read-only roles — now run several workers at once; a single
|
|
283
|
+
// shared string clobbered on every event made concurrent subagents look sequential
|
|
284
|
+
// and, worse, cleared the whole `(sub)` marker the instant ANY one worker finished).
|
|
285
|
+
// Keyed by stable `detachedId` when present, else fan-out `index` (1-based),
|
|
286
|
+
// else fixed key 0 for single foreground runs. `subagentLiveOrder` tracks touch order
|
|
287
|
+
// so the status row can show the MOST RECENTLY active slot plus a "+N more" count.
|
|
288
|
+
private readonly subagentLiveSlots = new Map<number | string, string>();
|
|
289
|
+
private readonly subagentLiveOrder: (number | string)[] = [];
|
|
287
290
|
// Bounded activity-history ring: one plain-text entry per ledger append, with a
|
|
288
291
|
// turn-relative timestamp. Powers Ctrl+O's "recent activity" tail so the detail
|
|
289
292
|
// view ALWAYS answers "what has been happening", even before the first reply.
|
|
@@ -1001,9 +1004,10 @@ export class LaunchTui {
|
|
|
1001
1004
|
}
|
|
1002
1005
|
// A delegated subagent's LATEST nested event beats the parent's static
|
|
1003
1006
|
// "Task: <role> …" card title — a long task otherwise reads as a stall even
|
|
1004
|
-
// though the subagent is actively reading/editing/running underneath. When
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
+
// though the subagent is actively reading/editing/running underneath. When a
|
|
1008
|
+
// fan-out batch has MULTIPLE concurrent slots live, append a "+N more running"
|
|
1009
|
+
// count so a parallel batch visibly reads as parallel instead of looking like
|
|
1010
|
+
// one subagent whose label keeps randomly changing.
|
|
1007
1011
|
if (this.subagentActive && this.subagentLiveSlots.size > 0) {
|
|
1008
1012
|
const mostRecent = this.subagentLiveOrder[this.subagentLiveOrder.length - 1];
|
|
1009
1013
|
const line = (mostRecent !== undefined ? this.subagentLiveSlots.get(mostRecent) : undefined)
|
|
@@ -1058,8 +1062,11 @@ export class LaunchTui {
|
|
|
1058
1062
|
if (this.finished) return;
|
|
1059
1063
|
const color = this.theme.color;
|
|
1060
1064
|
const role = e.role || "subagent";
|
|
1061
|
-
const roleLabel = e.
|
|
1062
|
-
|
|
1065
|
+
const roleLabel = e.detachedId
|
|
1066
|
+
? `${role.toUpperCase()}[${e.detachedId}]`
|
|
1067
|
+
: e.index && e.total
|
|
1068
|
+
? `${role.toUpperCase()}[${e.index}/${e.total}]`
|
|
1069
|
+
: role.toUpperCase();
|
|
1063
1070
|
const badge = categoryBadge("subagent", { color });
|
|
1064
1071
|
const ok = this.unicode ? "✓" : "v";
|
|
1065
1072
|
const bad = this.unicode ? "✗" : "x";
|
|
@@ -1067,14 +1074,10 @@ export class LaunchTui {
|
|
|
1067
1074
|
const last = this.unicode ? "└─" : "`-";
|
|
1068
1075
|
const detail = (e.detail ?? "").split("\n").find(l => l.trim().length > 0)?.trim().slice(0, 140) ?? "";
|
|
1069
1076
|
const summary = e.summary ? ` — ${e.summary}` : "";
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
const slot = e.
|
|
1074
|
-
? `detached:${e.id}`
|
|
1075
|
-
: e.index !== undefined
|
|
1076
|
-
? `fanout:${e.index}`
|
|
1077
|
-
: "single";
|
|
1077
|
+
// Slot key: stable `detachedId` when present (detached background task),
|
|
1078
|
+
// else fan-out 1-based `index` (concurrent batch worker),
|
|
1079
|
+
// else fixed key 0 (single foreground run).
|
|
1080
|
+
const slot: number | string = e.detachedId ?? e.index ?? 0;
|
|
1078
1081
|
const touchSlot = (line: string) => {
|
|
1079
1082
|
this.subagentLiveSlots.set(slot, line);
|
|
1080
1083
|
const at = this.subagentLiveOrder.indexOf(slot);
|
|
@@ -1087,58 +1090,41 @@ export class LaunchTui {
|
|
|
1087
1090
|
// activity readable in scrollback and visually separate from parent tools.
|
|
1088
1091
|
switch (e.kind) {
|
|
1089
1092
|
case "start":
|
|
1090
|
-
touchSlot(`${roleLabel}
|
|
1091
|
-
this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel}
|
|
1093
|
+
touchSlot(`${roleLabel} ${this.unicode ? "▸" : ">"} ${detail || "starting"}`);
|
|
1094
|
+
this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel} · ${detail}\n`, "subagent");
|
|
1092
1095
|
break;
|
|
1093
1096
|
case "step":
|
|
1094
|
-
touchSlot(`${roleLabel}
|
|
1095
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel}
|
|
1097
|
+
touchSlot(`${roleLabel} ${this.unicode ? "·" : "-"} ${detail || "working"}`);
|
|
1098
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel} · ${detail || "working"}\n`, "subagent");
|
|
1096
1099
|
break;
|
|
1097
1100
|
case "tool":
|
|
1098
|
-
touchSlot(`${roleLabel}
|
|
1099
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel}
|
|
1101
|
+
touchSlot(`${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}`);
|
|
1102
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}${summary}\n`, "subagent");
|
|
1100
1103
|
break;
|
|
1101
1104
|
case "error":
|
|
1102
|
-
touchSlot(`${roleLabel}
|
|
1103
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel}
|
|
1105
|
+
touchSlot(`${roleLabel} ${bad} ${detail || "error"}`);
|
|
1106
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel} ${bad} ${detail || "error"}\n`, "subagent");
|
|
1104
1107
|
break;
|
|
1105
1108
|
case "thinking":
|
|
1106
1109
|
// Live-only preview (mirrors the main turn's dimmed "Thinking" block) — never
|
|
1107
1110
|
// persisted to the ledger; see TaskSubEvent.kind's doc comment for why.
|
|
1108
|
-
touchSlot(`${roleLabel}
|
|
1111
|
+
touchSlot(`${roleLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
|
|
1109
1112
|
break;
|
|
1110
1113
|
case "done":
|
|
1111
|
-
// Clear ONLY this slot —
|
|
1112
|
-
// still-running siblings
|
|
1114
|
+
// Clear ONLY this slot — a fan-out batch where one worker finishes early
|
|
1115
|
+
// must keep showing its still-running siblings, not drop the `(sub)`
|
|
1116
|
+
// marker for the whole batch (the bug this per-slot map fixes).
|
|
1113
1117
|
this.subagentLiveSlots.delete(slot);
|
|
1114
1118
|
{
|
|
1115
1119
|
const at = this.subagentLiveOrder.indexOf(slot);
|
|
1116
1120
|
if (at !== -1) this.subagentLiveOrder.splice(at, 1);
|
|
1117
1121
|
}
|
|
1118
1122
|
this.subagentActive = this.subagentLiveSlots.size > 0;
|
|
1119
|
-
this.appendLedger(`${badge} ${last} ${roleLabel}
|
|
1123
|
+
this.appendLedger(`${badge} ${last} ${roleLabel} done${e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : ""}${e.success === false ? " (incomplete)" : ""}: ${detail}\n`, "subagent");
|
|
1120
1124
|
break;
|
|
1121
1125
|
}
|
|
1122
1126
|
this.draw();
|
|
1123
1127
|
}
|
|
1124
|
-
/** Render turn-scoped monitor lifecycle/output separately from subagent activity. */
|
|
1125
|
-
onMonitorEvent(e: MonitorJobEvent): void {
|
|
1126
|
-
if (this.finished) return;
|
|
1127
|
-
const badge = categoryBadge("progress", { color: this.theme.color });
|
|
1128
|
-
const label = sanitizeForFrame(
|
|
1129
|
-
`${e.record.id} · ${e.record.category} · ${e.record.description}`,
|
|
1130
|
-
).replace(/\s+/g, " ").trim().slice(0, 180);
|
|
1131
|
-
const branch = this.unicode ? "│" : "|";
|
|
1132
|
-
if (e.type === "start") {
|
|
1133
|
-
this.appendLedger(`${badge} ${this.unicode ? "◉" : "o"} Monitor ${label} started\n`, "monitor");
|
|
1134
|
-
} else if (e.type === "line") {
|
|
1135
|
-
const line = sanitizeForFrame(e.line).split("\n").find(row => row.trim())?.trim().slice(0, 180) ?? "";
|
|
1136
|
-
this.appendLedger(`${badge} ${branch} Monitor ${label}${line ? ` — ${line}` : ""}\n`, "monitor");
|
|
1137
|
-
} else {
|
|
1138
|
-
this.appendLedger(`${badge} ${this.unicode ? "└" : "`"} Monitor ${label} done\n`, "monitor");
|
|
1139
|
-
}
|
|
1140
|
-
this.draw();
|
|
1141
|
-
}
|
|
1142
1128
|
|
|
1143
1129
|
start(): void {
|
|
1144
1130
|
this.startedAt = Date.now();
|
|
@@ -1502,12 +1488,13 @@ export class LaunchTui {
|
|
|
1502
1488
|
* block shows only the most-recent lines, capped at ~30% of the screen height (a
|
|
1503
1489
|
* ceiling guards a tall terminal), so it grows with the stream and shrinks with the
|
|
1504
1490
|
* viewport. Returns [] when there is nothing to show. */
|
|
1505
|
-
/** gjc-parity multi-line parallel-subagent panel: when
|
|
1506
|
-
*
|
|
1491
|
+
/** gjc-parity multi-line parallel-subagent panel: when a fan-out `task` batch has
|
|
1492
|
+
* MORE THAN ONE concurrent slot live, render one status line PER active slot
|
|
1507
1493
|
* (most-recently-touched first) instead of collapsing everything into a single
|
|
1508
|
-
* "+N more running" summary line (see `currentActivity()`). A single
|
|
1509
|
-
* still uses the plain status-line path
|
|
1510
|
-
*
|
|
1494
|
+
* "+N more running" summary line (see `currentActivity()`). A single/detached
|
|
1495
|
+
* subagent still uses the plain status-line path — this panel exists specifically
|
|
1496
|
+
* to make a truly PARALLEL batch visibly read as parallel: every worker's latest
|
|
1497
|
+
* activity is on screen at once, capped so a large fan-out can't blow the frame. */
|
|
1511
1498
|
private static readonly SUBAGENT_PANEL_MAX_ROWS = 6;
|
|
1512
1499
|
private renderSubagentPanel(cols: number): string[] {
|
|
1513
1500
|
if (this.subagentLiveSlots.size <= 1) return [];
|
|
@@ -1706,8 +1693,9 @@ export class LaunchTui {
|
|
|
1706
1693
|
if (this.runningTool && this.liveToolOutput.trim()) {
|
|
1707
1694
|
tail.push(...this.renderLiveBlock("Output", this.liveToolOutput, cols, rows, 8));
|
|
1708
1695
|
}
|
|
1709
|
-
// Parallel subagent panel
|
|
1710
|
-
// multi-line block
|
|
1696
|
+
// Parallel subagent panel (gjc parity): a fan-out `task` batch with more than one
|
|
1697
|
+
// concurrent slot live gets its OWN multi-line block — one row per active worker —
|
|
1698
|
+
// so a genuinely parallel batch reads as parallel instead of a single rotating line.
|
|
1711
1699
|
if (this.subagentActive) {
|
|
1712
1700
|
tail.push(...this.renderSubagentPanel(cols));
|
|
1713
1701
|
}
|