@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.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 (148) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/docs/theme.md +38 -5
  3. package/examples/sdk/11-sessions.ts +2 -2
  4. package/package.json +7 -4
  5. package/src/cli/file-processor.ts +51 -2
  6. package/src/cli/plugin-cli.ts +25 -19
  7. package/src/cli/update-cli.ts +4 -3
  8. package/src/core/agent-session.ts +31 -4
  9. package/src/core/compaction/branch-summarization.ts +4 -32
  10. package/src/core/compaction/compaction.ts +6 -84
  11. package/src/core/compaction/utils.ts +2 -3
  12. package/src/core/custom-tools/types.ts +2 -0
  13. package/src/core/export-html/index.ts +1 -1
  14. package/src/core/hooks/tool-wrapper.ts +0 -1
  15. package/src/core/hooks/types.ts +2 -2
  16. package/src/core/plugins/doctor.ts +9 -1
  17. package/src/core/sdk.ts +2 -1
  18. package/src/core/session-manager.ts +518 -40
  19. package/src/core/settings-manager.ts +174 -0
  20. package/src/core/system-prompt.ts +9 -14
  21. package/src/core/title-generator.ts +2 -8
  22. package/src/core/tools/ask.ts +19 -37
  23. package/src/core/tools/bash.ts +2 -37
  24. package/src/core/tools/edit.ts +2 -9
  25. package/src/core/tools/exa/render.ts +52 -48
  26. package/src/core/tools/find.ts +10 -8
  27. package/src/core/tools/grep.ts +45 -17
  28. package/src/core/tools/ls.ts +22 -2
  29. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  30. package/src/core/tools/lsp/clients/index.ts +49 -0
  31. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  32. package/src/core/tools/lsp/config.ts +3 -0
  33. package/src/core/tools/lsp/index.ts +107 -55
  34. package/src/core/tools/lsp/render.ts +192 -79
  35. package/src/core/tools/lsp/types.ts +27 -0
  36. package/src/core/tools/lsp/utils.ts +62 -22
  37. package/src/core/tools/notebook.ts +9 -1
  38. package/src/core/tools/output.ts +37 -14
  39. package/src/core/tools/read.ts +349 -34
  40. package/src/core/tools/renderers.ts +290 -89
  41. package/src/core/tools/review.ts +12 -5
  42. package/src/core/tools/task/agents.ts +5 -5
  43. package/src/core/tools/task/commands.ts +3 -3
  44. package/src/core/tools/task/executor.ts +33 -1
  45. package/src/core/tools/task/index.ts +93 -6
  46. package/src/core/tools/task/render.ts +147 -66
  47. package/src/core/tools/task/types.ts +14 -9
  48. package/src/core/tools/web-fetch.ts +242 -103
  49. package/src/core/tools/web-search/index.ts +64 -20
  50. package/src/core/tools/web-search/providers/exa.ts +68 -172
  51. package/src/core/tools/web-search/render.ts +264 -74
  52. package/src/core/tools/write.ts +2 -8
  53. package/src/main.ts +10 -6
  54. package/src/modes/cleanup.ts +23 -0
  55. package/src/modes/index.ts +9 -4
  56. package/src/modes/interactive/components/bash-execution.ts +6 -3
  57. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  58. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  60. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  61. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  62. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  63. package/src/modes/interactive/components/hook-message.ts +2 -2
  64. package/src/modes/interactive/components/hook-selector.ts +1 -1
  65. package/src/modes/interactive/components/model-selector.ts +22 -9
  66. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  67. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  68. package/src/modes/interactive/components/session-selector.ts +9 -6
  69. package/src/modes/interactive/components/settings-defs.ts +285 -1
  70. package/src/modes/interactive/components/settings-selector.ts +176 -3
  71. package/src/modes/interactive/components/status-line/index.ts +4 -0
  72. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  73. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  74. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  75. package/src/modes/interactive/components/status-line/types.ts +81 -0
  76. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  77. package/src/modes/interactive/components/status-line.ts +170 -223
  78. package/src/modes/interactive/components/tool-execution.ts +446 -211
  79. package/src/modes/interactive/components/tree-selector.ts +17 -6
  80. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  81. package/src/modes/interactive/components/welcome.ts +27 -19
  82. package/src/modes/interactive/interactive-mode.ts +98 -13
  83. package/src/modes/interactive/theme/dark.json +3 -2
  84. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  85. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  86. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  87. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  88. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  89. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  90. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  91. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  92. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  93. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  94. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  95. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  96. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  97. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  98. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  99. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  100. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  101. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  102. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  103. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  104. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  105. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  106. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  107. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  108. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  109. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  110. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  111. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  112. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  113. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  114. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  115. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  116. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  117. package/src/modes/interactive/theme/light.json +3 -2
  118. package/src/modes/interactive/theme/theme-schema.json +120 -4
  119. package/src/modes/interactive/theme/theme.ts +1228 -14
  120. package/src/prompts/branch-summary-preamble.md +3 -0
  121. package/src/prompts/branch-summary.md +28 -0
  122. package/src/prompts/compaction-summary.md +34 -0
  123. package/src/prompts/compaction-turn-prefix.md +16 -0
  124. package/src/prompts/compaction-update-summary.md +41 -0
  125. package/src/prompts/init.md +30 -0
  126. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  127. package/src/prompts/summarization-system.md +3 -0
  128. package/src/prompts/system-prompt.md +27 -0
  129. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  130. package/src/prompts/title-system.md +8 -0
  131. package/src/prompts/tools/ask.md +24 -0
  132. package/src/prompts/tools/bash.md +23 -0
  133. package/src/prompts/tools/edit.md +9 -0
  134. package/src/prompts/tools/find.md +6 -0
  135. package/src/prompts/tools/grep.md +12 -0
  136. package/src/prompts/tools/lsp.md +14 -0
  137. package/src/prompts/tools/output.md +23 -0
  138. package/src/prompts/tools/read.md +25 -0
  139. package/src/prompts/tools/web-fetch.md +8 -0
  140. package/src/prompts/tools/web-search.md +10 -0
  141. package/src/prompts/tools/write.md +10 -0
  142. package/src/commands/init.md +0 -20
  143. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  144. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  145. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  146. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  147. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  148. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
