@oh-my-pi/pi-coding-agent 14.2.1 → 14.4.0

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 (137) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/package.json +19 -19
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/cli/args.ts +10 -1
  5. package/src/cli/shell-cli.ts +15 -3
  6. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  7. package/src/config/model-registry.ts +67 -15
  8. package/src/config/prompt-templates.ts +5 -5
  9. package/src/config/settings-schema.ts +63 -4
  10. package/src/cursor.ts +3 -8
  11. package/src/debug/system-info.ts +6 -2
  12. package/src/discovery/claude.ts +58 -36
  13. package/src/discovery/helpers.ts +3 -3
  14. package/src/discovery/opencode.ts +20 -2
  15. package/src/edit/diff.ts +50 -47
  16. package/src/edit/index.ts +87 -57
  17. package/src/edit/line-hash.ts +735 -19
  18. package/src/edit/modes/apply-patch.ts +0 -9
  19. package/src/edit/modes/atom.ts +658 -0
  20. package/src/edit/modes/chunk.ts +144 -78
  21. package/src/edit/modes/hashline.ts +223 -146
  22. package/src/edit/modes/patch.ts +5 -9
  23. package/src/edit/modes/replace.ts +6 -11
  24. package/src/edit/renderer.ts +112 -143
  25. package/src/edit/streaming.ts +385 -0
  26. package/src/exec/bash-executor.ts +58 -5
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +4 -12
  29. package/src/extensibility/custom-tools/types.ts +2 -0
  30. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  31. package/src/internal-urls/docs-index.generated.ts +7 -7
  32. package/src/internal-urls/pi-protocol.ts +0 -2
  33. package/src/lsp/client.ts +8 -1
  34. package/src/lsp/defaults.json +2 -1
  35. package/src/lsp/index.ts +1 -1
  36. package/src/mcp/render.ts +1 -8
  37. package/src/modes/acp/acp-agent.ts +76 -2
  38. package/src/modes/components/assistant-message.ts +5 -34
  39. package/src/modes/components/diff.ts +23 -14
  40. package/src/modes/components/footer.ts +21 -16
  41. package/src/modes/components/hook-editor.ts +1 -1
  42. package/src/modes/components/settings-defs.ts +6 -1
  43. package/src/modes/components/todo-reminder.ts +1 -8
  44. package/src/modes/components/tool-execution.ts +112 -105
  45. package/src/modes/controllers/input-controller.ts +1 -1
  46. package/src/modes/controllers/selector-controller.ts +1 -1
  47. package/src/modes/interactive-mode.ts +0 -2
  48. package/src/modes/print-mode.ts +8 -0
  49. package/src/modes/theme/mermaid-cache.ts +13 -52
  50. package/src/modes/theme/theme.ts +2 -2
  51. package/src/prompts/agents/librarian.md +1 -1
  52. package/src/prompts/agents/reviewer.md +4 -4
  53. package/src/prompts/ci-green-request.md +1 -1
  54. package/src/prompts/review-request.md +1 -1
  55. package/src/prompts/system/subagent-system-prompt.md +3 -3
  56. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  57. package/src/prompts/system/system-prompt.md +4 -1
  58. package/src/prompts/tools/ask.md +3 -2
  59. package/src/prompts/tools/ast-edit.md +15 -19
  60. package/src/prompts/tools/ast-grep.md +18 -24
  61. package/src/prompts/tools/atom.md +96 -0
  62. package/src/prompts/tools/browser.md +1 -0
  63. package/src/prompts/tools/chunk-edit.md +58 -179
  64. package/src/prompts/tools/debug.md +4 -5
  65. package/src/prompts/tools/exit-plan-mode.md +4 -5
  66. package/src/prompts/tools/find.md +4 -8
  67. package/src/prompts/tools/github.md +18 -0
  68. package/src/prompts/tools/grep.md +8 -8
  69. package/src/prompts/tools/hashline.md +22 -89
  70. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  71. package/src/prompts/tools/inspect-image.md +6 -6
  72. package/src/prompts/tools/lsp.md +6 -0
  73. package/src/prompts/tools/patch.md +12 -19
  74. package/src/prompts/tools/python.md +3 -2
  75. package/src/prompts/tools/read-chunk.md +46 -8
  76. package/src/prompts/tools/read.md +9 -6
  77. package/src/prompts/tools/ssh.md +8 -17
  78. package/src/prompts/tools/todo-write.md +54 -41
  79. package/src/sdk.ts +22 -14
  80. package/src/session/agent-session.ts +61 -22
  81. package/src/session/session-manager.ts +228 -57
  82. package/src/session/streaming-output.ts +11 -0
  83. package/src/system-prompt.ts +7 -2
  84. package/src/task/executor.ts +44 -48
  85. package/src/task/render.ts +11 -13
  86. package/src/tools/ask.ts +7 -7
  87. package/src/tools/ast-edit.ts +45 -41
  88. package/src/tools/ast-grep.ts +77 -85
  89. package/src/tools/bash.ts +21 -9
  90. package/src/tools/browser.ts +32 -30
  91. package/src/tools/calculator.ts +4 -4
  92. package/src/tools/cancel-job.ts +1 -1
  93. package/src/tools/checkpoint.ts +2 -2
  94. package/src/tools/debug.ts +41 -37
  95. package/src/tools/exit-plan-mode.ts +1 -1
  96. package/src/tools/find.ts +4 -4
  97. package/src/tools/gh-renderer.ts +12 -4
  98. package/src/tools/gh.ts +514 -712
  99. package/src/tools/grep.ts +115 -130
  100. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  101. package/src/tools/index.ts +14 -32
  102. package/src/tools/inspect-image.ts +3 -3
  103. package/src/tools/json-tree.ts +114 -114
  104. package/src/tools/match-line-format.ts +9 -8
  105. package/src/tools/notebook.ts +8 -7
  106. package/src/tools/poll-tool.ts +2 -1
  107. package/src/tools/python.ts +9 -23
  108. package/src/tools/read.ts +32 -21
  109. package/src/tools/render-mermaid.ts +1 -1
  110. package/src/tools/render-utils.ts +18 -0
  111. package/src/tools/renderers.ts +2 -2
  112. package/src/tools/report-tool-issue.ts +3 -2
  113. package/src/tools/resolve.ts +1 -1
  114. package/src/tools/review.ts +12 -10
  115. package/src/tools/search-tool-bm25.ts +2 -4
  116. package/src/tools/sqlite-reader.ts +116 -3
  117. package/src/tools/ssh.ts +4 -4
  118. package/src/tools/todo-write.ts +172 -147
  119. package/src/tools/vim.ts +14 -15
  120. package/src/tools/write.ts +4 -4
  121. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  122. package/src/utils/edit-mode.ts +2 -1
  123. package/src/utils/file-display-mode.ts +10 -5
  124. package/src/utils/git.ts +9 -5
  125. package/src/utils/shell-snapshot.ts +2 -3
  126. package/src/vim/render.ts +4 -4
  127. package/src/web/search/providers/codex.ts +129 -6
  128. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  129. package/src/prompts/tools/gh-issue-view.md +0 -11
  130. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  131. package/src/prompts/tools/gh-pr-diff.md +0 -12
  132. package/src/prompts/tools/gh-pr-push.md +0 -11
  133. package/src/prompts/tools/gh-pr-view.md +0 -11
  134. package/src/prompts/tools/gh-repo-view.md +0 -11
  135. package/src/prompts/tools/gh-run-watch.md +0 -12
  136. package/src/prompts/tools/gh-search-issues.md +0 -11
  137. package/src/prompts/tools/gh-search-prs.md +0 -11
