jeo-code 0.4.7 → 0.4.8
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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/tui/app.ts +36 -16
- package/src/tui/components/width.ts +51 -0
- package/src/tui/renderer.ts +38 -12
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.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
153
154
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
154
155
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
155
156
|
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
156
157
|
- **[0.4.4]** (2026-06-13) — Live subagent status mirroring, always-useful Ctrl+O activity tail, read lineRange crash guard.
|
|
157
|
-
- **[0.4.3]** (2026-06-13) — Readability pass for autopilot, subagent activity, and worked-history review.
|
|
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.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
153
154
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
154
155
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
155
156
|
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
156
157
|
- **[0.4.4]** (2026-06-13) — Live subagent status mirroring, always-useful Ctrl+O activity tail, read lineRange crash guard.
|
|
157
|
-
- **[0.4.3]** (2026-06-13) — Readability pass for autopilot, subagent activity, and worked-history review.
|
|
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.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
153
154
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
154
155
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
155
156
|
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
156
157
|
- **[0.4.4]** (2026-06-13) — Live subagent status mirroring, always-useful Ctrl+O activity tail, read lineRange crash guard.
|
|
157
|
-
- **[0.4.3]** (2026-06-13) — Readability pass for autopilot, subagent activity, and worked-history review.
|
|
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.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
153
154
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
154
155
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
155
156
|
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
156
157
|
- **[0.4.4]** (2026-06-13) — Live subagent status mirroring, always-useful Ctrl+O activity tail, read lineRange crash guard.
|
|
157
|
-
- **[0.4.3]** (2026-06-13) — Readability pass for autopilot, subagent activity, and worked-history review.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/tui/app.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { costForUsage, formatCost } from "../ai/pricing";
|
|
|
30
30
|
import { renderMarkdownTables } from "./components/markdown-table";
|
|
31
31
|
|
|
32
32
|
import { stripMarkdown, renderMarkdownAnsi } from "./components/markdown-text";
|
|
33
|
-
import { visibleWidth, wrapTextWithAnsi } from "./components/width";
|
|
33
|
+
import { visibleWidth, wrapTextWithAnsi, truncateToWidth, sanitizeForFrame } from "./components/width";
|
|
34
34
|
import { categoryBadge } from "./components/category-index";
|
|
35
35
|
import { formatStepTimeline, stepsFromTools, formatStepHeader, formatStepTimelineCompact, type StepState } from "./components/step-timeline";
|
|
36
36
|
import { formatHintBar } from "./components/hints";
|
|
@@ -428,7 +428,10 @@ export class LaunchTui {
|
|
|
428
428
|
},
|
|
429
429
|
onToolProgress: (_tool, partial) => {
|
|
430
430
|
if (this.finished) return;
|
|
431
|
-
|
|
431
|
+
// Sanitize raw child stdout (CR / EL / cursor-move escapes) before it enters the
|
|
432
|
+
// frame — unsanitized control bytes tore the renderer's next \x1b[2K (literal "2K")
|
|
433
|
+
// and hijacked the cursor, corrupting the live frame.
|
|
434
|
+
this.liveToolOutput = sanitizeForFrame(partial);
|
|
432
435
|
if (Date.now() - this.lastStreamDraw >= 100) {
|
|
433
436
|
this.lastStreamDraw = Date.now();
|
|
434
437
|
this.draw();
|
|
@@ -1119,12 +1122,16 @@ export class LaunchTui {
|
|
|
1119
1122
|
.split("\n")
|
|
1120
1123
|
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
1121
1124
|
.filter(l => l.length > 0);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1125
|
+
// FIXED reserved height (bottom-anchored, blank-padded at top): once present the
|
|
1126
|
+
// block's row count is CONSTANT, so streaming content never changes the frame
|
|
1127
|
+
// height. The per-100ms height thrash that desynced the differential renderer
|
|
1128
|
+
// (duplicate model bar) is gone; height now toggles only at lifecycle boundaries.
|
|
1129
|
+
const ROWS = 6;
|
|
1130
|
+
const shown = wrapped.slice(-ROWS);
|
|
1131
|
+
tail.push(dim(`${this.unicode ? "│" : "|"} thinking`));
|
|
1132
|
+
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1133
|
+
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1134
|
+
tail.push("");
|
|
1128
1135
|
}
|
|
1129
1136
|
|
|
1130
1137
|
// Live tool output (gjc-style streaming bash stdout): while a tool runs, its
|
|
@@ -1136,12 +1143,14 @@ export class LaunchTui {
|
|
|
1136
1143
|
.split("\n")
|
|
1137
1144
|
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
1138
1145
|
.filter(l => l.length > 0);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1146
|
+
// FIXED reserved height (see thinking block): constant rows while a tool streams,
|
|
1147
|
+
// so cumulative stdout growth does not thrash the frame height.
|
|
1148
|
+
const ROWS = 8;
|
|
1149
|
+
const shown = wrapped.slice(-ROWS);
|
|
1150
|
+
tail.push(dim(`${this.unicode ? "│" : "|"} output`));
|
|
1151
|
+
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1152
|
+
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1153
|
+
tail.push("");
|
|
1145
1154
|
}
|
|
1146
1155
|
|
|
1147
1156
|
// Live status field: unboxed thinking line + compact metrics row. The model's
|
|
@@ -1307,7 +1316,18 @@ export class LaunchTui {
|
|
|
1307
1316
|
// model bar), no outer border, no mascot art — completed work lives in scrollback.
|
|
1308
1317
|
if (fit && this.inline) {
|
|
1309
1318
|
const inlineFrame = this.composeInlineFrame({ cols, rows, stepNow, elapsedMs, idx, isThinking, planLines });
|
|
1310
|
-
|
|
1319
|
+
// Option C (constant live-frame height): pad the composed frame to EXACTLY `rows`
|
|
1320
|
+
// — blank rows at the TOP, the tail (status/hud/input/model bar) pinned to the
|
|
1321
|
+
// bottom. With a constant height the differential renderer reserves rows ONCE and
|
|
1322
|
+
// thereafter only does in-place, within-frame cursor moves; the bottom-margin
|
|
1323
|
+
// reserve-GROW that drifted the anchor by one row (the duplicate model bar during
|
|
1324
|
+
// rapid tool churn) never runs again. Every line is still width-clamped so a long
|
|
1325
|
+
// line cannot soft-wrap into a second physical row and desync the row accounting.
|
|
1326
|
+
const capped = inlineFrame.length > rows ? inlineFrame.slice(inlineFrame.length - rows) : inlineFrame;
|
|
1327
|
+
const fixedHeight = capped.length < rows
|
|
1328
|
+
? [...new Array(rows - capped.length).fill(""), ...capped]
|
|
1329
|
+
: capped;
|
|
1330
|
+
this.renderer.render(fixedHeight.map(l => truncateToWidth(l, cols)));
|
|
1311
1331
|
return;
|
|
1312
1332
|
}
|
|
1313
1333
|
|
|
@@ -1492,6 +1512,6 @@ export class LaunchTui {
|
|
|
1492
1512
|
if (fit) {
|
|
1493
1513
|
frame = frame.slice(0, rows);
|
|
1494
1514
|
}
|
|
1495
|
-
this.renderer.render(frame);
|
|
1515
|
+
this.renderer.render(frame.map(l => truncateToWidth(l, cols)));
|
|
1496
1516
|
}
|
|
1497
1517
|
}
|
|
@@ -134,6 +134,57 @@ export function truncateToWidth(s: string, cols: number): string {
|
|
|
134
134
|
return out;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Strip control bytes that would corrupt the live differential frame. KEEPS SGR color
|
|
139
|
+
* escapes (`\x1b[…m`); DROPS every other CSI (cursor moves, EL/ED erase, etc.), OSC
|
|
140
|
+
* sequences, other escapes, and bare C0/C1 control bytes except tab/newline; and DROPS
|
|
141
|
+
* an INCOMPLETE trailing escape (a chunk that ends mid-sequence) so it can never eat the
|
|
142
|
+
* next line's `\x1b[2K`. Used to sanitize raw child stdout (e.g. a streaming `bun test`
|
|
143
|
+
* with `\r\x1b[2K` progress lines) before it enters the frame — the torn-escape /
|
|
144
|
+
* cursor-hijack class of screen corruption.
|
|
145
|
+
*/
|
|
146
|
+
export function sanitizeForFrame(s: string): string {
|
|
147
|
+
if (!s.includes("\x1b") && !/[\x00-\x08\x0b-\x1f\x7f]/.test(s)) return s; // fast path
|
|
148
|
+
let out = "";
|
|
149
|
+
let i = 0;
|
|
150
|
+
const n = s.length;
|
|
151
|
+
while (i < n) {
|
|
152
|
+
const ch = s[i]!;
|
|
153
|
+
if (ch === "\x1b") {
|
|
154
|
+
if (s[i + 1] === "[") {
|
|
155
|
+
// CSI: ESC [ params (0-9;:?<>= space) final (@-~)
|
|
156
|
+
let j = i + 2;
|
|
157
|
+
while (j < n && /[0-9;:?<>= ]/.test(s[j]!)) j++;
|
|
158
|
+
if (j < n && /[@-~]/.test(s[j]!)) {
|
|
159
|
+
const seq = s.slice(i, j + 1);
|
|
160
|
+
if (seq.endsWith("m")) out += seq; // keep SGR color, drop all other CSI
|
|
161
|
+
i = j + 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
break; // incomplete CSI at the chunk tail → drop the rest
|
|
165
|
+
}
|
|
166
|
+
if (s[i + 1] === "]") {
|
|
167
|
+
// OSC: ESC ] … (BEL | ST = ESC \)
|
|
168
|
+
let j = i + 2;
|
|
169
|
+
while (j < n && s[j] !== "\x07" && !(s[j] === "\x1b" && s[j + 1] === "\\")) j++;
|
|
170
|
+
if (j >= n) break; // incomplete OSC → drop
|
|
171
|
+
i = s[j] === "\x07" ? j + 1 : j + 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
i += i + 1 < n ? 2 : 1; // other ESC x → drop ESC (+ its single byte)
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const code = ch.charCodeAt(0);
|
|
178
|
+
if ((code < 0x20 && code !== 0x09 && code !== 0x0a) || code === 0x7f) {
|
|
179
|
+
i++; // strip bare control bytes (CR/BS/…), keep tab + newline
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
out += ch;
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
137
188
|
/**
|
|
138
189
|
* Hard-wrap text to `cols` display columns, breaking long words and preserving
|
|
139
190
|
* existing newlines. SGR-aware (escapes don't consume width). Returns the wrapped
|
package/src/tui/renderer.ts
CHANGED
|
@@ -51,17 +51,22 @@ export class Renderer {
|
|
|
51
51
|
this.prevCols = currentCols;
|
|
52
52
|
|
|
53
53
|
const next = lines.map(line => truncate(line, currentCols));
|
|
54
|
+
// Rows physically occupied by the prior frame — or recorded by reset() when the
|
|
55
|
+
// baseline was dropped WITHOUT clearing the screen. The diff below EL-clears any
|
|
56
|
+
// of these that the new (possibly shorter) frame does not cover, and the reserve
|
|
57
|
+
// block below uses it so a post-reset repaint does not spuriously re-scroll.
|
|
58
|
+
const occupied = Math.max(this.prev.length, this.coverRows);
|
|
54
59
|
const maxLen = Math.max(this.prev.length, next.length, this.coverRows);
|
|
55
60
|
this.coverRows = 0;
|
|
56
61
|
let cursorRow = 0;
|
|
57
62
|
let out = "";
|
|
58
63
|
|
|
59
|
-
if (this.reserve && next.length >
|
|
64
|
+
if (this.reserve && next.length > occupied && next.length <= Math.max(1, size().rows)) {
|
|
60
65
|
// The cursor rests on the frame's first row (the anchor). Walk to the last
|
|
61
66
|
// currently-occupied row, emit one newline per missing row (scrolling the
|
|
62
67
|
// viewport when at the bottom margin), then hop back up to the — possibly
|
|
63
68
|
// shifted — anchor so the diff below paints at stable relative positions.
|
|
64
|
-
const have = Math.max(
|
|
69
|
+
const have = Math.max(occupied, 1);
|
|
65
70
|
out += cursorDown(have - 1) + "\n".repeat(next.length - have) + cursorUp(next.length - 1) + toColumn(1);
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -92,16 +97,17 @@ export class Renderer {
|
|
|
92
97
|
}
|
|
93
98
|
out += toColumn(1);
|
|
94
99
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.syncOpen = false;
|
|
101
|
-
}
|
|
102
|
-
|
|
100
|
+
// Atomic present (DECSET-2026 synchronized update): wrap the WHOLE repaint so the
|
|
101
|
+
// terminal never shows a half-painted frame — no torn row, no transient duplicate
|
|
102
|
+
// bar a mid-repaint snapshot could catch. An insertAbove() may have already opened
|
|
103
|
+
// the update (syncOpen); otherwise this render opens its own. Exactly one BSU/ESU
|
|
104
|
+
// pair is emitted per write.
|
|
103
105
|
if (out.length > 0) {
|
|
104
|
-
this.write(out);
|
|
106
|
+
this.write((this.syncOpen ? "" : BEGIN_SYNC) + out + END_SYNC);
|
|
107
|
+
this.syncOpen = false;
|
|
108
|
+
} else if (this.syncOpen) {
|
|
109
|
+
this.write(END_SYNC);
|
|
110
|
+
this.syncOpen = false;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
this.prev = next;
|
|
@@ -137,7 +143,13 @@ export class Renderer {
|
|
|
137
143
|
// clamped cursor-down desynced the row bookkeeping — each subsequent frame then
|
|
138
144
|
// painted one row higher, devouring the flushed scrollback content above (the
|
|
139
145
|
// "truncated card" corruption).
|
|
140
|
-
|
|
146
|
+
// Use the same occupancy measure the reserve block uses (max of prev.length and
|
|
147
|
+
// coverRows). A reset() between frames drops prev but records coverRows; ignoring it
|
|
148
|
+
// here left the old frame's lower rows uncleared and the cursor below the true
|
|
149
|
+
// anchor, so the next render's cursorDown crossed the bottom margin and clamped —
|
|
150
|
+
// the persistent off-by-one that duplicated the model bar.
|
|
151
|
+
const occupied = Math.max(this.prev.length, this.coverRows);
|
|
152
|
+
const stale = occupied - written;
|
|
141
153
|
if (stale > 0) {
|
|
142
154
|
for (let i = 0; i < stale; i++) {
|
|
143
155
|
out += toColumn(1) + clearLine() + (i < stale - 1 ? cursorDown(1) : "");
|
|
@@ -146,6 +158,7 @@ export class Renderer {
|
|
|
146
158
|
}
|
|
147
159
|
this.write(out);
|
|
148
160
|
this.prev = [];
|
|
161
|
+
this.coverRows = 0; // consumed: the frame below is now the single source of truth
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/** Clear the live frame. Inline (reserve) mode walks the known frame rows with
|
|
@@ -174,6 +187,19 @@ export class Renderer {
|
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
reset(): void {
|
|
190
|
+
// Drop the diff baseline so the next render() repaints every line — but REMEMBER
|
|
191
|
+
// how many rows are physically on screen so that repaint also EL-clears any the
|
|
192
|
+
// new (possibly shorter) frame doesn't cover. Without this, a self-heal reset on a
|
|
193
|
+
// frame that just shrank left stale rows behind (duplicate model bars / orphaned
|
|
194
|
+
// borders — the live-analysis screen corruption).
|
|
195
|
+
this.coverRows = Math.max(this.coverRows, this.prev.length);
|
|
177
196
|
this.prev = [];
|
|
197
|
+
// Close any synchronized update opened by a preceding insertAbove() so a reset()
|
|
198
|
+
// landing between insertAbove() and the next render() cannot strand an open BSU
|
|
199
|
+
// window (which times out ~150ms later and flashes a partial frame).
|
|
200
|
+
if (this.syncOpen) {
|
|
201
|
+
this.write(END_SYNC);
|
|
202
|
+
this.syncOpen = false;
|
|
203
|
+
}
|
|
178
204
|
}
|
|
179
205
|
}
|