@oh-my-pi/pi-coding-agent 15.10.2 → 15.10.4

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 (95) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
  3. package/dist/types/edit/index.d.ts +0 -1
  4. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
  5. package/dist/types/eval/bridge-timeout.d.ts +1 -1
  6. package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
  7. package/dist/types/eval/idle-timeout.d.ts +1 -1
  8. package/dist/types/lsp/index.d.ts +0 -5
  9. package/dist/types/main.d.ts +11 -0
  10. package/dist/types/modes/components/assistant-message.d.ts +0 -9
  11. package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
  12. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  13. package/dist/types/modes/components/session-selector.d.ts +16 -7
  14. package/dist/types/modes/components/tool-execution.d.ts +0 -18
  15. package/dist/types/modes/types.d.ts +4 -0
  16. package/dist/types/session/messages.d.ts +11 -8
  17. package/dist/types/session/yield-queue.d.ts +10 -1
  18. package/dist/types/tools/eval-render.d.ts +0 -1
  19. package/dist/types/tools/index.d.ts +31 -0
  20. package/dist/types/tools/path-utils.d.ts +5 -1
  21. package/dist/types/tools/read.d.ts +2 -1
  22. package/dist/types/tools/render-utils.d.ts +3 -1
  23. package/dist/types/tools/renderers.d.ts +0 -15
  24. package/dist/types/tools/write.d.ts +0 -2
  25. package/dist/types/tui/code-cell.d.ts +0 -2
  26. package/dist/types/tui/hyperlink.d.ts +5 -7
  27. package/dist/types/tui/output-block.d.ts +0 -18
  28. package/package.json +9 -9
  29. package/src/cli/gallery-cli.ts +4 -0
  30. package/src/cli/gallery-fixtures/codeintel.ts +0 -1
  31. package/src/cli/gallery-fixtures/fs.ts +68 -1
  32. package/src/cli/gallery-fixtures/types.ts +8 -1
  33. package/src/commit/agentic/agent.ts +1 -0
  34. package/src/edit/hashline/diff.ts +86 -0
  35. package/src/edit/hashline/execute.ts +14 -1
  36. package/src/edit/index.ts +31 -17
  37. package/src/edit/renderer.ts +116 -31
  38. package/src/eval/__tests__/agent-bridge.test.ts +13 -0
  39. package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
  40. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  41. package/src/eval/agent-bridge.ts +6 -1
  42. package/src/eval/bridge-timeout.ts +1 -1
  43. package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
  44. package/src/eval/idle-timeout.ts +1 -1
  45. package/src/eval/js/context-manager.ts +66 -6
  46. package/src/eval/js/shared/prelude.txt +28 -12
  47. package/src/eval/js/tool-bridge.ts +3 -3
  48. package/src/eval/js/worker-entry.ts +6 -0
  49. package/src/eval/py/prelude.py +3 -3
  50. package/src/internal-urls/docs-index.generated.ts +8 -7
  51. package/src/lsp/index.ts +128 -52
  52. package/src/main.ts +54 -14
  53. package/src/modes/components/assistant-message.ts +3 -15
  54. package/src/modes/components/late-diagnostics-message.ts +60 -0
  55. package/src/modes/components/plan-review-overlay.ts +26 -5
  56. package/src/modes/components/read-tool-group.ts +415 -35
  57. package/src/modes/components/session-selector.ts +89 -35
  58. package/src/modes/components/tips.txt +1 -1
  59. package/src/modes/components/tool-execution.ts +7 -49
  60. package/src/modes/components/transcript-container.ts +108 -32
  61. package/src/modes/controllers/event-controller.ts +6 -1
  62. package/src/modes/controllers/input-controller.ts +10 -2
  63. package/src/modes/types.ts +4 -0
  64. package/src/modes/utils/ui-helpers.ts +26 -5
  65. package/src/prompts/system/manual-continue.md +7 -0
  66. package/src/prompts/system/plan-mode-active.md +56 -72
  67. package/src/prompts/system/tiny-title-system.md +1 -1
  68. package/src/prompts/system/title-system.md +16 -3
  69. package/src/prompts/system/workflow-notice.md +1 -1
  70. package/src/prompts/tools/eval.md +6 -4
  71. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  72. package/src/sdk.ts +59 -1
  73. package/src/session/agent-session.ts +5 -3
  74. package/src/session/messages.ts +21 -14
  75. package/src/session/session-manager.ts +2 -2
  76. package/src/session/yield-queue.ts +20 -2
  77. package/src/task/executor.ts +1 -0
  78. package/src/tiny/title-client.ts +6 -1
  79. package/src/tools/bash.ts +0 -7
  80. package/src/tools/eval-render.ts +6 -25
  81. package/src/tools/eval.ts +1 -1
  82. package/src/tools/find.ts +148 -106
  83. package/src/tools/index.ts +32 -0
  84. package/src/tools/path-utils.ts +19 -22
  85. package/src/tools/read.ts +16 -8
  86. package/src/tools/render-utils.ts +3 -1
  87. package/src/tools/renderers.ts +0 -15
  88. package/src/tools/ssh.ts +0 -1
  89. package/src/tools/todo.ts +1 -0
  90. package/src/tools/write.ts +3 -12
  91. package/src/tui/code-cell.ts +1 -6
  92. package/src/tui/hyperlink.ts +13 -23
  93. package/src/tui/output-block.ts +2 -97
  94. package/src/utils/title-generator.ts +2 -2
  95. /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