@@ -45,7 +45,6 @@ export class PiProtocolHandler implements ProtocolHandler {
45
45
  content,
46
46
  contentType: "text/markdown",
47
47
  size: Buffer.byteLength(content, "utf-8"),
48
- sourcePath: "pi://",
49
48
  };
50
49
  }
51
50
 
@@ -78,7 +77,6 @@ export class PiProtocolHandler implements ProtocolHandler {
78
77
  content,
79
78
  contentType: "text/markdown",
80
79
  size: Buffer.byteLength(content, "utf-8"),
81
- sourcePath: `pi://${normalized}`,
82
80
  };
83
81
  }
84
82
  }
package/src/lsp/client.ts CHANGED
@@ -484,7 +484,14 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
484
484
 
485
485
  // Reject any pending requests — the server is gone, they will never complete.
486
486
  if (client.pendingRequests.size > 0) {
487
- const stderr = proc.peekStderr().trim();
487
+ // Strip informational log lines (e.g. marksman's [INF]/[DBG] prefix)
488
+ // — they are startup noise, not actionable errors.
489
+ const rawStderr = proc.peekStderr().trim();
490
+ const stderr = rawStderr
491
+ .split("\n")
492
+ .filter(line => !/^\[\d{2}:\d{2}:\d{2} (?:INF|DBG|VRB)\]/.test(line))
493
+ .join("\n")
494
+ .trim();
488
495
  const code = proc.exitCode;
489
496
  const err = new Error(
490
497
  stderr ? `LSP server exited (code ${code}): ${stderr}` : `LSP server exited unexpectedly (code ${code})`,
@@ -857,7 +857,8 @@
857
857
  "rootMarkers": [
858
858
  ".marksman.toml",
859
859
  ".git"
860
- ]
860
+ ],
861
+ "warmupTimeoutMs": 15000
861
862
  },