@@ -14,14 +14,6 @@ import { highlight, supportsLanguage } from "cli-highlight";
14
14
  import type { Theme } from "../../../modes/interactive/theme/theme";
15
15
  import type { LspParams, LspToolDetails } from "./types";
16
16
 
17
- // =============================================================================
18
- // Tree Drawing Characters
19
- // =============================================================================
20
-
21
- const TREE_MID = "├─";
22
- const TREE_END = "└─";
23
- const TREE_PIPE = "│";
24
-
25
17
  // =============================================================================
26
18
  // Call Rendering
27
19
  // =============================================================================
@@ -75,7 +67,7 @@ export function renderResult(
75
67
 
76
68
  const errorMatch = text.match(/(\d+)\s+error\(s\)/);
77
69
  const warningMatch = text.match(/(\d+)\s+warning\(s\)/);
78
- if (errorMatch || warningMatch || text.includes("✗")) {
70
+ if (errorMatch || warningMatch || text.includes(theme.status.error)) {
79
71
  return renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
80
72
  }
81
73
 
@@ -112,16 +104,20 @@ function renderHover(
112
104
  const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
113
105
 
114
106
  const codeLines = highlightCode(code, lang, theme);
115
- const icon = theme.fg("accent", "");
107
+ const icon = theme.styledSymbol("status.info", "accent");
116
108
  const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
117
109
 
118
110
  if (expanded) {
111
+ const h = theme.boxSharp.horizontal;
112
+ const v = theme.boxSharp.vertical;
113
+ const top = `${theme.boxSharp.topLeft}${h.repeat(3)}`;
114
+ const bottom = `${theme.boxSharp.bottomLeft}${h.repeat(3)}`;
119
115
  let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}`;
120
- output += `\n ${theme.fg("mdCodeBlockBorder", "┌───")}`;
116
+ output += `\n ${theme.fg("mdCodeBlockBorder", top)}`;
121
117
  for (const line of codeLines) {
122
- output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${line}`;
118
+ output += `\n ${theme.fg("mdCodeBlockBorder", v)} ${line}`;
123
119
  }
124
- output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
120
+ output += `\n ${theme.fg("mdCodeBlockBorder", bottom)}`;
125
121
  if (afterCode) {
126
122
  output += `\n ${theme.fg("muted", afterCode)}`;
127
123
  }
@@ -133,17 +129,25 @@ function renderHover(
133
129
  const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
134
130
 
135
131
  let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}${expandHint}`;
136
- output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${firstCodeLine}`;
132
+ const h = theme.boxSharp.horizontal;
133
+ const v = theme.boxSharp.vertical;
134
+ const bottom = `${theme.boxSharp.bottomLeft}${h.repeat(3)}`;
135
+ output += `\n ${theme.fg("mdCodeBlockBorder", v)} ${firstCodeLine}`;
137
136
 
138
137
  if (codeLines.length > 1) {
139
- output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${theme.fg("muted", `… ${codeLines.length - 1} more lines`)}`;
138
+ output += `\n ${theme.fg("mdCodeBlockBorder", v)} ${theme.fg(
139
+ "muted",
140
+ `${theme.format.ellipsis} ${codeLines.length - 1} more lines`,
141
+ )}`;
140
142
  }
141
143
 
142
144
  if (afterCode) {
143
- const docPreview = afterCode.length > 60 ? `${afterCode.slice(0, 60)}…` : afterCode;
144
- output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", docPreview)}`;
145
+ const ellipsis = theme.format.ellipsis;
146
+ const sliceLen = Math.max(0, 60 - ellipsis.length);
147
+ const docPreview = afterCode.length > 60 ? `${afterCode.slice(0, sliceLen)}${ellipsis}` : afterCode;
148
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", docPreview)}`;
145
149
  } else {
146
- output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
150
+ output += `\n ${theme.fg("mdCodeBlockBorder", bottom)}`;
147
151
  }
148
152
 
149
153
  return new Text(output, 0, 0);
@@ -196,23 +200,44 @@ function renderDiagnostics(
196
200
  const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
197
201
 
198
202
  const icon =
199
- errorCount > 0 ? theme.fg("error", "●") : warnCount > 0 ? theme.fg("warning", "●") : theme.fg("success", "●");
203
+ errorCount > 0
204
+ ? theme.styledSymbol("status.error", "error")
205
+ : warnCount > 0
206
+ ? theme.styledSymbol("status.warning", "warning")
207
+ : theme.styledSymbol("status.success", "success");
200
208
 
201
209
  const meta: string[] = [];
202
210
  if (errorCount > 0) meta.push(`${errorCount} error${errorCount !== 1 ? "s" : ""}`);
203
211
  if (warnCount > 0) meta.push(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`);
204
212
  if (meta.length === 0) meta.push("No issues");
205
213
 
206
- const diagLines = lines.filter((l) => l.includes("✗") || /:\d+:\d+/.test(l));
214
+ const diagLines = lines.filter((l) => l.includes(theme.status.error) || /:\d+:\d+/.test(l));
215
+ const parsedDiagnostics = diagLines
216
+ .map((line) => parseDiagnosticLine(line))
217
+ .filter((diag): diag is ParsedDiagnostic => diag !== null);
218
+ const fallbackDiagnostics: RawDiagnostic[] = diagLines.map((line) => ({ raw: line.trim() }));
207
219
 
208
220
  if (expanded) {
209
221
  let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}`;
210
- for (let i = 0; i < diagLines.length; i++) {
211
- const isLast = i === diagLines.length - 1;
212
- const branch = isLast ? TREE_END : TREE_MID;
213
- const line = diagLines[i].trim();
214
- const color = line.includes("[error]") ? "error" : line.includes("[warning]") ? "warning" : "dim";
215
- output += `\n ${theme.fg("dim", branch)} ${theme.fg(color, line)}`;
222
+ const items: DiagnosticItem[] = parsedDiagnostics.length > 0 ? parsedDiagnostics : fallbackDiagnostics;
223
+ for (let i = 0; i < items.length; i++) {
224
+ const item = items[i];
225
+ const isLast = i === items.length - 1;
226
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
227
+ const detailPrefix = isLast ? " " : `${theme.tree.vertical} `;
228
+ if ("raw" in item) {
229
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg("muted", item.raw)}`;
230
+ continue;
231
+ }
232
+ const severityColor = severityToColor(item.severity);
233
+ const location = `${item.file}:${item.line}:${item.col}`;
234
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg(
235
+ "dim",
236
+ `[${item.severity}]`,
237
+ )}`;
238
+ if (item.message) {
239
+ output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg("muted", trimTo(item.message, 120, theme))}`;
240
+ }
216
241
  }
217
242
  return new Text(output, 0, 0);
218
243
  }
@@ -221,14 +246,28 @@ function renderDiagnostics(
221
246
  const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
222
247
  let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}${expandHint}`;
223
248
 
224
- const previewLines = diagLines.length > 0 ? diagLines.slice(0, 4) : lines.slice(0, 4);
225
- for (let i = 0; i < previewLines.length; i++) {
226
- const isLast = i === previewLines.length - 1 && diagLines.length <= 4;
227
- const branch = isLast ? TREE_END : TREE_MID;
228
- output += `\n ${theme.fg("dim", branch)} ${previewLines[i].trim()}`;
249
+ const previewItems: DiagnosticItem[] =
250
+ parsedDiagnostics.length > 0 ? parsedDiagnostics.slice(0, 3) : fallbackDiagnostics.slice(0, 3);
251
+ const remaining =
252
+ (parsedDiagnostics.length > 0 ? parsedDiagnostics.length : fallbackDiagnostics.length) - previewItems.length;
253
+ for (let i = 0; i < previewItems.length; i++) {
254
+ const item = previewItems[i];
255
+ const isLast = i === previewItems.length - 1 && remaining <= 0;
256
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
257
+ if ("raw" in item) {
258
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg("muted", item.raw)}`;
259
+ continue;
260
+ }
261
+ const severityColor = severityToColor(item.severity);
262
+ const location = `${item.file}:${item.line}:${item.col}`;
263
+ const message = item.message ? ` ${theme.fg("muted", trimTo(item.message, 80, theme))}` : "";
264
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)}${message}`;
229
265
  }
230
- if (diagLines.length > 4) {
231
- output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${diagLines.length - 4} more`)}`;
266
+ if (remaining > 0) {
267
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
268
+ "muted",
269
+ `${theme.format.ellipsis} ${remaining} more`,
270
+ )}`;
232
271
  }
233
272
 
234
273
  return new Text(output, 0, 0);
@@ -243,7 +282,8 @@ function renderDiagnostics(
243
282
  */
244
283
  function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
245
284
  const refCount = Number.parseInt(refMatch[1], 10);
246
- const icon = refCount > 0 ? theme.fg("success", "●") : theme.fg("warning", "●");
285
+ const icon =
286
+ refCount > 0 ? theme.styledSymbol("status.success", "success") : theme.styledSymbol("status.warning", "warning");
247
287
 
248
288
  const locLines = lines.filter((l) => /^\s*\S+:\d+:\d+/.test(l));
249
289
 
@@ -269,41 +309,55 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
269
309
  const file = filesToShow[fi];
270
310
  const locs = byFile.get(file)!;
271
311
  const isLastFile = fi === filesToShow.length - 1 && files.length <= maxFiles;
272
- const fileBranch = isLastFile ? TREE_END : TREE_MID;
273
- const fileCont = isLastFile ? " " : `${TREE_PIPE} `;
274
-
275
- if (locs.length === 1) {
276
- output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}:${theme.fg(
277
- "muted",
278
- `${locs[0][0]}:${locs[0][1]}`,
279
- )}`;
280
- } else {
281
- output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}`;
312
+ const fileBranch = isLastFile ? theme.tree.last : theme.tree.branch;
313
+ const fileCont = isLastFile ? " " : `${theme.tree.vertical} `;
282
314
 
283
- const locsToShow = locs.slice(0, maxLocsPerFile);
284
- const locStrs = locsToShow.map(([l, c]) => `${l}:${c}`);
285
- const locsText = locStrs.join(", ");
286
- const hasMore = locs.length > maxLocsPerFile;
315
+ const fileMeta = `${locs.length} reference${locs.length !== 1 ? "s" : ""}`;
316
+ output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)} ${theme.fg("dim", fileMeta)}`;
287
317
 
288
- output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", TREE_END)} ${theme.fg("muted", locsText)}`;
289
- if (hasMore) {
290
- output += theme.fg("dim", ` +${locs.length - maxLocsPerFile} more`);
318
+ if (maxLocsPerFile > 0) {
319
+ const locsToShow = locs.slice(0, maxLocsPerFile);
320
+ for (let li = 0; li < locsToShow.length; li++) {
321
+ const [line, col] = locsToShow[li];
322
+ const isLastLoc = li === locsToShow.length - 1 && locs.length <= maxLocsPerFile;
323
+ const locBranch = isLastLoc ? theme.tree.last : theme.tree.branch;
324
+ const locCont = isLastLoc ? " " : `${theme.tree.vertical} `;
325
+ output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locBranch)} ${theme.fg(
326
+ "muted",
327
+ `line ${line}, col ${col}`,
328
+ )}`;
329
+ if (expanded) {
330
+ const context = `at ${file}:${line}:${col}`;
331
+ output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locCont)}${theme.fg(
332
+ "muted",
333
+ trimTo(context, 120, theme),
334
+ )}`;
335
+ }
336
+ }
337
+ if (locs.length > maxLocsPerFile) {
338
+ output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
339
+ "muted",
340
+ `${theme.format.ellipsis} ${locs.length - maxLocsPerFile} more`,
341
+ )}`;
291
342
  }
292
343
  }
293
344
  }
294
345
 
295
346
  if (files.length > maxFiles) {
296
- output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${files.length - maxFiles} more files`)}`;
347
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
348
+ "muted",
349
+ `${theme.format.ellipsis} ${files.length - maxFiles} more files`,
350
+ )}`;
297
351
  }
298
352
 
299
353
  return output;
300
354
  };
