jeo-code 0.7.30 → 0.7.32

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.ja.md +5 -3
  3. package/README.ko.md +5 -3
  4. package/README.md +5 -3
  5. package/README.zh.md +5 -3
  6. package/package.json +3 -3
  7. package/scripts/install.sh +19 -0
  8. package/scripts/uninstall.sh +2 -2
  9. package/src/agent/AGENTS.md +12 -0
  10. package/src/agent/ast-edit-tool.ts +123 -0
  11. package/src/agent/ast-file-scan.ts +62 -0
  12. package/src/agent/ast-grep-tool.ts +107 -0
  13. package/src/agent/ast-match.ts +241 -0
  14. package/src/agent/compaction.ts +6 -19
  15. package/src/agent/debug-session.ts +435 -0
  16. package/src/agent/debug-tool.ts +89 -0
  17. package/src/agent/engine.ts +103 -18
  18. package/src/agent/goal-tool.ts +57 -0
  19. package/src/agent/irc-tool.ts +71 -0
  20. package/src/agent/job-registry.ts +163 -0
  21. package/src/agent/job-tool.ts +106 -0
  22. package/src/agent/lsp-rename-tool.ts +108 -0
  23. package/src/agent/lsp-tool.ts +177 -0
  24. package/src/agent/memory-graph.ts +6 -1
  25. package/src/agent/memory.ts +73 -52
  26. package/src/agent/session.ts +27 -0
  27. package/src/agent/subagent-registry.ts +40 -4
  28. package/src/agent/subagent-tool.ts +27 -7
  29. package/src/agent/subagents.ts +1 -1
  30. package/src/agent/task-tool.ts +27 -5
  31. package/src/agent/tools.ts +100 -5
  32. package/src/agent/ts-language-service.ts +173 -0
  33. package/src/ai/model-manager.ts +58 -26
  34. package/src/ai/sse.ts +22 -7
  35. package/src/ai/think-tags.ts +10 -1
  36. package/src/commands/deep-interview.ts +49 -1
  37. package/src/commands/launch/agents-slash.ts +489 -0
  38. package/src/commands/launch/flags.ts +27 -9
  39. package/src/commands/launch/model-slash.ts +290 -0
  40. package/src/commands/launch/session-slash.ts +305 -0
  41. package/src/commands/launch/slash-handlers.ts +6 -0
  42. package/src/commands/launch/system-slash.ts +81 -0
  43. package/src/commands/launch/tmux.ts +32 -0
  44. package/src/commands/launch.ts +227 -750
  45. package/src/tui/app.ts +32 -12
  46. package/src/tui/components/evolution.ts +23 -3
  47. package/src/tui/components/width.ts +11 -6