package/src/tools/todo.ts CHANGED
@@ -927,6 +927,7 @@ export const todoToolRenderer = {
927
927
  sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
928
928
  state: options.isPartial ? "pending" : "success",
929
929
  borderColor: "borderMuted",
930
+ applyBg: false,
930
931
  width,
931
932
  };
932
933
  });
@@ -277,7 +277,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
277
277
  readonly label = "Write";
278
278
  readonly description: string;
279
279
  readonly parameters = writeSchema;
280
- readonly nonAbortable = true;
281
280
  readonly strict = true;
282
281
  readonly concurrency = "exclusive";
283
282
  readonly loadMode = "discoverable";
@@ -582,6 +581,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
582
581
  const batchRequest = getLspBatchRequest(context?.toolCall);
583
582
  const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
584
583
  invalidateFsScanAfterWrite(absolutePath);
584
+ this.session.bumpFileMutationVersion?.(absolutePath);
585
585
  this.session.fileSnapshotStore?.invalidate(absolutePath);
586
586
  this.session.conflictHistory?.invalidate(entry.id);
587
587
 
@@ -707,6 +707,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
707
707
 
708
708
  const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
709
709
  invalidateFsScanAfterWrite(absolutePath);
710
+ this.session.bumpFileMutationVersion?.(absolutePath);
710
711
  this.session.fileSnapshotStore?.invalidate(absolutePath);
711
712
  for (const entry of fileEntries) history.invalidate(entry.id);
712
713
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, text);
@@ -886,6 +887,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
886
887
 
887
888
  const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
888
889
  invalidateFsScanAfterWrite(absolutePath);
890
+ this.session.bumpFileMutationVersion?.(absolutePath);
889
891
  const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
890
892
 
891
893
  const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
@@ -1039,17 +1041,6 @@ export const writeToolRenderer = {
1039
1041
  });
1040
1042
  },
1041
1043
 
1042
- // Only the expanded (Ctrl+O) preview is append-only: it renders the whole
1043
- // content top-anchored, so streamed chunks only append rows at the bottom.
1044
- // The collapsed preview slides a bounded tail window (`formatStreamingContent`
1045
- // with `WRITE_STREAMING_PREVIEW_LINES`) whose visible rows re-layout as the
1046
- // window moves — not append-only, but it never overflows the viewport, so its
1047
- // head is never at risk of being dropped regardless. `write` has no partial
1048
- // result (content streams as args), so `result` is ignored here.
1049
- isStreamingPreviewAppendOnly(args: WriteRenderArgs, options: RenderResultOptions, _result?: unknown): boolean {
1050
- return Boolean(options?.expanded && args.content);
1051
- },
1052
-
1053
1044
  renderResult(
1054
1045
  result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
1055
1046
  options: RenderResultOptions,
@@ -32,8 +32,6 @@ export interface CodeCellOptions {
32
32
  */
33
33
  codeTail?: boolean;
34
34
  expanded?: boolean;
35
- /** Animate the cell border with a sweeping segment while pending/running. */
36
- animate?: boolean;
37
35
  width: number;
38
36
  }
39
37
 
@@ -147,10 +145,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
147
145
  sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
148
146
  }