301
355
 
302
356
  if (expanded) {
303
- return new Text(renderGrouped(files.length, 30, false), 0, 0);
357
+ return new Text(renderGrouped(files.length, 3, false), 0, 0);
304
358
  }
305
359
 
306
- return new Text(renderGrouped(4, 10, true), 0, 0);
360
+ return new Text(renderGrouped(3, 1, true), 0, 0);
307
361
  }
308
362
 
309
363
  // =============================================================================
@@ -315,12 +369,13 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
315
369
  */
316
370
  function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
317
371
  const fileName = symbolsMatch[1];
318
- const icon = theme.fg("accent", "");
372
+ const icon = theme.styledSymbol("status.info", "accent");
319
373
 
320
374
  interface SymbolInfo {
321
375
  name: string;
322
376
  line: string;
323
377
  indent: number;
378
+ icon: string;
324
379
  }
325
380
 
326
381
  const symbolLines = lines.filter((l) => l.includes("@") && l.includes("line"));
@@ -328,9 +383,9 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
328
383
 
329
384
  for (const line of symbolLines) {
330
385
  const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
331
- const symMatch = line.trim().match(/^(.+?)\s*@\s*line\s*(\d+)/);
386
+ const symMatch = line.trim().match(/^(\S+)\s+(.+?)\s*@\s*line\s*(\d+)/);
332
387
  if (symMatch) {
333
- symbols.push({ name: symMatch[1], line: symMatch[2], indent });
388
+ symbols.push({ icon: symMatch[1], name: symMatch[2], line: symMatch[3], indent });
334
389
  }
335
390
  }
