jeo-code 0.8.41 → 0.8.43
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 +37 -0
- package/README.ja.md +3 -2
- package/README.ko.md +3 -2
- package/README.md +3 -2
- package/README.zh.md +3 -2
- package/package.json +1 -1
- package/src/agent/ast-edit-tool.ts +2 -2
- package/src/agent/engine.ts +44 -7
- package/src/agent/job-registry.ts +186 -28
- package/src/agent/job-tool.ts +1 -1
- package/src/agent/lsp-rename-tool.ts +2 -2
- package/src/agent/monitor-tool.ts +51 -0
- package/src/agent/subagent-registry.ts +2 -2
- package/src/agent/subagent-tool.ts +1 -1
- package/src/agent/subagents.ts +2 -2
- package/src/agent/task-tool.ts +74 -12
- package/src/agent/tool-schemas.ts +16 -0
- package/src/agent/tools.ts +36 -4
- package/src/ai/providers/anthropic.ts +31 -6
- package/src/commands/launch/slash-handlers.ts +2 -0
- package/src/commands/launch/stream.ts +16 -1
- package/src/commands/launch.ts +38 -8
- package/src/tui/app.ts +57 -41
- package/src/tui/components/forge.ts +31 -0
- package/src/types/playwriter.d.ts +15 -0
package/src/commands/launch.ts
CHANGED
|
@@ -19,7 +19,8 @@ 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 } from "../agent/job-registry";
|
|
22
|
+
import { JobRegistry, type MonitorJobEvent } from "../agent/job-registry";
|
|
23
|
+
import { createMonitorTool, MONITOR_TOOL_PROTOCOL_LINE } from "../agent/monitor-tool";
|
|
23
24
|
import { createIrcTool, IRC_TOOL_PROTOCOL_LINE } from "../agent/irc-tool";
|
|
24
25
|
import { createGoalTool, GOAL_TOOL_PROTOCOL_LINE } from "../agent/goal-tool";
|
|
25
26
|
import { createApproveTool, APPROVE_TOOL_PROTOCOL_LINE } from "../agent/approve-tool";
|
|
@@ -185,6 +186,7 @@ import {
|
|
|
185
186
|
gatedStdout,
|
|
186
187
|
formatTaskSubEvent,
|
|
187
188
|
logTaskSubEvent,
|
|
189
|
+
formatMonitorJobEvent,
|
|
188
190
|
createStreamEvents,
|
|
189
191
|
shouldUseOneShotTui,
|
|
190
192
|
} from "./launch/stream";
|
|
@@ -560,7 +562,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
560
562
|
// pi-style: load project context (JEO.md / AGENTS.md / .jeo/context.md / CLAUDE.md) into the prompt.
|
|
561
563
|
const contextFiles = await loadProjectContext(cwd);
|
|
562
564
|
|
|
563
|
-
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "eval", "todo", "subagent", "job", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
|
|
565
|
+
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "eval", "todo", "subagent", "job", "monitor", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
|
|
564
566
|
let allowedTools = new Set(KNOWN_TOOLS);
|
|
565
567
|
|
|
566
568
|
if (flags.noTools) {
|
|
@@ -633,6 +635,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
633
635
|
(allowedTools.has("subagent") ? "\n\nDetached subagents: " + SUBAGENT_TOOL_PROTOCOL_LINE +
|
|
634
636
|
" Launch background work with task {\"detached\": true, \"role\": <role>, \"task\": <assignment>}; it returns a subagent id immediately so you can keep working and collect the result later." : "") +
|
|
635
637
|
(allowedTools.has("job") ? "\n\nBackground jobs: " + JOB_TOOL_PROTOCOL_LINE : "") +
|
|
638
|
+
(allowedTools.has("monitor") ? "\n\nBackground monitors: " + MONITOR_TOOL_PROTOCOL_LINE : "") +
|
|
636
639
|
(allowedTools.has("irc") ? "\n\nPeer messaging: " + IRC_TOOL_PROTOCOL_LINE : "") +
|
|
637
640
|
(allowedTools.has("goal") ? "\n\nGoal tracking: " + GOAL_TOOL_PROTOCOL_LINE : "") +
|
|
638
641
|
(allowedTools.has("approve") ? "\n\nPlan approval: " + APPROVE_TOOL_PROTOCOL_LINE : "") +
|
|
@@ -1422,7 +1425,32 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1422
1425
|
// #9: per-turn registry for DETACHED subagents (task{detached:true}); the
|
|
1423
1426
|
// `subagent` tool controls them and cancelAll() in finally prevents orphans.
|
|
1424
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);
|
|
1425
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);
|
|
1426
1454
|
sessionNotifyEndpoint?.attachRegistry(subagentRegistry);
|
|
1427
1455
|
currentTurnSteer = { push: line => steerInbox.push(line), flushCard: line => tui?.flushSteerCard(line) };
|
|
1428
1456
|
if (sessionNotifyEndpoint && !notifyRedact) {
|
|
@@ -1495,21 +1523,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1495
1523
|
steer: drainSteer,
|
|
1496
1524
|
registry: subagentRegistry,
|
|
1497
1525
|
sessionEndpoint: sessionNotifyEndpoint,
|
|
1498
|
-
onEvent:
|
|
1499
|
-
? (e => tui?.onSubagentEvent(e))
|
|
1500
|
-
: (e => logTaskSubEvent(e)),
|
|
1526
|
+
onEvent: onSubagentEvent,
|
|
1501
1527
|
}),
|
|
1502
1528
|
eval: createEvalTool({
|
|
1503
1529
|
config: { ...turnConfig, defaultModel: activeModel },
|
|
1504
1530
|
signal: ac.signal,
|
|
1505
1531
|
steer: drainSteer,
|
|
1506
|
-
onEvent:
|
|
1507
|
-
? (e => tui?.onSubagentEvent(e))
|
|
1508
|
-
: (e => logTaskSubEvent(e)),
|
|
1532
|
+
onEvent: onSubagentEvent,
|
|
1509
1533
|
}),
|
|
1510
1534
|
todo: createTodoTool({ onChange: items => { turnTodos = items; tui?.setTodos(items); } }),
|
|
1511
1535
|
subagent: createSubagentTool(subagentRegistry),
|
|
1512
1536
|
job: createJobTool(jobRegistry),
|
|
1537
|
+
monitor: createMonitorTool(jobRegistry),
|
|
1513
1538
|
irc: createIrcTool(subagentRegistry),
|
|
1514
1539
|
goal: createGoalTool(),
|
|
1515
1540
|
approve: createApproveTool(),
|
|
@@ -1615,6 +1640,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1615
1640
|
signal: ac.signal,
|
|
1616
1641
|
sessionKey: cacheKey,
|
|
1617
1642
|
steer: drainSteer,
|
|
1643
|
+
monitor: drainMonitor,
|
|
1618
1644
|
rateLimitFallbackAvailable,
|
|
1619
1645
|
safetyFallbackAvailable,
|
|
1620
1646
|
events: wrapEvents(withStepPersistence({ ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone }, persistTurnTail), opik),
|
|
@@ -1748,6 +1774,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1748
1774
|
signal: ac.signal,
|
|
1749
1775
|
sessionKey: turnSessionKey,
|
|
1750
1776
|
steer: drainSteer,
|
|
1777
|
+
monitor: drainMonitor,
|
|
1751
1778
|
// Deliberately NOT wiring rateLimitFallbackAvailable here: this repair
|
|
1752
1779
|
// retry has no reroute loop around it (unlike runLoopWithModel's
|
|
1753
1780
|
// routeFallbackAttempt loop) — bailing the retry ladder early on a 429
|
|
@@ -1772,11 +1799,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1772
1799
|
await opik.endTurn({ done: result.done, steps: result.steps, output: result.doneReason });
|
|
1773
1800
|
} finally {
|
|
1774
1801
|
harness.dispose();
|
|
1802
|
+
acceptingTuiSubagentEvents = false;
|
|
1803
|
+
acceptingMonitorEvents = false;
|
|
1775
1804
|
subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
|
|
1776
1805
|
sessionNotifyEndpoint?.detachRegistry(); // this turn's registry is gone — subagent frames stop applying to it
|
|
1777
1806
|
currentTurnSteer = undefined; // a remote message after this point queues instead of steering a dead turn
|
|
1778
1807
|
await stopSessionNotifyEndpoint(subagentRegistry); // tear down the FALLBACK lazy endpoint, if one was started (no-op when sessionNotifyEndpoint owns the registry)
|
|
1779
1808
|
jobRegistry.cancelAll(); // background jobs are turn-scoped too — no orphaned processes
|
|
1809
|
+
unsubscribeMonitor();
|
|
1780
1810
|
|
|
1781
1811
|
// Steering typed but never drained (e.g. entered just after the final step)
|
|
1782
1812
|
// must not be lost — fold it into the next prompt draft so it runs next.
|
package/src/tui/app.ts
CHANGED
|
@@ -20,6 +20,7 @@ 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";
|
|
23
24
|
import { supportsUnicode } from "./components/capability";
|
|
24
25
|
import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
|
|
25
26
|
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
@@ -274,19 +275,15 @@ export class LaunchTui {
|
|
|
274
275
|
// Cumulative token usage for the live turn (engine onUsage event).
|
|
275
276
|
private turnUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
276
277
|
// True while at least one delegated subagent SLOT is in flight — drives the `(sub)`
|
|
277
|
-
// status marker. Derived from `subagentLiveSlots.size`
|
|
278
|
-
//
|
|
279
|
-
// clears the marker while its siblings are still visibly running.
|
|
278
|
+
// status marker. Derived from `subagentLiveSlots.size` rather than a plain boolean
|
|
279
|
+
// so one completed concurrent run cannot clear still-running siblings.
|
|
280
280
|
private subagentActive = false;
|
|
281
|
-
// Latest nested subagent activity, PER CONCURRENT SLOT
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// and, worse, cleared the whole `(sub)` marker the instant ANY one worker finished).
|
|
285
|
-
// Keyed by the event's fan-out `index` (1-based); a single/detached run with no
|
|
286
|
-
// index shares the fixed key 0. `subagentLiveOrder` tracks touch order so the
|
|
281
|
+
// Latest nested subagent activity, PER CONCURRENT SLOT. Detached runs use their
|
|
282
|
+
// stable registry id, fan-out workers use their 1-based index, and legacy
|
|
283
|
+
// synchronous runs share one slot. `subagentLiveOrder` tracks touch order so the
|
|
287
284
|
// status row can show the MOST RECENTLY active slot plus a "+N more" count.
|
|
288
|
-
private readonly subagentLiveSlots = new Map<
|
|
289
|
-
private readonly subagentLiveOrder:
|
|
285
|
+
private readonly subagentLiveSlots = new Map<string, string>();
|
|
286
|
+
private readonly subagentLiveOrder: string[] = [];
|
|
290
287
|
// Bounded activity-history ring: one plain-text entry per ledger append, with a
|
|
291
288
|
// turn-relative timestamp. Powers Ctrl+O's "recent activity" tail so the detail
|
|
292
289
|
// view ALWAYS answers "what has been happening", even before the first reply.
|
|
@@ -1004,10 +1001,9 @@ export class LaunchTui {
|
|
|
1004
1001
|
}
|
|
1005
1002
|
// A delegated subagent's LATEST nested event beats the parent's static
|
|
1006
1003
|
// "Task: <role> …" card title — a long task otherwise reads as a stall even
|
|
1007
|
-
// though the subagent is actively reading/editing/running underneath. When
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
// one subagent whose label keeps randomly changing.
|
|
1004
|
+
// though the subagent is actively reading/editing/running underneath. When
|
|
1005
|
+
// multiple concurrent slots live, append a "+N more running" count so parallel
|
|
1006
|
+
// work does not look like one subagent whose label keeps randomly changing.
|
|
1011
1007
|
if (this.subagentActive && this.subagentLiveSlots.size > 0) {
|
|
1012
1008
|
const mostRecent = this.subagentLiveOrder[this.subagentLiveOrder.length - 1];
|
|
1013
1009
|
const line = (mostRecent !== undefined ? this.subagentLiveSlots.get(mostRecent) : undefined)
|
|
@@ -1063,6 +1059,7 @@ export class LaunchTui {
|
|
|
1063
1059
|
const color = this.theme.color;
|
|
1064
1060
|
const role = e.role || "subagent";
|
|
1065
1061
|
const roleLabel = e.index && e.total ? `${role.toUpperCase()}[${e.index}/${e.total}]` : role.toUpperCase();
|
|
1062
|
+
const detachedLabel = e.detached && e.id ? ` [${e.id}]` : "";
|
|
1066
1063
|
const badge = categoryBadge("subagent", { color });
|
|
1067
1064
|
const ok = this.unicode ? "✓" : "v";
|
|
1068
1065
|
const bad = this.unicode ? "✗" : "x";
|
|
@@ -1070,10 +1067,14 @@ export class LaunchTui {
|
|
|
1070
1067
|
const last = this.unicode ? "└─" : "`-";
|
|
1071
1068
|
const detail = (e.detail ?? "").split("\n").find(l => l.trim().length > 0)?.trim().slice(0, 140) ?? "";
|
|
1072
1069
|
const summary = e.summary ? ` — ${e.summary}` : "";
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
const slot = e.
|
|
1070
|
+
// Detached runs must retain their registry identity so concurrent detached events
|
|
1071
|
+
// cannot collide. Fan-out indices remain separate, while legacy synchronous/eval
|
|
1072
|
+
// events preserve their single shared slot.
|
|
1073
|
+
const slot = e.detached && e.id
|
|
1074
|
+
? `detached:${e.id}`
|
|
1075
|
+
: e.index !== undefined
|
|
1076
|
+
? `fanout:${e.index}`
|
|
1077
|
+
: "single";
|
|
1077
1078
|
const touchSlot = (line: string) => {
|
|
1078
1079
|
this.subagentLiveSlots.set(slot, line);
|
|
1079
1080
|
const at = this.subagentLiveOrder.indexOf(slot);
|
|
@@ -1086,41 +1087,58 @@ export class LaunchTui {
|
|
|
1086
1087
|
// activity readable in scrollback and visually separate from parent tools.
|
|
1087
1088
|
switch (e.kind) {
|
|
1088
1089
|
case "start":
|
|
1089
|
-
touchSlot(`${roleLabel} ${this.unicode ? "▸" : ">"} ${detail || "starting"}`);
|
|
1090
|
-
this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel} · ${detail}\n`, "subagent");
|
|
1090
|
+
touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "▸" : ">"} ${detail || "starting"}`);
|
|
1091
|
+
this.appendLedger(`${badge} ${this.unicode ? "▸" : ">"} ${roleLabel}${detachedLabel} · ${detail}\n`, "subagent");
|
|
1091
1092
|
break;
|
|
1092
1093
|
case "step":
|
|
1093
|
-
touchSlot(`${roleLabel} ${this.unicode ? "·" : "-"} ${detail || "working"}`);
|
|
1094
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel} · ${detail || "working"}\n`, "subagent");
|
|
1094
|
+
touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "·" : "-"} ${detail || "working"}`);
|
|
1095
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} · ${detail || "working"}\n`, "subagent");
|
|
1095
1096
|
break;
|
|
1096
1097
|
case "tool":
|
|
1097
|
-
touchSlot(`${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}`);
|
|
1098
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel} ${e.success === false ? bad : ok} ${detail || "tool"}${summary}\n`, "subagent");
|
|
1098
|
+
touchSlot(`${roleLabel}${detachedLabel} ${e.success === false ? bad : ok} ${detail || "tool"}`);
|
|
1099
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} ${e.success === false ? bad : ok} ${detail || "tool"}${summary}\n`, "subagent");
|
|
1099
1100
|
break;
|
|
1100
1101
|
case "error":
|
|
1101
|
-
touchSlot(`${roleLabel} ${bad} ${detail || "error"}`);
|
|
1102
|
-
this.appendLedger(` ${badge} ${branch} ${roleLabel} ${bad} ${detail || "error"}\n`, "subagent");
|
|
1102
|
+
touchSlot(`${roleLabel}${detachedLabel} ${bad} ${detail || "error"}`);
|
|
1103
|
+
this.appendLedger(` ${badge} ${branch} ${roleLabel}${detachedLabel} ${bad} ${detail || "error"}\n`, "subagent");
|
|
1103
1104
|
break;
|
|
1104
1105
|
case "thinking":
|
|
1105
1106
|
// Live-only preview (mirrors the main turn's dimmed "Thinking" block) — never
|
|
1106
1107
|
// persisted to the ledger; see TaskSubEvent.kind's doc comment for why.
|
|
1107
|
-
touchSlot(`${roleLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
|
|
1108
|
+
touchSlot(`${roleLabel}${detachedLabel} ${this.unicode ? "…" : "..."} ${detail || "thinking"}`);
|
|
1108
1109
|
break;
|
|
1109
1110
|
case "done":
|
|
1110
|
-
// Clear ONLY this slot —
|
|
1111
|
-
//
|
|
1112
|
-
// marker for the whole batch (the bug this per-slot map fixes).
|
|
1111
|
+
// Clear ONLY this slot — completing one concurrent run must leave its
|
|
1112
|
+
// still-running siblings visible and keep the `(sub)` marker active.
|
|
1113
1113
|
this.subagentLiveSlots.delete(slot);
|
|
1114
1114
|
{
|
|
1115
1115
|
const at = this.subagentLiveOrder.indexOf(slot);
|
|
1116
1116
|
if (at !== -1) this.subagentLiveOrder.splice(at, 1);
|
|
1117
1117
|
}
|
|
1118
1118
|
this.subagentActive = this.subagentLiveSlots.size > 0;
|
|
1119
|
-
this.appendLedger(`${badge} ${last} ${roleLabel} done${e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : ""}${e.success === false ? " (incomplete)" : ""}: ${detail}\n`, "subagent");
|
|
1119
|
+
this.appendLedger(`${badge} ${last} ${roleLabel}${detachedLabel} done${e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : ""}${e.success === false ? " (incomplete)" : ""}: ${detail}\n`, "subagent");
|
|
1120
1120
|
break;
|
|
1121
1121
|
}
|
|
1122
1122
|
this.draw();
|
|
1123
1123
|
}
|
|
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
|
+
}
|
|
1124
1142
|
|
|
1125
1143
|
start(): void {
|
|
1126
1144
|
this.startedAt = Date.now();
|
|
@@ -1484,13 +1502,12 @@ export class LaunchTui {
|
|
|
1484
1502
|
* block shows only the most-recent lines, capped at ~30% of the screen height (a
|
|
1485
1503
|
* ceiling guards a tall terminal), so it grows with the stream and shrinks with the
|
|
1486
1504
|
* viewport. Returns [] when there is nothing to show. */
|
|
1487
|
-
/** gjc-parity multi-line parallel-subagent panel: when
|
|
1488
|
-
*
|
|
1505
|
+
/** gjc-parity multi-line parallel-subagent panel: when MORE THAN ONE concurrent
|
|
1506
|
+
* fan-out or detached slot is live, render one status line PER active slot
|
|
1489
1507
|
* (most-recently-touched first) instead of collapsing everything into a single
|
|
1490
|
-
* "+N more running" summary line (see `currentActivity()`). A single
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
* activity is on screen at once, capped so a large fan-out can't blow the frame. */
|
|
1508
|
+
* "+N more running" summary line (see `currentActivity()`). A single subagent
|
|
1509
|
+
* still uses the plain status-line path; this panel makes concurrent work visibly
|
|
1510
|
+
* parallel while capping rows so a large batch cannot blow the frame. */
|
|
1494
1511
|
private static readonly SUBAGENT_PANEL_MAX_ROWS = 6;
|
|
1495
1512
|
private renderSubagentPanel(cols: number): string[] {
|
|
1496
1513
|
if (this.subagentLiveSlots.size <= 1) return [];
|
|
@@ -1689,9 +1706,8 @@ export class LaunchTui {
|
|
|
1689
1706
|
if (this.runningTool && this.liveToolOutput.trim()) {
|
|
1690
1707
|
tail.push(...this.renderLiveBlock("Output", this.liveToolOutput, cols, rows, 8));
|
|
1691
1708
|
}
|
|
1692
|
-
// Parallel subagent panel
|
|
1693
|
-
//
|
|
1694
|
-
// so a genuinely parallel batch reads as parallel instead of a single rotating line.
|
|
1709
|
+
// Parallel subagent panel: more than one fan-out or detached slot gets its own
|
|
1710
|
+
// multi-line block so genuinely parallel work does not read as one rotating line.
|
|
1695
1711
|
if (this.subagentActive) {
|
|
1696
1712
|
tail.push(...this.renderSubagentPanel(cols));
|
|
1697
1713
|
}
|
|
@@ -293,6 +293,37 @@ export function summarizeForgeInvocation(tool: string, rawArgs: unknown, opts: {
|
|
|
293
293
|
|
|
294
294
|
if (normalized === "task") {
|
|
295
295
|
const role = stringArg(args, "role") ?? "executor";
|
|
296
|
+
// Fan-out/batch form (`tasks: [...]`, task-tool.ts's concurrent MAX_FANOUT path) previously
|
|
297
|
+
// fell through to the single-task branch below, which only reads `task`/`prompt`/`assignment`
|
|
298
|
+
// — every batch call rendered as a bare "<missing task>" placeholder with zero visibility into
|
|
299
|
+
// what was actually about to run concurrently, even on a well-formed call (live-reproduced: a
|
|
300
|
+
// real 3-item fan-out showed nothing but "<missing task>" in the tool-call preview box).
|
|
301
|
+
// A JSON-STRINGIFIED `tasks` (live-reproduced model mistake task-tool.ts's handler itself now
|
|
302
|
+
// tolerates — see createTaskTool's `tasksArg` coercion) is accepted here too, for the same
|
|
303
|
+
// reason: the preview must reflect what will actually run, not just the well-formed case.
|
|
304
|
+
const tasksValue = typeof args.tasks === "string"
|
|
305
|
+
? (() => { try { const p = JSON.parse(args.tasks as string); return Array.isArray(p) ? p : args.tasks; } catch { return args.tasks; } })()
|
|
306
|
+
: args.tasks;
|
|
307
|
+
if (Array.isArray(tasksValue)) {
|
|
308
|
+
const items = (tasksValue as unknown[])
|
|
309
|
+
.map(entry => {
|
|
310
|
+
if (typeof entry === "string") return entry.trim();
|
|
311
|
+
if (entry && typeof entry === "object") {
|
|
312
|
+
const e = entry as Record<string, unknown>;
|
|
313
|
+
return String(e.task ?? e.assignment ?? e.prompt ?? "").trim();
|
|
314
|
+
}
|
|
315
|
+
return "";
|
|
316
|
+
})
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
const lines = items.length > 0
|
|
319
|
+
? items.map((t, i) => {
|
|
320
|
+
const firstLine = t.split("\n")[0]!;
|
|
321
|
+
const truncated = firstLine.length > 120 || t.includes("\n");
|
|
322
|
+
return `${i + 1}. ${firstLine.slice(0, 120)}${truncated ? "…" : ""}`;
|
|
323
|
+
})
|
|
324
|
+
: ["<missing tasks>"];
|
|
325
|
+
return { title: `Task: ${role} ×${items.length}`, language: "text", lines };
|
|
326
|
+
}
|
|
296
327
|
const task = stringArg(args, "task", "prompt", "assignment") ?? "<missing task>";
|
|
297
328
|
const context = stringArg(args, "context");
|
|
298
329
|
return {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/types/playwriter.d.ts
|
|
2
|
+
// Declaration file to allow TypeScript to recognize the optional Playwriter CLI module.
|
|
3
|
+
// This is only needed for the example automation script in scripts/playwriter-apply.ts.
|
|
4
|
+
// The actual Playwriter package is expected to be installed globally or run via npx.
|
|
5
|
+
|
|
6
|
+
declare module "playwriter" {
|
|
7
|
+
export class Playwriter {
|
|
8
|
+
/** Connect to the running Chrome extension bridge */
|
|
9
|
+
connect(): Promise<void>;
|
|
10
|
+
/** Open a new page (tab) in the connected browser */
|
|
11
|
+
newPage(): Promise<any>;
|
|
12
|
+
/** Disconnect from the bridge */
|
|
13
|
+
disconnect(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
}
|