jeo-code 0.5.8 → 0.5.10

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/README.ja.md CHANGED
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
150
150
  ## 変更履歴 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
154
+ - **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
153
155
  - **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
154
156
  - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
155
157
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
156
- - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
157
- - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
150
150
  ## 변경 이력 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
154
+ - **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
153
155
  - **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
154
156
  - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
155
157
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
156
- - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
157
- - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
150
150
  ## Changelog
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
154
+ - **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
153
155
  - **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
154
156
  - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
155
157
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
156
- - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
157
- - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
150
150
  ## 更新日志 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
154
+ - **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
153
155
  - **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
154
156
  - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
155
157
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
156
- - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
157
- - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
package/src/tui/app.ts CHANGED
@@ -100,6 +100,18 @@ function extractStreamingActivity(buf: string): string {
100
100
  return t.replace(/\s+/g, " ").slice(0, 140);
101
101
  }
102
102
 
103
+ /** Bound the input to a per-frame wrap to a fixed trailing window. The live thinking
104
+ * and tool-output blocks only ever DISPLAY their last few wrapped rows, but they
105
+ * accumulate the whole step's text — re-wrapping the FULL string every 120ms tick made
106
+ * per-frame work (and GC churn) grow linearly with how much had streamed (a long
107
+ * reasoning trace or a chatty tool can be hundreds of KB). Slicing to the last
108
+ * `maxChars` first keeps the visible tail byte-identical while capping wrap cost at
109
+ * O(maxChars) regardless of total size. 16 KiB is far more than the ~1 KB the visible
110
+ * rows need, so no on-screen row is ever lost to the cut. */
111
+ export const FRAME_WRAP_TAIL_CHARS = 16 * 1024;
112
+ export function tailForWrap(text: string, maxChars = FRAME_WRAP_TAIL_CHARS): string {
113
+ return text.length > maxChars ? text.slice(text.length - maxChars) : text;
114
+ }
103
115
  const DEFAULT_MAX_STEPS = 100;
104
116
  // Tools light enough that they never get a forge card (gjc parity): completion is a
105
117
  // single ✓/✗ ledger line; only failures surface a result card with the error body.