336
391
 
@@ -360,7 +415,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
360
415
  if (ancestorIdx >= 0 && isLastSibling(ancestorIdx)) {
361
416
  prefix += " ";
362
417
  } else {
363
- prefix += `${TREE_PIPE} `;
418
+ prefix += `${theme.tree.vertical} `;
364
419
  }
365
420
  }
366
421
  return prefix;
@@ -374,28 +429,37 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
374
429
  for (let i = 0; i < symbols.length; i++) {
375
430
  const sym = symbols[i];
376
431
  const prefix = getPrefix(i);
377
- const branch = isLastSibling(i) ? TREE_END : TREE_MID;
378
- output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg(
379
- "muted",
380
- `@${sym.line}`,
432
+ const isLast = isLastSibling(i);
433
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
434
+ const detailPrefix = isLast ? " " : `${theme.tree.vertical} `;
435
+ output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg(
436
+ "accent",
437
+ sym.name,
381
438
  )}`;
439
+ output += `\n${prefix}${theme.fg("dim", detailPrefix)}${theme.fg("muted", `line ${sym.line}`)}`;
382
440
  }
383
441
  return new Text(output, 0, 0);
384
442
  }
385
443
 
386
- // Collapsed: show first 4 top-level symbols
444
+ // Collapsed: show first 3 top-level symbols
387
445
  const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
388
446
  let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
389
447
 
390
- const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 4);
448
+ const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 3);
391
449
  for (let i = 0; i < topLevel.length; i++) {
392
450
  const sym = topLevel[i];
393
- const isLast = i === topLevel.length - 1 && topLevelCount <= 4;
394
- const branch = isLast ? TREE_END : TREE_MID;
395
- output += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg("muted", `@${sym.line}`)}`;
451
+ const isLast = i === topLevel.length - 1 && topLevelCount <= 3;
452
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
453
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg(
454
+ "accent",
455
+ sym.name,
456
+ )} ${theme.fg("muted", `line ${sym.line}`)}`;
396
457
  }
397
- if (topLevelCount > 4) {
398
- output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${topLevelCount - 4} more`)}`;
458
+ if (topLevelCount > 3) {
459
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
460
+ "muted",
461
+ `${theme.format.ellipsis} ${topLevelCount - 3} more`,
462
+ )}`;
399
463
  }