862
863
  "texlab": {
863
864
  "command": "texlab",
package/src/lsp/index.ts CHANGED
@@ -1115,11 +1115,11 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1115
1115
  readonly label = "LSP";
1116
1116
  readonly description: string;
1117
1117
  readonly parameters = lspSchema;
1118
- readonly strict = true;
1119
1118
  readonly renderCall = renderCall;
1120
1119
  readonly renderResult = renderResult;
1121
1120
  readonly mergeCallAndResult = true;
1122
1121
  readonly inline = true;
1122
+ readonly strict = true;
1123
1123
 
1124
1124
  constructor(private readonly session: ToolSession) {
1125
1125
  this.description = prompt.render(lspDescription);
package/src/mcp/render.ts CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  JSON_TREE_SCALAR_LEN_COLLAPSED,
18
18
  JSON_TREE_SCALAR_LEN_EXPANDED,
19
19
  renderJsonTreeLines,
20
- stripInternalArgs,
21
20
  } from "../tools/json-tree";
22
21
  import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
23
22
  import { renderStatusLine } from "../tui";
@@ -58,13 +57,7 @@ export function renderMCPResult(
58
57
  lines.push(`${theme.fg("dim", "Args")}`);
59
58
  const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
60
59
  const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
61
- const tree = renderJsonTreeLines(
62
- stripInternalArgs(args),
63
- theme,
64
- maxDepth,
65
- maxLines,
66
- JSON_TREE_SCALAR_LEN_EXPANDED,
67
- );
60
+ const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
68
61
  for (const line of tree.lines) {
69
62
  lines.push(line);
70
63
  }
@@ -39,11 +39,14 @@ import {
39
39
  } from "@agentclientprotocol/sdk";
40
40
  import type { Model } from "@oh-my-pi/pi-ai";
41
41
  import { logger, VERSION } from "@oh-my-pi/pi-utils";
42
+ import { disableProvider, enableProvider } from "../../capability";
43
+ import { Settings } from "../../config/settings";
42
44
  import type { ExtensionUIContext } from "../../extensibility/extensions";
43
45
  import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
44
46
  import { loadSlashCommands } from "../../extensibility/slash-commands";
45
47
  import { MCPManager } from "../../mcp/manager";
46
48
  import type { MCPServerConfig } from "../../mcp/types";
49
+ import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
47
50
  import { theme } from "../../modes/theme/theme";
48
51
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
49
52
  import {
@@ -379,8 +382,79 @@ export class AcpAgent implements Agent {
379
382
  }
380
383
  }
381
384
 
382
- async extMethod(_method: string, _params: { [key: string]: unknown }): Promise<{ [key: string]: unknown }> {
383
- throw new Error("ACP extension methods are not implemented");
385
+ async extMethod(method: string, params: { [key: string]: unknown }): Promise<{ [key: string]: unknown }> {
386
+ switch (method) {
387
+ case "omp/sessions/listAll": {
388
+ const limit = typeof params.limit === "number" ? Math.max(1, Math.min(5000, params.limit as number)) : 1000;
389
+ const sessions = await SessionManager.listAll();
390
+ const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
391
+ return {
392
+ sessions: sorted.map(s => this.#toSessionInfo(s)),
393
+ total: sessions.length,
394
+ };
395
+ }
396
+ case "omp/projects/list": {
397
+ const sessions = await SessionManager.listAll();
398
+ const buckets = new Map<
399
+ string,
400
+ { cwd: string; sessionCount: number; lastActivityAt: number; lastTitle: string }
401
+ >();
402
+ for (const s of sessions) {
403
+ if (!s.cwd) continue;
404
+ const ts = s.modified.getTime();
405
+ const existing = buckets.get(s.cwd);
406
+ if (existing) {
407
+ existing.sessionCount += 1;
408
+ if (ts > existing.lastActivityAt) {
409
+ existing.lastActivityAt = ts;
410
+ existing.lastTitle = s.title ?? "";
411
+ }
412
+ } else {
413
+ buckets.set(s.cwd, {
414
+ cwd: s.cwd,
415
+ sessionCount: 1,
416
+ lastActivityAt: ts,
417
+ lastTitle: s.title ?? "",
418
+ });
419
+ }
420
+ }
421
+ const projects = Array.from(buckets.values()).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
422
+ return { projects, totalSessions: sessions.length };
423
+ }
424
+ case "omp/chats/byCwd": {
425
+ const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
426
+ if (!cwd) throw new Error("cwd required");
427
+ const limit = typeof params.limit === "number" ? Math.max(1, Math.min(500, params.limit as number)) : 100;
428
+ const sessions = await SessionManager.list(cwd);
429
+ const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
430
+ return { sessions: sorted.map(s => this.#toSessionInfo(s)) };
431
+ }
432
+ case "omp/usage": {
433
+ const [firstRecord] = this.#sessions.values();
434
+ const target = firstRecord?.session ?? this.#initialSession;
435
+ const reports = await target.fetchUsageReports();
436
+ return { reports: reports ?? [] };
437
+ }
438
+ case "omp/extensions": {
439
+ const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
440
+ const sm = await Settings.init();
441
+ const disabledIds = (sm.get("disabledExtensions") as string[] | undefined) ?? [];
442
+ const extensions = await loadAllExtensions(cwd, disabledIds);
443
+ return { extensions: extensions as unknown as Array<{ [key: string]: unknown }> };
444
+ }
445
+ case "omp/extensions/toggle": {
446
+ const providerId = params.providerId;
447
+ if (typeof providerId !== "string") throw new Error("providerId required");
448
+ if (params.enabled === false) {
449
+ disableProvider(providerId);
450
+ return { enabled: false };
451
+ }
452
+ enableProvider(providerId);
453
+ return { enabled: true };
454
+ }
455
+ default:
456
+ throw new Error(`Unknown ACP ext method: ${method}`);
457
+ }
384
458
  }
385
459
 
386
460
  async extNotification(_method: string, _params: { [key: string]: unknown }): Promise<void> {}
@@ -1,8 +1,7 @@
1
1
  import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
2
2
  import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
3
- import { formatNumber, logger } from "@oh-my-pi/pi-utils";
3
+ import { formatNumber } from "@oh-my-pi/pi-utils";
4
4
  import { settings } from "../../config/settings";
5
- import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache";
6
5
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
6
  import { resolveImageOptions } from "../../tools/render-utils";
8
7
 
@@ -12,7 +11,6 @@ import { resolveImageOptions } from "../../tools/render-utils";
12
11
  export class AssistantMessageComponent extends Container {
13
12
  #contentContainer: Container;
14
13
  #lastMessage?: AssistantMessage;
15
- #prerenderInFlight = false;
16
14
  #toolImagesByCallId = new Map<string, ImageContent[]>();
17
15
  #usageInfo?: Usage;
18
16
 
@@ -85,34 +83,6 @@ export class AssistantMessageComponent extends Container {
85
83
  this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
86
84
  }
87
85
  }
88
- #triggerMermaidPrerender(message: AssistantMessage): void {
89
- if (!TERMINAL.imageProtocol || this.#prerenderInFlight) return;
90
-
91
- // Check if any text content has pending mermaid blocks
92
- const hasPending = message.content.some(c => c.type === "text" && c.text.trim() && hasPendingMermaid(c.text));
93
- if (!hasPending) return;
94
-
95
- this.#prerenderInFlight = true;
96
-
97
- // Fire off background prerender
98
- void (async () => {
99
- try {
100
- for (const content of message.content) {
101
- if (content.type === "text" && content.text.trim() && hasPendingMermaid(content.text)) {
102
- prerenderMermaid(content.text);
103
- }
104
- }
105
- } catch (error) {
106
- logger.warn("Background mermaid prerender failed", {
107
- error: error instanceof Error ? error.message : String(error),
108
- });
109
- } finally {
110
- this.#prerenderInFlight = false;
111
- // Invalidate to re-render with cached images
112
- this.invalidate();
113
- }
114
- })();
115
- }
116
86
 
117
87
  updateContent(message: AssistantMessage): void {
118
88
  this.#lastMessage = message;
@@ -120,9 +90,6 @@ export class AssistantMessageComponent extends Container {
120
90
  // Clear content container
121
91
  this.#contentContainer.clear();
122
92
 
123
- // Trigger background mermaid pre-rendering if needed
124
- this.#triggerMermaidPrerender(message);
125
-
126
93
  const hasVisibleContent = message.content.some(
127
94
  c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
128
95
  );
@@ -188,6 +155,10 @@ export class AssistantMessageComponent extends Container {
188
155
  this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
189
156
  }
190
157
  }
158
+ if (message.errorMessage && message.stopReason !== "aborted" && message.stopReason !== "error") {
159
+ this.#contentContainer.addChild(new Spacer(1));
160
+ this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
161
+ }
191
162
 
192
163
  // Token usage metadata
193
164
  if (settings.get("display.showTokenUsage") && this.#usageInfo) {
@@ -1,7 +1,7 @@
1
1
  import { getIndentation } from "@oh-my-pi/pi-utils";
2
2
  import * as Diff from "diff";
3
3
  import { theme } from "../../modes/theme/theme";
4
- import { replaceTabs } from "../../tools/render-utils";
4
+ import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
5
5
 
6
6
  /** SGR dim on / normal intensity — additive, preserves fg/bg colors. */
7
7
  const DIM = "\x1b[2m";
@@ -37,14 +37,14 @@ function visualizeIndent(text: string, filePath?: string): string {
37
37
  * Parse diff line to extract prefix, line number, and content.
38
38
  * Supported formats: "+123|content" (canonical) and "+123 content" (legacy).
39
39
  */
40
- function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null {
40
+ function parseDiffLine(line: string): { prefix: CodeFrameMarker; lineNum: string; content: string } | null {
41
41
  const canonical = line.match(/^([+-\s])(\s*\d+)\|(.*)$/);
42
42
  if (canonical) {
43
- return { prefix: canonical[1], lineNum: canonical[2], content: canonical[3] };
43
+ return { prefix: canonical[1] as CodeFrameMarker, lineNum: canonical[2] ?? "", content: canonical[3] ?? "" };
44
44
  }
45
45
  const legacy = line.match(/^([+-\s])(?:(\s*\d+)\s)?(.*)$/);
46
46
  if (!legacy) return null;
47
- return { prefix: legacy[1], lineNum: legacy[2] ?? "", content: legacy[3] };
47
+ return { prefix: legacy[1] as CodeFrameMarker, lineNum: legacy[2] ?? "", content: legacy[3] ?? "" };
48
48
  }
49
49
 
50
50
  /**
@@ -108,12 +108,27 @@ export interface RenderDiffOptions {
108
108
  export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
109
109
  const lines = diffText.split("\n");
110
110
  const result: string[] = [];
111
-
112
- const formatLine = (prefix: string, lineNum: string, content: string): string => {
111
+ const parsedLines = lines.map(parseDiffLine);
112
+ const lineNumberWidth = parsedLines.reduce((width, parsed) => {
113
+ const lineNumber = parsed?.lineNum.trim() ?? "";
114
+ return Math.max(width, lineNumber.length);
115
+ }, 0);
116
+
117
+ // Track the line number rendered on the previous emitted line so we can
118
+ // blank out duplicate gutters. Two cases trigger this:
119
+ // 1. Single-line replacement (`-N` followed by `+N`) — the `+N` repeats `N`.
120
+ // 2. Insertion followed by context (`+N` then ` N` if producer used oldLine).
121
+ let prevLineNum = "";
122
+
123
+ const formatLine = (prefix: CodeFrameMarker, lineNum: string, content: string): string => {
113
124
  if (lineNum.trim().length === 0) {
125
+ prevLineNum = "";
114
126
  return `${prefix}${content}`;
115
127
  }
116
- return `${prefix}${lineNum}|${content}`;
128
+ const trimmed = lineNum.trim();
129
+ const displayNum = trimmed === prevLineNum ? "" : trimmed;
130
+ prevLineNum = trimmed;
131
+ return formatCodeFrameLine(prefix, displayNum, content, lineNumberWidth);
117
132
  };
118
133
 
119
134
  let i = 0;
@@ -122,13 +137,13 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
122
137
  const parsed = parseDiffLine(line);
123
138
 
124
139
  if (!parsed) {
140
+ prevLineNum = "";
125
141
  result.push(theme.fg("toolDiffContext", line));
126
142
  i++;
127
143
  continue;
128
144
  }
129
145
 
130
146
  if (parsed.prefix === "-") {
131
- // Collect consecutive removed lines
132
147
  const removedLines: { lineNum: string; content: string }[] = [];
133
148
  while (i < lines.length) {
134
149
  const p = parseDiffLine(lines[i]);
@@ -137,7 +152,6 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
137
152
  i++;
138
153
  }
139
154
 
140
- // Collect consecutive added lines
141
155
  const addedLines: { lineNum: string; content: string }[] = [];
142
156
  while (i < lines.length) {
143
157
  const p = parseDiffLine(lines[i]);
@@ -146,8 +160,6 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
146
160
  i++;
147
161
  }
148
162
 
149
- // Only do intra-line diffing when there's exactly one removed and one added line
150
- // (indicating a single line modification). Otherwise, show lines as-is.
151
163
  if (removedLines.length === 1 && addedLines.length === 1) {
152
164
  const removed = removedLines[0];
153
165
  const added = addedLines[0];
@@ -167,7 +179,6 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
167
179
  theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine, options.filePath))),
168
180
  );
169
181
  } else {
170
- // Show all removed lines first, then all added lines
171
182
  for (const removed of removedLines) {
172
183
  result.push(
173
184
  theme.fg(
@@ -186,7 +197,6 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
186
197
  }
187
198
  }
188
199
  } else if (parsed.prefix === "+") {
189
- // Standalone added line
190
200
  result.push(
191
201
  theme.fg(
192
202
  "toolDiffAdded",
@@ -195,7 +205,6 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
195
205
  );
196
206
  i++;
197
207
  } else {
198
- // Context line
199
208
  result.push(
200
209
  theme.fg(
201
210
  "toolDiffContext",
@@ -56,22 +56,27 @@ export class FooterComponent implements Component {
56
56
  this.#gitWatcher = null;
57
57
  }
58
58
 
59
- git.head.resolve(getProjectDir()).then(head => {
60
- if (!head) {
61
- return;
62
- }
63
-
64
- try {
65
- this.#gitWatcher = fs.watch(head.headPath, () => {
66
- this.#cachedBranch = undefined; // Invalidate cache
67
- if (this.#onBranchChange) {
68
- this.#onBranchChange();
69
- }
70
- });
71
- } catch {
72
- // Silently fail if we can't watch
73
- }
74
- });
59
+ void git.head
60
+ .resolve(getProjectDir())
61
+ .then(head => {
62
+ if (!head) {
63
+ return;
64
+ }
65
+
66
+ try {
67
+ this.#gitWatcher = fs.watch(head.headPath, () => {
68
+ this.#cachedBranch = undefined; // Invalidate cache
69
+ if (this.#onBranchChange) {
70
+ this.#onBranchChange();
71
+ }
72
+ });
73
+ } catch {
74
+ // Silently fail if we can't watch
75
+ }
76
+ })
77
+ .catch(() => {
78
+ this.#cachedBranch = null;
79
+ });
75
80
  }
76
81
 
77
82
  /**
@@ -136,7 +136,7 @@ export class HookEditorComponent extends Container {
136
136
  const editorCmd = getEditorCommand();
137
137
  if (!editorCmd) return;
138
138
 
139
- const currentText = this.#editor.getText();
139
+ const currentText = this.#editor.getExpandedText();
140
140
  try {
141
141
  this.#tui.stop();
142
142
  const result = await openInEditor(editorCmd, currentText);
@@ -355,7 +355,12 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
355
355
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
356
356
  ],
357
357
  "providers.image": [
358
- { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
358
+ {
359
+ value: "auto",
360
+ label: "Auto",
361
+ description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
362
+ },
363
+ { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
359
364
  { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
360
365
  { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
361
366
  ],
@@ -34,14 +34,7 @@ export class TodoReminderComponent extends Container {
34
34
  this.#box.addChild(new Text(header, 0, 0));
35
35
  this.#box.addChild(new Spacer(1));
36
36
 
37
- const todoList = this.todos
38
- .map(t => {
39
- const line = ` ${theme.checkbox.unchecked} ${t.content}`;
40
- if (!t.details) return line;
41
- const detailLines = t.details.split("\n").map(l => ` ${l}`);
42
- return [line, ...detailLines].join("\n");
43
- })
44
- .join("\n");
37
+ const todoList = this.todos.map(todo => ` ${theme.checkbox.unchecked} ${todo.content}`).join("\n");
45
38
  this.#box.addChild(new Text(theme.italic(todoList), 0, 0));
46
39
  }
47
40
  }