jeo-code 0.8.42 → 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/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` (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.
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 (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 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<number, string>();
289
- private readonly subagentLiveOrder: number[] = [];
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 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.
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
- // Fan-out slot key: the event's 1-based `index` when present (concurrent batch
1074
- // worker), else the fixed key 0 (single-task/detached run — matches the prior
1075
- // single-string behavior for the non-batch case).
1076
- const slot = e.index ?? 0;
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 — a fan-out batch where one worker finishes early
1111
- // must keep showing its still-running siblings, not drop the `(sub)`
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 a fan-out `task` batch has
1488
- * MORE THAN ONE concurrent slot live, render one status line PER active slot
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/detached
1491
- * subagent still uses the plain status-line path — this panel exists specifically
1492
- * to make a truly PARALLEL batch visibly read as parallel: every worker's latest
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 (gjc parity): a fan-out `task` batch with more than one
1693
- // concurrent slot live gets its OWN multi-line block — one row per active worker —
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
  }
@@ -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
+ }