400
464
 
401
465
  return new Text(output, 0, 0);
@@ -409,20 +473,22 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
409
473
  * Generic fallback rendering for unknown result types.
410
474
  */
411
475
  function renderGeneric(text: string, lines: string[], expanded: boolean, theme: Theme): Text {
412
- const hasError = text.includes("Error:") || text.includes("✗");
413
- const hasSuccess = text.includes("✓") || text.includes("Applied");
476
+ const hasError = text.includes("Error:") || text.includes(theme.status.error);
477
+ const hasSuccess = text.includes(theme.status.success) || text.includes("Applied");
414
478
 
415
479
  const icon =
416
480
  hasError && !hasSuccess
417
- ? theme.fg("error", "")
481
+ ? theme.styledSymbol("status.error", "error")
418
482
  : hasSuccess && !hasError
419
- ? theme.fg("success", "")
420
- : theme.fg("accent", "");
483
+ ? theme.styledSymbol("status.success", "success")
484
+ : theme.styledSymbol("status.info", "accent");
421
485
 
422
486
  if (expanded) {
423
- let output = `${icon} ${theme.fg("toolTitle", "LSP")}`;
424
- for (const line of lines) {
425
- output += `\n ${line}`;
487
+ let output = `${icon} ${theme.fg("toolTitle", "LSP")} ${theme.fg("dim", "Output")}`;
488
+ for (let i = 0; i < lines.length; i++) {
489
+ const isLast = i === lines.length - 1;
490
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
491
+ output += `\n ${theme.fg("dim", branch)} ${lines[i]}`;
426
492
  }
427
493
  return new Text(output, 0, 0);
428
494
  }
@@ -435,13 +501,60 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
435
501
  const previewLines = lines.slice(1, 4);
436
502
  for (let i = 0; i < previewLines.length; i++) {
437
503
  const isLast = i === previewLines.length - 1 && lines.length <= 4;
438
- const branch = isLast ? TREE_END : TREE_MID;
504
+ const branch = isLast ? theme.tree.last : theme.tree.branch;
439
505
  output += `\n ${theme.fg("dim", branch)} ${theme.fg("dim", previewLines[i].trim().slice(0, 80))}`;
440
506
  }
441
507
  if (lines.length > 4) {
442
- output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${lines.length - 4} more lines`)}`;
508
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
509
+ "muted",
510
+ `${theme.format.ellipsis} ${lines.length - 4} more lines`,
511
+ )}`;
443
512
  }
444
513
  }
445
514
 
446
515
  return new Text(output, 0, 0);
447
516
  }
517
+
518
+ // =============================================================================
519
+ // Parsing Helpers
520
+ // =============================================================================
521
+
522
+ interface ParsedDiagnostic {
523
+ file: string;
524
+ line: string;
525
+ col: string;
526
+ severity: string;
527
+ message: string;
528
+ }
529
+
530
+ interface RawDiagnostic {
531
+ raw: string;
532
+ }
533
+
534
+ type DiagnosticItem = ParsedDiagnostic | RawDiagnostic;
535
+
536
+ function parseDiagnosticLine(line: string): ParsedDiagnostic | null {
537
+ const match = line.trim().match(/^(.*):(\d+):(\d+)\s+\[(\w+)\]\s*(.*)$/);
538
+ if (!match) return null;
539
+ const [, file, lineNum, colNum, severity, message] = match;
540
+ return { file, line: lineNum, col: colNum, severity: severity.toLowerCase(), message };
541
+ }
542
+
543
+ function severityToColor(severity: string): "error" | "warning" | "accent" | "dim" {
544
+ switch (severity) {
545
+ case "error":
546
+ return "error";
547
+ case "warning":
548
+ return "warning";
549
+ case "info":
550
+ return "accent";
551
+ default:
552
+ return "dim";
553
+ }
554
+ }
555
+
556
+ function trimTo(value: string, maxLength: number, theme: Theme): string {
557
+ if (value.length <= maxLength) return value;
558
+ const sliceLen = Math.max(0, maxLength - theme.format.ellipsis.length);
559
+ return `${value.slice(0, sliceLen)}${theme.format.ellipsis}`;
560
+ }
@@ -316,6 +316,28 @@ export interface Hover {
316
316
  range?: Range;
317
317
  }
318
318
 
319
+ // =============================================================================
320
+ // Linter Client Interface
321
+ // =============================================================================
322
+
323
+ /**
324
+ * Interface for linter/formatter clients.
325
+ * Can be implemented using LSP protocol or CLI tools.
326
+ */
327
+ export interface LinterClient {
328
+ /** Format file content. Returns formatted content. */
329
+ format(filePath: string, content: string): Promise<string>;
330
+
331
+ /** Get diagnostics for a file. Content should already be written to disk. */
332
+ lint(filePath: string): Promise<Diagnostic[]>;
333
+
334
+ /** Dispose of any resources (e.g., LSP connection) */
335
+ dispose?(): void;
336
+ }
337
+
338
+ /** Factory function to create a LinterClient */
339
+ export type LinterClientFactory = (config: ServerConfig, cwd: string) => LinterClient;
340
+
319
341
  // =============================================================================
320
342
  // Server Configuration
321
343
  // =============================================================================
@@ -341,6 +363,11 @@ export interface ServerConfig {
341
363
  isLinter?: boolean;
342
364
  /** Resolved absolute path to the command binary (set during config loading) */
343
365
  resolvedCommand?: string;
366
+ /**
367
+ * Custom linter client factory. If provided, creates a custom client instead of using LSP.
368
+ * The client handles format/lint operations. Useful for tools with buggy LSP implementations.
369
+ */
370
+ createClient?: LinterClientFactory;
344
371
  }
345
372
 
346
373
  // =============================================================================
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { type Theme, theme } from "../../../modes/interactive/theme/theme";
2
3
  import type {
3
4
  Diagnostic,
4
5
  DiagnosticSeverity,
@@ -197,13 +198,6 @@ const SEVERITY_NAMES: Record<DiagnosticSeverity, string> = {
197
198
  4: "hint",
198
199
  };
199
200
 
200
- const SEVERITY_ICONS: Record<DiagnosticSeverity, string> = {
201
- 1: "✖",
202
- 2: "⚠",
203
- 3: "ℹ",
204
- 4: "💡",
205
- };
206
-
207
201
  /**
208
202
  * Convert diagnostic severity number to string name.
209
203
  */
@@ -215,7 +209,21 @@ export function severityToString(severity?: DiagnosticSeverity): string {
215
209
  * Get icon for diagnostic severity.
216
210
  */
217
211
  export function severityToIcon(severity?: DiagnosticSeverity): string {
218
- return SEVERITY_ICONS[severity ?? 1] ?? "?";
212
+ const currentTheme = theme as Theme | undefined;
213
+ const fallback = currentTheme?.format?.bullet ?? "*";
214
+ const status = currentTheme?.status;
215
+ switch (severity ?? 1) {
216
+ case 1:
217
+ return status?.error ?? fallback;
218
+ case 2:
219
+ return status?.warning ?? fallback;
220
+ case 3:
221
+ return status?.info ?? fallback;
222
+ case 4:
223
+ return currentTheme?.format?.bullet ?? fallback;
224
+ default:
225
+ return status?.error ?? fallback;
226
+ }
219
227
  }
220
228
 
221
229
  /**
@@ -305,7 +313,7 @@ export function formatWorkspaceEdit(edit: WorkspaceEdit, cwd: string): string[]
305
313
  break;
306
314
  case "rename":
307
315
  results.push(
308
- `RENAME: ${path.relative(cwd, uriToFile(change.oldUri))} ${path.relative(cwd, uriToFile(change.newUri))}`,
316
+ `RENAME: ${path.relative(cwd, uriToFile(change.oldUri))} ${theme.nav.cursor} ${path.relative(cwd, uriToFile(change.newUri))}`,
309
317
  );
310
318
  break;
311
319
  case "delete":
@@ -328,30 +336,62 @@ export function formatTextEdit(edit: TextEdit, maxLength = 50): string {
328
336
  edit.newText.length > maxLength
329
337
  ? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}...`
330
338
  : edit.newText.replace(/\n/g, "\\n");
331
- return `line ${range} "${preview}"`;
339
+ return `line ${range} ${theme.nav.cursor} "${preview}"`;
332
340
  }