package/src/tui/app.ts CHANGED
@@ -288,6 +288,10 @@ export class LaunchTui {
288
288
  // block above the heartbeat; pressing Ctrl+O again clears it and restores the
289
289
  // normal activity view. Kept as data, not scrollback text, so it can actually close.
290
290
  private historyLines: string[] | null = null;
291
+ // ponytail: cached wrapped lines for history panel to avoid O(history) re-wrap per tick
292
+ private cachedWrappedHistory: string[] | null = null;
293
+ private cachedHistoryLines: string[] | null = null;
294
+ private cachedHistoryWidth: number | null = null;
291
295
  // Ctrl+O detail panel scroll: a window offset so long/CJK content is fully
292
296
  // reachable (↑↓/PgUp/PgDn) instead of clipped at "… N more". Bound + page size
293
297
  // are recomputed each render from the visible body height.
@@ -323,6 +327,9 @@ export class LaunchTui {
323
327
  private readonly unicode: boolean = supportsUnicode();
324
328
  // Active color theme (JEO_TUI_THEME), default cosmic; `mono` disables color.
325
329
  private readonly theme = resolveTheme(process.env);
330
+ // Terminal color depth, detected once — TERM/COLORTERM/NO_COLOR are process-constant,
331
+ // so re-running the regex chain 2-4× per 120ms draw was pure waste.
332
+ private readonly colorLevel = detectColorLevel(process.env, isTTY());
326
333
  // Neon flow palette derived from the active theme (forge-card border + forge mark
327
334
  // animation), so the live glow tracks the theme instead of a fixed brand palette.
328
335
  private readonly themeFlow = themeFlowPalette(this.theme);
@@ -557,7 +564,9 @@ export class LaunchTui {
557
564
  // Sanitize raw child stdout (CR / EL / cursor-move escapes) before it enters the
558
565
  // frame — unsanitized control bytes tore the renderer's next \x1b[2K (literal "2K")
559
566
  // and hijacked the cursor, corrupting the live frame.
560
- this.liveToolOutput = sanitizeForFrame(partial);
567
+ // Only the display WINDOW is sanitized: renderLiveBlock tails the text anyway, so
568
+ // scanning the full cumulative buffer per event was O(n²) over a chatty stream.
569
+ this.liveToolOutput = sanitizeForFrame(tailForWrap(partial));
561
570
  if (Date.now() - this.lastStreamDraw >= 100) {
562
571
  this.lastStreamDraw = Date.now();
563
572
  this.draw();
@@ -1339,10 +1348,18 @@ export class LaunchTui {
1339
1348
  const inner = Math.max(10, boxWidth - 2);
1340
1349
  const accent = this.theme.color ? accentPaint(this.theme) : (s: string) => s;
1341
1350
  const dim = this.theme.color ? chalk.dim : (s: string) => s;
1342
- const wrapped = this.historyLines.flatMap(line => {
1343
- const physical = line.split("\n");
1344
- return physical.flatMap(part => (visibleWidth(part) <= inner ? [part] : wrapTextWithAnsi(part, inner)));
1345
- });
1351
+ let wrapped: string[];
1352
+ if (this.historyLines === this.cachedHistoryLines && inner === this.cachedHistoryWidth && this.cachedWrappedHistory) {
1353
+ wrapped = this.cachedWrappedHistory;
1354
+ } else {
1355
+ wrapped = this.historyLines.flatMap(line => {
1356
+ const physical = line.split("\n");
1357
+ return physical.flatMap(part => (visibleWidth(part) <= inner ? [part] : wrapTextWithAnsi(part, inner)));
1358
+ });
1359
+ this.cachedHistoryLines = this.historyLines;
1360
+ this.cachedHistoryWidth = inner;
1361
+ this.cachedWrappedHistory = wrapped;
1362
+ }
1346
1363
  const bodyLimit = Math.max(1, maxRows - 4); // box borders (2) + title + divider
1347
1364
  const scrollable = wrapped.length > bodyLimit;
1348
1365
  // Window capacity at the bottom (worst case: both ↑ and ↓ indicators take a row),
@@ -1433,7 +1450,7 @@ export class LaunchTui {
1433
1450
  }): string[] {
1434
1451
  const { cols, rows, stepNow, elapsedMs, idx, isThinking } = args;
1435
1452
  const dim = this.theme.color ? chalk.dim : (s: string) => s;
1436
- const colorLevel = detectColorLevel(process.env, isTTY());
1453
+ const colorLevel = this.colorLevel;
1437
1454
  // One quantized animation clock for the whole frame: gradient phase cycles 20
1438
1455
  // steps, the prompt beat advances every 3 ticks. Quantization keeps repaints
1439
1456
  // coherent (status field + forge border move together) and bounds per-tick work.
@@ -1541,7 +1558,7 @@ export class LaunchTui {
1541
1558
  cols,
1542
1559
  unicode: this.unicode,
1543
1560
  color: this.theme.color,
1544
- colorLevel: detectColorLevel(process.env, isTTY()),
1561
+ colorLevel: this.colorLevel,
1545
1562
  gradient: themeGradient(this.theme, 2),
1546
1563
  });
1547
1564
  }
@@ -1584,7 +1601,7 @@ export class LaunchTui {
1584
1601
  frame: twist,
1585
1602
  unicode: this.unicode,
1586
1603
  color: this.theme.color,
1587
- colorLevel: detectColorLevel(process.env, isTTY()),
1604
+ colorLevel: this.colorLevel,
1588
1605
  palette: this.themeFlow,
1589
1606
  });
1590
1607
  this.cachedArt = fit ? centerBlock(art, innerWidth) : art;
@@ -1598,9 +1615,6 @@ export class LaunchTui {
1598
1615
  const trackCount = showArt ? 1 : 0;
1599
1616
  const headerHeight = artLinesCount + trackCount + (showArt ? 1 : 0);
1600
1617
 
1601
- const toolLines = this.tools.render(fit ? Math.max(3, rows - 15) : undefined, { color: this.theme.color, indexed: fit });
1602
- const toolListHeight = toolLines.length;
1603
-
1604
1618
  const planLines = this.renderPlan(this.theme.color);
1605
1619
  const planHeight = planLines.length;
1606
1620
 
@@ -1619,6 +1633,12 @@ export class LaunchTui {
1619
1633
  return;
1620
1634
  }
1621
1635
 
1636
+ // Tool rows are only consumed by the boxed (non-inline) layout below — computing
1637
+ // them before the inline early-return re-painted up to 500 chalk rows per tick
1638
+ // on the DEFAULT path that never uses them.
1639
+ const toolLines = this.tools.render(fit ? Math.max(3, rows - 15) : undefined, { color: this.theme.color, indexed: fit });
1640
+ const toolListHeight = toolLines.length;
1641
+
1622
1642
  // Bottom-pinned status + footer.
1623
1643
  const bottom: string[] = [];
1624
1644
  const statusMsg = this.currentActivity();
@@ -1626,7 +1646,7 @@ export class LaunchTui {
1626
1646
  // known price — then no $ is shown). Computed once per draw, fed to status + footer.
1627
1647
  const costUsd = costForUsage(this.footer.model, this.turnUsage) ?? undefined;
1628
1648
  if (isThinking) {
1629
- const colorLevel = detectColorLevel(process.env, isTTY());
1649
+ const colorLevel = this.colorLevel;
1630
1650
  const phase = (this.tickCount * 0.05) % 1;
1631
1651
  const grad = themeGradient(this.theme, idx);
1632
1652
  const palette = [grad.from, grad.to];
@@ -308,7 +308,17 @@ export function transitionMessage(index: number): string {
308
308
  * Applies a dimensional, animated gradient (foreground or background) to a string of text.
309
309
  * Driven by a phaseMs timestamp, it creates a slow, tasteful shimmer wave
310
310
  * and adds depth cues (bright core, dimmer edges).
311
+ *
312
+ * The clock is QUANTIZED to 225ms buckets (20 discrete phases over the 4.5s period,
313
+ * matching the forge art's quantized-clock rule): callers pass raw Date.now(), but two
314
+ * calls in the same bucket produce byte-identical output, so the differential renderer
315
+ * skips the row instead of rewriting ~2-4KB of truecolor escapes on every idle tick.
316
+ * A small memo returns the exact prior string for a repeated (bucket, style, text) —
317
+ * the common case between consecutive 120ms ticks.
311
318
  */
319
+ const GRADIENT_BUCKET_MS = 225;
320
+ const gradientMemo = new Map<string, string>();
321
+ const GRADIENT_MEMO_CAP = 16;
312
322
  export function applyDimensionalGradient(
313
323
  text: string,
314
324
  timeMs: number,
@@ -320,6 +330,10 @@ export function applyDimensionalGradient(
320
330
  ): string {
321
331
  const plain = stripAnsi(text);
322
332
  if (level === ColorLevel.None || plain.length === 0) return plain;
333
+ const bucket = timeMs - (timeMs % GRADIENT_BUCKET_MS);
334
+ const memoKey = `${bucket}|${fromHex}|${toHex}|${level}|${isBg ? 1 : 0}|${fgHex}|${plain}`;
335
+ const hit = gradientMemo.get(memoKey);
336
+ if (hit !== undefined) return hit;
323
337
 
324
338
  const from = hexToRgb(fromHex);
325
339
  const to = hexToRgb(toHex);
@@ -331,8 +345,8 @@ export function applyDimensionalGradient(
331
345
  out += fgEscape(fg, level);
332
346
  }
333
347
 
334
- // Slow period: 4.5 seconds for a complete wave cycle
335
- const p = (timeMs / 4500) % 1;
348
+ // Slow period: 4.5 seconds for a complete wave cycle (quantized bucket → 20 phases)
349
+ const p = (bucket / 4500) % 1;
336
350
 
337
351
  for (let i = 0; i < L; i++) {
338
352
  const ch = plain[i]!;
@@ -380,5 +394,11 @@ export function applyDimensionalGradient(
380
394
  }
381
395
  }
382
396
 
383
- return out + resetEscape(level);
397
+ const result = out + resetEscape(level);
398
+ if (gradientMemo.size >= GRADIENT_MEMO_CAP) {
399
+ const oldest = gradientMemo.keys().next().value;
400
+ if (oldest !== undefined) gradientMemo.delete(oldest);
401
+ }
402
+ gradientMemo.set(memoKey, result);
403
+ return result;
384
404
  }
@@ -217,9 +217,13 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
217
217
  out.push(rawLine);
218
218
  continue;
219
219
  }
220
+ if (!rawLine.includes("\x1b") && !/[\t\u0300-\uffff]/.test(rawLine) && !/[\u{10000}-\u{10ffff}]/u.test(rawLine)) {
221
+ for (let i = 0; i < rawLine.length; i += width) out.push(rawLine.slice(i, i + width));
222
+ continue;
223
+ }
220
224
  let rest = rawLine;
221
225
  let active = ""; // SGR open at the wrap boundary, carried to the next line
222
- while (visibleWidth(rest) > width) {
226
+ while (true) {
223
227
  const head = truncateToWidth(rest, width);
224
228
  // Advance past exactly the consumed substring. truncateToWidth may append a
225
229
  // SYNTHETIC trailing reset (frame safety) that is NOT in `rest` — including it
@@ -230,6 +234,12 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
230
234
  : head.endsWith(RESET) && rest.startsWith(head.slice(0, -RESET.length))
231
235
  ? head.slice(0, -RESET.length)
232
236
  : head;
237
+ if (consumed.length === rest.length) {
238
+ let line = active + rest;
239
+ if (active && !line.endsWith(RESET)) line += RESET;
240
+ out.push(line);
241
+ break;
242
+ }
233
243
  let line = active + head;
234
244
  const next = sgrStateAfter(active, consumed);
235
245
  // Close any color still open at the line end so it cannot tint the padding/border.
@@ -238,11 +248,6 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
238
248
  active = next;
239
249
  rest = rest.slice(consumed.length);
240
250
  }
241
- if (rest.length > 0) {
242
- let line = active + rest;
243
- if (active && !line.endsWith(RESET)) line += RESET;
244
- out.push(line);
245
- }
246
251
  }
247
252
  return out;
248
253
  }