149
147
 
150
- return renderOutputBlock(
151
- { header: title, headerMeta: meta, state, sections, width, animate: options.animate },
152
- theme,
153
- );
148
+ return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
154
149
  }
155
150
 
156
151
  export interface MarkdownCellOptions {
@@ -5,6 +5,7 @@
5
5
  * sequences when the active terminal supports hyperlinks and the user setting
6
6
  * permits it. Falls back to plain text when disabled.
7
7
  */
8
+ import * as url from "node:url";
8
9
  import { TERMINAL } from "@oh-my-pi/pi-tui";
9
10
  import { settings } from "../config/settings";
10
11
  import {
@@ -28,21 +29,12 @@ function buildLinkId(uri: string): string {
28
29
  return (h >>> 0).toString(16).padStart(8, "0");
29
30
  }
30
31
 
31
- /** Build a `file://` URI for an absolute path with optional line/col query params. */
32
- function buildFileUri(absPath: string, opts?: { line?: number; col?: number }): string {
33
- // Normalize backslashes for Windows paths before constructing the URL.
34
- const normalized = absPath.replaceAll("\\", "/");
35
- const prefix = normalized.startsWith("/") ? "file://" : "file:///";
36
- // Split on slashes, encode each component, reassemble.
37
- const encoded = normalized
38
- .split("/")
39
- .map(segment => encodeURIComponent(segment))
40
- .join("/");
41
- const params: string[] = [];
42
- if (opts?.line !== undefined) params.push(`line=${opts.line}`);
43
- if (opts?.col !== undefined) params.push(`col=${opts.col}`);
44
- const query = params.length > 0 ? `?${params.join("&")}` : "";
45
- return `${prefix}${encoded}${query}`;
32
+ /** Build a properly encoded `file://` URI with optional line/col query params. */
33
+ function buildFileUri(filePath: string, opts?: { line?: number; col?: number }): string {
34
+ const uri = url.pathToFileURL(filePath);
35
+ if (opts?.line !== undefined) uri.searchParams.set("line", String(opts.line));
36
+ if (opts?.col !== undefined) uri.searchParams.set("col", String(opts.col));
37
+ return uri.href;
46
38
  }
47
39
 
48
40
  /**
@@ -104,21 +96,19 @@ export function urlHyperlink(url: string, displayText: string): string {
104
96
  }
105
97
 
106
98
  /**
107
- * Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
99
+ * Wrap `displayText` in an OSC 8 hyperlink pointing at a filesystem path.
108
100
  *
109
101
  * Returns `displayText` unchanged when hyperlinks are disabled or when
110
102
  * the text already contains an OSC 8 sequence (prevents double-wrapping).
103
+ * Relative paths resolve against the current working directory before URI
104
+ * encoding so the OSC 8 target is always a valid `file://` URL.
111
105
  *
112
- * The caller is responsible for passing an absolute path. Relative paths
113
- * produce invalid `file://` URIs and are accepted silently to avoid runtime
114
- * errors in renderer hot paths.
115
- *
116
- * @param absPath - Absolute filesystem path
106
+ * @param filePath - Filesystem path
117
107
  * @param displayText - Text to render as the hyperlink anchor (may contain ANSI codes)
118
108
  * @param opts - Optional line/col position appended as `?line=N&col=M` query params
119
109
  */
120
- export function fileHyperlink(absPath: string, displayText: string, opts?: { line?: number; col?: number }): string {
121
- return wrapHyperlink(buildFileUri(absPath, opts), displayText);
110
+ export function fileHyperlink(filePath: string, displayText: string, opts?: { line?: number; col?: number }): string {
111
+ return wrapHyperlink(buildFileUri(filePath, opts), displayText);
122
112
  }
123
113
 
124
114
  /**
@@ -17,8 +17,6 @@ export interface OutputBlockOptions {
17
17
  width: number;
18
18
  applyBg?: boolean;
19
19
  contentPaddingLeft?: number;
20
- /** Animate the border with a sweeping dark segment (pending/running state). */
21
- animate?: boolean;
22
20
  /** Override the state-derived border color. Used for muted "legacy" tool
23
21
  * frames that should not visually compete with framed-output tools. */
24
22
  borderColor?: ThemeColor;
@@ -37,59 +35,6 @@ export function isFramedBlockComponent(component: Component): boolean {
37
35
  return (component as FramedBlockComponent)[FRAMED_BLOCK_COMPONENT] === true;
38
36
  }
39
37
 
40
- const BORDER_SHIMMER_TICK_MS = 1000 / 30;
41
- /** Duration of one full left↔right↔left bounce of the bottom-edge segment, in
42
- * ms. Position is derived from the wall clock against this fixed cycle so a
43
- * resize only nudges the segment proportionally instead of teleporting it. */
44
- const BORDER_BOUNCE_MS = 3000;
45
- /** Length, in border cells, of the moving segment. */
46
- const BORDER_SEGMENT_LEN = 8;
47
-
48
- /**
49
- * Monotonic frame counter for animated borders, quantized to the TUI's ~30fps
50
- * render cap so the cache key advances once per animation frame — fine enough
51
- * for a smooth segment sweep, coarse enough to coalesce multiple render passes
52
- * land inside the same frame.
53
- */
54
- export function borderShimmerTick(): number {
55
- return Math.floor(Date.now() / BORDER_SHIMMER_TICK_MS);
56
- }
57
-
58
- /** Ease-in-out so the segment decelerates into and accelerates out of each wall. */
59
- function easeInOutQuad(t: number): number {
60
- return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
61
- }
62
-
63
- /**
64
- * Column of the travelling segment's center on the bottom edge for a box of
65
- * inner width `W` at time `now`. The segment bounces left → right → left across
66
- * the bottom border: a triangle wave over one full there-and-back cycle, eased
67
- * per leg so it slows as it nears each wall before reversing. Position is
68
- * derived from the wall clock against a fixed cycle, so a resize shifts the
69
- * center proportionally — no reset.
70
- */
71
- export function borderSegmentHeadCol(W: number, now: number): number {
72
- if (W <= 1) return 0;
73
- const phase = (((now % BORDER_BOUNCE_MS) + BORDER_BOUNCE_MS) % BORDER_BOUNCE_MS) / BORDER_BOUNCE_MS;
74
- // Triangle: 0→1 rightward over the first half, 1→0 leftward over the second.
75
- const leg = phase < 0.5 ? phase * 2 : 2 - phase * 2;
76
- return easeInOutQuad(leg) * (W - 1);
77
- }
78
-
79
- /**
80
- * Scale a truecolor foreground escape toward black by `factor`. Returns
81
- * undefined for 256-color escapes (no RGB to scale) so callers fall back to a
82
- * dimmer theme color.
83
- */
84
- function darkenFgAnsi(ansi: string, factor: number): string | undefined {
85
- const m = /38;2;(\d+);(\d+);(\d+)/.exec(ansi);
86
- if (!m) return undefined;
87
- const r = Math.round(Number(m[1]) * factor);
88
- const g = Math.round(Number(m[2]) * factor);
89
- const b = Math.round(Number(m[3]) * factor);
90
- return `\x1b[38;2;${r};${g};${b}m`;
91
- }
92
-
93
38
  type BlockRow =
94
39
  | { kind: "bar"; leftChar: string; rightChar: string; label?: string; meta?: string }
95
40
  | { kind: "bottom"; leftChar: string; rightChar: string }
@@ -135,8 +80,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
135
80
  const contentWidth = Math.max(0, lineWidth - visibleWidth(v) - contentPaddingLeft - visibleWidth(v));
136
81
  const contentLeftPadding = contentPaddingLeft > 0 ? padding(contentPaddingLeft) : "";
137
82
 
138
- // ── Layout pass: collect row descriptors so the border perimeter length is
139
- // known before the moving segment is positioned. ──
83
+ // ── Layout pass: collect row descriptors before emitting the bordered lines. ──
140
84
  const rows: BlockRow[] = [];
141
85
  rows.push({
142
86
  kind: "bar",
@@ -185,39 +129,6 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
185
129
  rows.push({ kind: "bottom", leftChar: theme.boxSharp.bottomLeft, rightChar: theme.boxSharp.bottomRight });
186
130
 
187
131
  const H = rows.length;
188
- const W = lineWidth;
189
- const animate = (options.animate ?? false) && (state === "running" || state === "pending") && W >= 2 && H >= 2;
190
-
191
- // ── Segment geometry: one dark run bounces left ↔ right along the bottom
192
- // edge only. The top, interior separators, and side borders stay the flat
193
- // accent color. ──
194
- const segLen = animate ? Math.min(BORDER_SEGMENT_LEN, W) : 0;
195
- const head = animate ? borderSegmentHeadCol(W, Date.now()) : 0;
196
- const segHalf = segLen / 2;
197
- const segAnsi = animate ? (darkenFgAnsi(theme.getFgAnsi(borderColor), 0.4) ?? theme.getFgAnsi("borderMuted")) : "";
198
- const seg = (text: string) => `${segAnsi}${text}\x1b[39m`;
199
-
200
- // A bottom-edge column is lit when it lies within half a segment of the
201
- // travelling center.
202
- const isLit = (col: number): boolean => Math.abs(col - head) < segHalf;
203
- // Color a run of bottom-edge glyphs starting at column `startCol`, grouping
204
- // consecutive same-state cells so each run emits a single escape pair.
205
- const colorEdge = (glyphs: string, startCol: number): string => {
206
- let out = "";
207
- let runLit: boolean | null = null;
208
- let buf = "";
209
- for (let i = 0; i < glyphs.length; i++) {
210
- const lit = isLit(startCol + i);
211
- if (lit !== runLit) {
212
- if (runLit !== null) out += (runLit ? seg : border)(buf);
213
- buf = "";
214
- runLit = lit;
215
- }
216
- buf += glyphs[i];
217
- }
218
- if (runLit !== null) out += (runLit ? seg : border)(buf);
219
- return out;
220
- };
221
132
 
222
133
  const renderBar = (row: { leftChar: string; rightChar: string; label?: string; meta?: string }): string => {
223
134
  const leftGlyphs = `${row.leftChar}${cap}`;
@@ -245,11 +156,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
245
156
  const rightGlyph = row.rightChar;
246
157
  const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
247
158
  const fillGlyphs = h.repeat(fillCount);
248
- if (!animate) return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
249
- const leftStr = colorEdge(leftGlyphs, 0);
250
- const fillStr = colorEdge(fillGlyphs, visibleWidth(leftGlyphs));
251
- const rightStr = colorEdge(rightGlyph, lineWidth - visibleWidth(rightGlyph));
252
- return `${leftStr}${fillStr}${rightStr}`;
159
+ return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
253
160
  };
254
161
 
255
162
  const renderContent = (inner: string): string => `${border(v)}${contentLeftPadding}${inner}${border(v)}`;
@@ -302,8 +209,6 @@ export class CachedOutputBlock {
302
209
  h.optional(options.state);
303
210
  h.optional(options.borderColor);
304
211
  h.bool(options.applyBg ?? true);
305
- h.bool(options.animate ?? false);
306
- if (options.animate) h.u32(borderShimmerTick());
307
212
  if (options.sections) {
308
213
  for (const s of options.sections) {
309
214
  h.optional(s.label);
@@ -33,7 +33,7 @@ const setTitleTool: Tool = {
33
33
  title: {
34
34
  type: "string",
35
35
  description:
36
- 'A concise 3-6 word title for the session, or exactly "none" when the message carries no concrete task yet (greeting, small talk, vague).',
36
+ 'A concise, sentence-case 3-7 word title for the session (capitalize only the first word and proper nouns), or exactly "none" when the message carries no concrete task yet (greeting, small talk, vague).',
37
37
  },
38
38
  },
39
39
  required: ["title"],
@@ -224,7 +224,7 @@ export async function generateTitleOnline(
224
224
  // account_uuid rather than the snapshot-at-call-site value.
225
225
  const metadata = metadataResolver?.(model.provider);
226
226
 
227
- // Title generation is a 3-6 word task, but some reasoning backends ignore
227
+ // Title generation is a 3-7 word task, but some reasoning backends ignore
228
228
  // disableReasoning. Keep the normal cheap budget for non-reasoning models
229
229
  // while reserving enough output room for reasoning models to still emit
230
230
  // the forced tool call after any unavoidable thinking tokens.