333
341
 
334
342
  // =============================================================================
335
343
  // Symbol Formatting
336
344
  // =============================================================================
337
345
 
338
- const SYMBOL_KIND_ICONS: Partial<Record<SymbolKind, string>> = {
339
- 5: "○", // Class
340
- 6: "ƒ", // Method
341
- 11: "◇", // Interface
342
- 12: "ƒ", // Function
343
- 13: "◆", // Variable
344
- 14: "◆", // Constant
345
- 10: "◎", // Enum
346
- 23: "□", // Struct
347
- 2: "◫", // Module
348
- };
346
+ function getSymbolKindIcons(): Record<SymbolKind, string> {
347
+ const currentTheme = theme as Theme | undefined;
348
+ const fallback = currentTheme?.format?.bullet ?? "*";
349
+ const dash = currentTheme?.format?.dash ?? fallback;
350
+ const icon = currentTheme?.icon;
351
+
352
+ const file = icon?.file ?? fallback;
353
+ const folder = icon?.folder ?? fallback;
354
+ const pkg = icon?.package ?? folder;
355
+ const model = icon?.model ?? fallback;
356
+ const func = icon?.auto ?? dash;
357
+
358
+ return {
359
+ 1: file, // File
360
+ 2: folder, // Module
361
+ 3: folder, // Namespace
362
+ 4: pkg, // Package
363
+ 5: model, // Class
364
+ 6: func, // Method
365
+ 7: fallback, // Property
366
+ 8: fallback, // Field
367
+ 9: func, // Constructor
368
+ 10: fallback, // Enum
369
+ 11: model, // Interface
370
+ 12: func, // Function
371
+ 13: fallback, // Variable
372
+ 14: fallback, // Constant
373
+ 15: fallback, // String
374
+ 16: fallback, // Number
375
+ 17: fallback, // Boolean
376
+ 18: fallback, // Array
377
+ 19: fallback, // Object
378
+ 20: fallback, // Key
379
+ 21: fallback, // Null
380
+ 22: fallback, // EnumMember
381
+ 23: folder, // Struct
382
+ 24: fallback, // Event
383
+ 25: fallback, // Operator
384
+ 26: fallback, // TypeParameter
385
+ };
386
+ }
349
387
 
