@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * LSP Tool TUI Rendering
3
+ *
4
+ * Renders LSP tool calls and results in the TUI with:
5
+ * - Syntax-highlighted hover information
6
+ * - Color-coded diagnostics by severity
7
+ * - Grouped references and symbols
8
+ * - Collapsible/expandable views
9
+ */
10
+
11
+ import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
12
+ import { Text } from "@oh-my-pi/pi-tui";
13
+ import { highlight, supportsLanguage } from "cli-highlight";
14
+ import type { Theme } from "../../../modes/interactive/theme/theme.js";
15
+ import type { LspParams, LspToolDetails } from "./types.js";
16
+
17
+ // =============================================================================
18
+ // Tree Drawing Characters
19
+ // =============================================================================
20
+
21
+ const TREE_MID = "├─";
22
+ const TREE_END = "└─";
23
+ const TREE_PIPE = "│";
24
+
25
+ // =============================================================================
26
+ // Call Rendering
27
+ // =============================================================================
28
+
29
+ /**
30
+ * Render the LSP tool call in the TUI.
31
+ * Shows: "lsp <operation> <file/filecount>"
32
+ */
33
+ export function renderCall(args: unknown, theme: Theme): Text {
34
+ const p = args as LspParams & { file?: string; files?: string[] };
35
+
36
+ let text = theme.fg("toolTitle", theme.bold("LSP "));
37
+ text += theme.fg("accent", p.action || "?");
38
+
39
+ if (p.file) {
40
+ text += ` ${theme.fg("muted", p.file)}`;
41
+ } else if (p.files?.length) {
42
+ text += ` ${theme.fg("muted", `${p.files.length} file(s)`)}`;
43
+ }
44
+
45
+ return new Text(text, 0, 0);
46
+ }
47
+
48
+ // =============================================================================
49
+ // Result Rendering
50
+ // =============================================================================
51
+
52
+ /**
53
+ * Render LSP tool result with intelligent formatting based on result type.
54
+ * Detects hover, diagnostics, references, symbols, etc. and formats accordingly.
55
+ */
56
+ export function renderResult(
57
+ result: AgentToolResult<LspToolDetails>,
58
+ options: RenderResultOptions,
59
+ theme: Theme,
60
+ ): Text {
61
+ const content = result.content?.[0];
62
+ if (!content || content.type !== "text" || !("text" in content) || !content.text) {
63
+ return new Text(theme.fg("error", "No result"), 0, 0);
64
+ }
65
+
66
+ const text = content.text;
67
+ const lines = text.split("\n").filter((l) => l.trim());
68
+ const expanded = options.expanded;
69
+
70
+ // Detect result type and render accordingly
71
+ const codeBlockMatch = text.match(/```(\w*)\n([\s\S]*?)```/);
72
+ if (codeBlockMatch) {
73
+ return renderHover(codeBlockMatch, text, lines, expanded, theme);
74
+ }
75
+
76
+ const errorMatch = text.match(/(\d+)\s+error\(s\)/);
77
+ const warningMatch = text.match(/(\d+)\s+warning\(s\)/);
78
+ if (errorMatch || warningMatch || text.includes("✗")) {
79
+ return renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
80
+ }
81
+
82
+ const refMatch = text.match(/(\d+)\s+reference\(s\)/);
83
+ if (refMatch) {
84
+ return renderReferences(refMatch, lines, expanded, theme);
85
+ }
86
+
87
+ const symbolsMatch = text.match(/Symbols in (.+):/);
88
+ if (symbolsMatch) {
89
+ return renderSymbols(symbolsMatch, lines, expanded, theme);
90
+ }
91
+
92
+ // Default fallback rendering
93
+ return renderGeneric(text, lines, expanded, theme);
94
+ }
95
+
96
+ // =============================================================================
97
+ // Hover Rendering
98
+ // =============================================================================
99
+
100
+ /**
101
+ * Render hover information with syntax-highlighted code blocks.
102
+ */
103
+ function renderHover(
104
+ codeBlockMatch: RegExpMatchArray,
105
+ fullText: string,
106
+ _lines: string[],
107
+ expanded: boolean,
108
+ theme: Theme,
109
+ ): Text {
110
+ const lang = codeBlockMatch[1] || "";
111
+ const code = codeBlockMatch[2].trim();
112
+ const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
113
+
114
+ const codeLines = highlightCode(code, lang, theme);
115
+ const icon = theme.fg("accent", "●");
116
+ const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
117
+
118
+ if (expanded) {
119
+ let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}`;
120
+ output += `\n ${theme.fg("mdCodeBlockBorder", "┌───")}`;
121
+ for (const line of codeLines) {
122
+ output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${line}`;
123
+ }
124
+ output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
125
+ if (afterCode) {
126
+ output += `\n ${theme.fg("muted", afterCode)}`;
127
+ }
128
+ return new Text(output, 0, 0);
129
+ }
130
+
131
+ // Collapsed view
132
+ const firstCodeLine = codeLines[0] || "";
133
+ const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
134
+
135
+ let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}${expandHint}`;
136
+ output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${firstCodeLine}`;
137
+
138
+ if (codeLines.length > 1) {
139
+ output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${theme.fg("muted", `… ${codeLines.length - 1} more lines`)}`;
140
+ }
141
+
142
+ 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
+ } else {
146
+ output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
147
+ }
148
+
149
+ return new Text(output, 0, 0);
150
+ }
151
+
152
+ /**
153
+ * Syntax highlight code using highlight.ts.
154
+ */
155
+ function highlightCode(codeText: string, language: string, theme: Theme): string[] {
156
+ const validLang = language && supportsLanguage(language) ? language : undefined;
157
+ try {
158
+ const cliTheme = {
159
+ keyword: (s: string) => theme.fg("syntaxKeyword", s),
160
+ built_in: (s: string) => theme.fg("syntaxType", s),
161
+ literal: (s: string) => theme.fg("syntaxNumber", s),
162
+ number: (s: string) => theme.fg("syntaxNumber", s),
163
+ string: (s: string) => theme.fg("syntaxString", s),
164
+ comment: (s: string) => theme.fg("syntaxComment", s),
165
+ function: (s: string) => theme.fg("syntaxFunction", s),
166
+ title: (s: string) => theme.fg("syntaxFunction", s),
167
+ class: (s: string) => theme.fg("syntaxType", s),
168
+ type: (s: string) => theme.fg("syntaxType", s),
169
+ attr: (s: string) => theme.fg("syntaxVariable", s),
170
+ variable: (s: string) => theme.fg("syntaxVariable", s),
171
+ params: (s: string) => theme.fg("syntaxVariable", s),
172
+ operator: (s: string) => theme.fg("syntaxOperator", s),
173
+ punctuation: (s: string) => theme.fg("syntaxPunctuation", s),
174
+ };
175
+ return highlight(codeText, { language: validLang, ignoreIllegals: true, theme: cliTheme }).split("\n");
176
+ } catch {
177
+ return codeText.split("\n");
178
+ }
179
+ }
180
+
181
+ // =============================================================================
182
+ // Diagnostics Rendering
183
+ // =============================================================================
184
+
185
+ /**
186
+ * Render diagnostics with color-coded severity.
187
+ */
188
+ function renderDiagnostics(
189
+ errorMatch: RegExpMatchArray | null,
190
+ warningMatch: RegExpMatchArray | null,
191
+ lines: string[],
192
+ expanded: boolean,
193
+ theme: Theme,
194
+ ): Text {
195
+ const errorCount = errorMatch ? Number.parseInt(errorMatch[1], 10) : 0;
196
+ const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
197
+
198
+ const icon =
199
+ errorCount > 0 ? theme.fg("error", "●") : warnCount > 0 ? theme.fg("warning", "●") : theme.fg("success", "●");
200
+
201
+ const meta: string[] = [];
202
+ if (errorCount > 0) meta.push(`${errorCount} error${errorCount !== 1 ? "s" : ""}`);
203
+ if (warnCount > 0) meta.push(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`);
204
+ if (meta.length === 0) meta.push("No issues");
205
+
206
+ const diagLines = lines.filter((l) => l.includes("✗") || /:\d+:\d+/.test(l));
207
+
208
+ if (expanded) {
209
+ 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)}`;
216
+ }
217
+ return new Text(output, 0, 0);
218
+ }
219
+
220
+ // Collapsed view
221
+ const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
222
+ let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}${expandHint}`;
223
+
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()}`;
229
+ }
230
+ if (diagLines.length > 4) {
231
+ output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${diagLines.length - 4} more`)}`;
232
+ }
233
+
234
+ return new Text(output, 0, 0);
235
+ }
236
+
237
+ // =============================================================================
238
+ // References Rendering
239
+ // =============================================================================
240
+
241
+ /**
242
+ * Render references grouped by file.
243
+ */
244
+ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
245
+ const refCount = Number.parseInt(refMatch[1], 10);
246
+ const icon = refCount > 0 ? theme.fg("success", "●") : theme.fg("warning", "●");
247
+
248
+ const locLines = lines.filter((l) => /^\s*\S+:\d+:\d+/.test(l));
249
+
250
+ // Group by file
251
+ const byFile = new Map<string, Array<[string, string]>>();
252
+ for (const loc of locLines) {
253
+ const match = loc.trim().match(/^(.+):(\d+):(\d+)$/);
254
+ if (match) {
255
+ const [, file, line, col] = match;
256
+ if (!byFile.has(file)) byFile.set(file, []);
257
+ byFile.get(file)!.push([line, col]);
258
+ }
259
+ }
260
+
261
+ const files = Array.from(byFile.keys());
262
+
263
+ const renderGrouped = (maxFiles: number, maxLocsPerFile: number, showHint: boolean): string => {
264
+ const expandHint = showHint ? theme.fg("dim", " (Ctrl+O to expand)") : "";
265
+ let output = `${icon} ${theme.fg("toolTitle", "References")} ${theme.fg("dim", `${refCount} found`)}${expandHint}`;
266
+
267
+ const filesToShow = files.slice(0, maxFiles);
268
+ for (let fi = 0; fi < filesToShow.length; fi++) {
269
+ const file = filesToShow[fi];
270
+ const locs = byFile.get(file)!;
271
+ 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)}`;
282
+
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;
287
+
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`);
291
+ }
292
+ }
293
+ }
294
+
295
+ if (files.length > maxFiles) {
296
+ output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${files.length - maxFiles} more files`)}`;
297
+ }
298
+
299
+ return output;
300
+ };
301
+
302
+ if (expanded) {
303
+ return new Text(renderGrouped(files.length, 30, false), 0, 0);
304
+ }
305
+
306
+ return new Text(renderGrouped(4, 10, true), 0, 0);
307
+ }
308
+
309
+ // =============================================================================
310
+ // Symbols Rendering
311
+ // =============================================================================
312
+
313
+ /**
314
+ * Render document symbols in a hierarchical tree.
315
+ */
316
+ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
317
+ const fileName = symbolsMatch[1];
318
+ const icon = theme.fg("accent", "●");
319
+
320
+ interface SymbolInfo {
321
+ name: string;
322
+ line: string;
323
+ indent: number;
324
+ }
325
+
326
+ const symbolLines = lines.filter((l) => l.includes("@") && l.includes("line"));
327
+ const symbols: SymbolInfo[] = [];
328
+
329
+ for (const line of symbolLines) {
330
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
331
+ const symMatch = line.trim().match(/^(.+?)\s*@\s*line\s*(\d+)/);
332
+ if (symMatch) {
333
+ symbols.push({ name: symMatch[1], line: symMatch[2], indent });
334
+ }
335
+ }
336
+
337
+ const isLastSibling = (i: number): boolean => {
338
+ const myIndent = symbols[i].indent;
339
+ for (let j = i + 1; j < symbols.length; j++) {
340
+ const nextIndent = symbols[j].indent;
341
+ if (nextIndent === myIndent) return false;
342
+ if (nextIndent < myIndent) return true;
343
+ }
344
+ return true;
345
+ };
346
+
347
+ const getPrefix = (i: number): string => {
348
+ const myIndent = symbols[i].indent;
349
+ if (myIndent === 0) return " ";
350
+
351
+ let prefix = " ";
352
+ for (let level = 2; level <= myIndent; level += 2) {
353
+ let ancestorIdx = -1;
354
+ for (let j = i - 1; j >= 0; j--) {
355
+ if (symbols[j].indent === level - 2) {
356
+ ancestorIdx = j;
357
+ break;
358
+ }
359
+ }
360
+ if (ancestorIdx >= 0 && isLastSibling(ancestorIdx)) {
361
+ prefix += " ";
362
+ } else {
363
+ prefix += `${TREE_PIPE} `;
364
+ }
365
+ }
366
+ return prefix;
367
+ };
368
+
369
+ const topLevelCount = symbols.filter((s) => s.indent === 0).length;
370
+
371
+ if (expanded) {
372
+ let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}`;
373
+
374
+ for (let i = 0; i < symbols.length; i++) {
375
+ const sym = symbols[i];
376
+ 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}`,
381
+ )}`;
382
+ }
383
+ return new Text(output, 0, 0);
384
+ }
385
+
386
+ // Collapsed: show first 4 top-level symbols
387
+ const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
388
+ let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
389
+
390
+ const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 4);
391
+ for (let i = 0; i < topLevel.length; i++) {
392
+ 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}`)}`;
396
+ }
397
+ if (topLevelCount > 4) {
398
+ output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${topLevelCount - 4} more`)}`;
399
+ }
400
+
401
+ return new Text(output, 0, 0);
402
+ }
403
+
404
+ // =============================================================================
405
+ // Generic Rendering
406
+ // =============================================================================
407
+
408
+ /**
409
+ * Generic fallback rendering for unknown result types.
410
+ */
411
+ 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");
414
+
415
+ const icon =
416
+ hasError && !hasSuccess
417
+ ? theme.fg("error", "●")
418
+ : hasSuccess && !hasError
419
+ ? theme.fg("success", "●")
420
+ : theme.fg("accent", "●");
421
+
422
+ if (expanded) {
423
+ let output = `${icon} ${theme.fg("toolTitle", "LSP")}`;
424
+ for (const line of lines) {
425
+ output += `\n ${line}`;
426
+ }
427
+ return new Text(output, 0, 0);
428
+ }
429
+
430
+ const firstLine = lines[0] || "No output";
431
+ const expandHint = lines.length > 1 ? theme.fg("dim", " (Ctrl+O to expand)") : "";
432
+ let output = `${icon} ${theme.fg("toolTitle", "LSP")} ${theme.fg("dim", firstLine.slice(0, 60))}${expandHint}`;
433
+
434
+ if (lines.length > 1) {
435
+ const previewLines = lines.slice(1, 4);
436
+ for (let i = 0; i < previewLines.length; i++) {
437
+ const isLast = i === previewLines.length - 1 && lines.length <= 4;
438
+ const branch = isLast ? TREE_END : TREE_MID;
439
+ output += `\n ${theme.fg("dim", branch)} ${theme.fg("dim", previewLines[i].trim().slice(0, 80))}`;
440
+ }
441
+ if (lines.length > 4) {
442
+ output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${lines.length - 4} more lines`)}`;
443
+ }
444
+ }
445
+
446
+ return new Text(output, 0, 0);
447
+ }
@@ -0,0 +1,145 @@
1
+ import { sendNotification, sendRequest } from "./client.js";
2
+ import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types.js";
3
+ import { fileToUri } from "./utils.js";
4
+
5
+ /**
6
+ * Wait for specified milliseconds.
7
+ */
8
+ async function sleep(ms: number): Promise<void> {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ /**
13
+ * Run flycheck (cargo check) and collect diagnostics.
14
+ * Sends rust-analyzer/runFlycheck notification and waits for diagnostics to accumulate.
15
+ *
16
+ * @param client - LSP client instance
17
+ * @param file - Optional file path to check (if not provided, checks entire workspace)
18
+ * @returns Array of all collected diagnostics
19
+ */
20
+ export async function flycheck(client: LspClient, file?: string): Promise<Diagnostic[]> {
21
+ const textDocument = file ? { uri: fileToUri(file) } : null;
22
+ await sendNotification(client, "rust-analyzer/runFlycheck", { textDocument });
23
+
24
+ // Wait for diagnostics to accumulate (2 seconds as per reference)
25
+ await sleep(2000);
26
+
27
+ // Collect all diagnostics from client
28
+ const allDiags: Diagnostic[] = [];
29
+ for (const diags of Array.from(client.diagnostics.values())) {
30
+ allDiags.push(...diags);
31
+ }
32
+
33
+ return allDiags;
34
+ }
35
+
36
+ /**
37
+ * Expand macro at the given position.
38
+ *
39
+ * @param client - LSP client instance
40
+ * @param file - File path containing the macro
41
+ * @param line - 1-based line number
42
+ * @param character - 1-based character offset
43
+ * @returns ExpandMacroResult with macro name and expansion, or null if no macro at position
44
+ */
45
+ export async function expandMacro(
46
+ client: LspClient,
47
+ file: string,
48
+ line: number,
49
+ character: number,
50
+ ): Promise<ExpandMacroResult | null> {
51
+ const result = (await sendRequest(client, "rust-analyzer/expandMacro", {
52
+ textDocument: { uri: fileToUri(file) },
53
+ position: { line: line - 1, character: character - 1 },
54
+ })) as ExpandMacroResult | null;
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Perform structural search and replace (SSR).
61
+ *
62
+ * @param client - LSP client instance
63
+ * @param pattern - Search pattern
64
+ * @param replacement - Replacement pattern
65
+ * @param parseOnly - If true, returns matches only; if false, returns WorkspaceEdit to apply
66
+ * @returns WorkspaceEdit containing matches or changes to apply
67
+ */
68
+ export async function ssr(
69
+ client: LspClient,
70
+ pattern: string,
71
+ replacement: string,
72
+ parseOnly = true,
73
+ ): Promise<WorkspaceEdit> {
74
+ const result = (await sendRequest(client, "experimental/ssr", {
75
+ query: `${pattern} ==>> ${replacement}`,
76
+ parseOnly,
77
+ textDocument: { uri: "" }, // SSR searches workspace-wide
78
+ position: { line: 0, character: 0 },
79
+ selections: [],
80
+ })) as WorkspaceEdit;
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Get runnables (tests, binaries, examples) for a file.
87
+ *
88
+ * @param client - LSP client instance
89
+ * @param file - File path to query
90
+ * @param line - Optional 1-based line number to get runnables at specific position
91
+ * @returns Array of Runnable items
92
+ */
93
+ export async function runnables(client: LspClient, file: string, line?: number): Promise<Runnable[]> {
94
+ const params: { textDocument: { uri: string }; position?: { line: number; character: number } } = {
95
+ textDocument: { uri: fileToUri(file) },
96
+ };
97
+
98
+ if (line !== undefined) {
99
+ params.position = { line: line - 1, character: 0 };
100
+ }
101
+
102
+ const result = (await sendRequest(client, "experimental/runnables", params)) as Runnable[];
103
+ return result ?? [];
104
+ }
105
+
106
+ /**
107
+ * Get related tests for a position (e.g., tests for a function).
108
+ *
109
+ * @param client - LSP client instance
110
+ * @param file - File path
111
+ * @param line - 1-based line number
112
+ * @param character - 1-based character offset
113
+ * @returns Array of test runnable labels
114
+ */
115
+ export async function relatedTests(
116
+ client: LspClient,
117
+ file: string,
118
+ line: number,
119
+ character: number,
120
+ ): Promise<string[]> {
121
+ const tests = (await sendRequest(client, "rust-analyzer/relatedTests", {
122
+ textDocument: { uri: fileToUri(file) },
123
+ position: { line: line - 1, character: character - 1 },
124
+ })) as RelatedTest[];
125
+
126
+ if (!tests?.length) return [];
127
+
128
+ const labels: string[] = [];
129
+ for (const t of tests) {
130
+ if (t.runnable?.label) {
131
+ labels.push(t.runnable.label);
132
+ }
133
+ }
134
+
135
+ return labels;
136
+ }
137
+
138
+ /**
139
+ * Reload workspace (re-index Cargo projects).
140
+ *
141
+ * @param client - LSP client instance
142
+ */
143
+ export async function reloadWorkspace(client: LspClient): Promise<void> {
144
+ await sendRequest(client, "rust-analyzer/reloadWorkspace", null);
145
+ }