@@ -1112,7 +1124,7 @@ export class LaunchTui {
1112
1124
  const liveThink = this.streamingThought.trim() || this.streamingReasoning.trim();
1113
1125
  if (isThinking && liveThink) {
1114
1126
  const wrapW = Math.max(8, Math.min(120, cols) - 2);
1115
- const wrapped = liveThink
1127
+ const wrapped = tailForWrap(liveThink)
1116
1128
  .split("\n")
1117
1129
  .flatMap(l => wrapTextWithAnsi(l, wrapW))
1118
1130
  .filter(l => l.length > 0);
@@ -1133,7 +1145,7 @@ export class LaunchTui {
1133
1145
  // It is transient — cleared on result, when the formatted forge card takes over.
1134
1146
  if (this.runningTool && this.liveToolOutput.trim()) {
1135
1147
  const wrapW = Math.max(8, Math.min(120, cols) - 2);
1136
- const wrapped = this.liveToolOutput
1148
+ const wrapped = tailForWrap(this.liveToolOutput)
1137
1149
  .split("\n")
1138
1150
  .flatMap(l => wrapTextWithAnsi(l, wrapW))
1139
1151
  .filter(l => l.length > 0);
@@ -46,6 +46,20 @@ function firstToolResultLine(text: string | undefined): string {
46
46
  .slice(0, 96) ?? "";
47
47
  }
48
48
 
49
+ const TOOL_RESULT_GLOBAL = /^Tool \[([^\]]+)\] result \((ok|fail)\):/gm;
50
+ /** Split a tool-result user message — which for a BATCH holds several
51
+ * `Tool [x] result (ok|fail):` blocks joined by blank lines — into per-call
52
+ * verdicts in the order the engine emitted them (= the batch's call order). */
53
+ function parseToolVerdicts(text: string | undefined): { tool: string; status: string; firstLine: string }[] {
54
+ if (!text) return [];
55
+ const matches = [...text.matchAll(TOOL_RESULT_GLOBAL)];
56
+ return matches.map((mt, k) => {
57
+ const start = mt.index ?? 0;
58
+ const end = k + 1 < matches.length ? (matches[k + 1]!.index ?? text.length) : text.length;
59
+ return { tool: mt[1]!, status: mt[2]!, firstLine: firstToolResultLine(text.slice(start, end)) };
60
+ });
61
+ }
62
+
49
63
  /** Format engine history as a scrollback-friendly transcript. */
50
64
  export function formatTranscript(messages: readonly Message[], opts: TranscriptOptions = {}): string[] {
51
65
  const color = opts.color !== false;
@@ -92,25 +106,41 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
92
106
  lines.push(...clipBody(m.content, bodyCap));
93
107
  continue;
94
108
  }
95
- // assistant: a JSON tool call (one compact ledger line) or a prose reply.
96
- let invocation: { tool?: unknown; arguments?: unknown } | null = null;
109
+ // assistant: one or more JSON tool calls (compact ledger lines) or a prose reply.
110
+ // Handles BOTH the single `{tool,arguments}` form AND the batched `{tools:[...]}`
111
+ // form — the batch case previously parsed to no `tool` field, fell through, and
112
+ // dumped the raw JSON object into the transcript (the "/resume shows JSON" bug).
113
+ let parsed: { tool?: unknown; tools?: unknown; arguments?: unknown } | null = null;
97
114
  try {
98
- const parsed = JSON.parse(m.content) as { tool?: unknown; arguments?: unknown };
99
- if (parsed && typeof parsed === "object" && typeof parsed.tool === "string") invocation = parsed;
115
+ const p: unknown = JSON.parse(m.content);
116
+ if (p && typeof p === "object") parsed = p as { tool?: unknown; tools?: unknown; arguments?: unknown };
100
117
  } catch { /* prose reply */ }
101
- if (invocation && typeof invocation.tool === "string" && invocation.tool !== "done") {
102
- // The matching `Tool [x] result (ok|fail)` user message tells success/failure.
118
+ const calls: { tool: string; arguments?: unknown }[] =
119
+ parsed && typeof parsed.tool === "string"
120
+ ? [{ tool: parsed.tool, arguments: parsed.arguments }]
121
+ : parsed && Array.isArray(parsed.tools)
122
+ ? (parsed.tools as { tool?: unknown; arguments?: unknown }[])
123
+ .filter(c => c && typeof c.tool === "string")
124
+ .map(c => ({ tool: c.tool as string, arguments: c.arguments }))
125
+ : [];
126
+ const toolCalls = calls.filter(c => c.tool !== "done");
127
+ if (toolCalls.length > 0) {
128
+ // The matching `Tool [x] result (ok|fail)` user message follows; for a batch it
129
+ // is ONE message with several blocks. Parse verdicts in call order.
103
130
  const next = messages[i + 1];
104
- const verdict = next?.role === "user" ? next.content.match(TOOL_RESULT_RE) : null;
105
- const mark = verdict?.[2] === "fail" ? red(bad) : green(ok);
106
- const title = summarizeForgeInvocation(invocation.tool, invocation.arguments).title;
107
- const resultLine = firstToolResultLine(next?.content);
108
- const suffix = resultLine ? dim(` — ${resultLine}`) : "";
109
- lines.push(` ${mark} ${title}${suffix}`);
131
+ const verdicts = next?.role === "user" ? parseToolVerdicts(next.content) : [];
132
+ toolCalls.forEach((c, ci) => {
133
+ const v = verdicts[ci] ?? verdicts.find(x => x.tool === c.tool);
134
+ const mark = v?.status === "fail" ? red(bad) : green(ok);
135
+ const title = summarizeForgeInvocation(c.tool, c.arguments).title;
136
+ const suffix = v?.firstLine ? dim(` ${v.firstLine}`) : "";
137
+ lines.push(` ${mark} ${title}${suffix}`);
138
+ });
110
139
  continue;
111
140
  }
112
- const reason = invocation
113
- ? String((invocation.arguments as { reason?: unknown } | undefined)?.reason ?? "")
141
+ // A lone `done` (show its reason) or a plain prose reply.
142
+ const reason = parsed
143
+ ? String((parsed.arguments as { reason?: unknown } | undefined)?.reason ?? "")
114
144
  : m.content;
115
145
  if (!reason.trim()) continue;
116
146
  lines.push(`${magentaBold(`jeo ${jeoMark}`)}`);