350
388
  /**
351
389
  * Get icon for symbol kind.
352
390
  */
353
391
  export function symbolKindToIcon(kind: SymbolKind): string {
354
- return SYMBOL_KIND_ICONS[kind] ?? "•";
392
+ const currentTheme = theme as Theme | undefined;
393
+ const bullet = currentTheme?.format?.bullet ?? "*";
394
+ return getSymbolKindIcons()[kind] ?? bullet;
355
395
  }
356
396
 
357
397
  /**
@@ -26,6 +26,8 @@ export interface NotebookToolDetails {
26
26
  cellType?: string;
27
27
  /** Total cell count after operation */
28
28
  totalCells: number;
29
+ /** Cell content lines after operation (or removed content for delete) */
30
+ cellSource?: string[];
29
31
  }
30
32
 
31
33
  interface NotebookCell {
@@ -110,12 +112,14 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
110
112
  // Perform the action
111
113
  let resultMessage: string;
112
114
  let finalCellType: string | undefined;
115
+ let cellSource: string[] | undefined;
113
116
 
114
117
  switch (action) {
115
118
  case "edit": {
116
119
  const sourceLines = splitIntoLines(content!);
117
120
  notebook.cells[cell_index].source = sourceLines;
118
121
  finalCellType = notebook.cells[cell_index].cell_type;
122
+ cellSource = sourceLines;
119
123
  resultMessage = `Replaced cell ${cell_index} (${finalCellType})`;
120
124
  break;
121
125
  }
@@ -133,11 +137,14 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
133
137
  }
134
138
  notebook.cells.splice(cell_index, 0, newCell);
135
139
  finalCellType = newCellType;
140
+ cellSource = sourceLines;
136
141
  resultMessage = `Inserted ${newCellType} cell at position ${cell_index}`;
137
142
  break;
138
143
  }
139
144
  case "delete": {
140
- finalCellType = notebook.cells[cell_index].cell_type;
145
+ const removedCell = notebook.cells[cell_index];
146
+ finalCellType = removedCell.cell_type;
147
+ cellSource = removedCell.source;
141
148
  notebook.cells.splice(cell_index, 1);
142
149
  resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
143
150
  break;
@@ -163,6 +170,7 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
163
170
  cellIndex: cell_index,
164
171
  cellType: finalCellType,
165
172
  totalCells: newCellCount,
173
+ cellSource,
166
174
  },
167
175
  };
168
176
  });