one-code-extension 0.1.4 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -1,125 +1,299 @@
1
1
  /**
2
- * lsp extension — Claude Code's post-edit diagnostics.
2
+ * lsp extension — language-server diagnostics, the way Claude Code surfaces them.
3
3
  *
4
- * Two behaviors, no more:
5
- * 1. After a successful `edit` or `write`, error diagnostics for that file are
6
- * appended to the tool result, so the model immediately sees what it broke.
7
- * 2. An `lsp_diagnostics` tool (deferred behind `tool_search`) for asking about
8
- * a file on demand.
4
+ * Three behaviors:
5
+ * 1. A session-wide watcher (watcher.ts): new diagnostics — cross-file, all
6
+ * severities, deduplicated against what was already delivered — ride the
7
+ * tool result of the round that surfaced them as a raw `<new-diagnostics>`
8
+ * block (a reminder-queue one-shot, pinned to the trailing tool result
9
+ * after the `<total_tokens>` line — the exact place a Claude Code session
10
+ * shows them), or the message that opens the next run when they arrived
11
+ * while idle (`agent_start`, so a notification-opened run gets them too).
12
+ * Until 2026-09-05 they went out as a custom steer message of their own
13
+ * (a separate user turn on the wire). Editing a file clears its
14
+ * delivered-set so fixed-then-reintroduced issues resurface.
15
+ * 2. Plugin-provided servers (plugin-servers.ts): an enabled plugin's
16
+ * `.lsp.json` / manifest `lspServers` adds servers routed by file
17
+ * extension, taking precedence over the built-in table — installing the
18
+ * plugin is explicit intent. Built-in servers keep root-marker detection;
19
+ * plugin servers root at `workspaceFolder ?? cwd`.
20
+ * 3. An `lsp_diagnostics` tool (deferred behind `tool_search`) for asking
21
+ * about a file on demand.
9
22
  *
10
- * Claude Code has no other LSP tools; navigation goes through grep/find. Adding
11
- * hover or definition later is a small `client.request(...)` call each, if the
12
- * need appears.
23
+ * Claude Code has no other LSP tools; navigation goes through grep/find.
13
24
  */
14
25
 
15
- import { relative } from "node:path";
26
+ import { mkdirSync } from "node:fs";
27
+ import { extname, relative } from "node:path";
16
28
  import { StringEnum } from "@earendil-works/pi-ai";
17
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
29
+ import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
18
30
  import { Type } from "typebox";
19
31
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
20
- import { ccToolRenderers } from "../lib/tui-render.ts";
21
- import { LspClient } from "./client.ts";
22
- import { filterDiagnostics, formatDiagnostics, type SeverityFilter } from "./format.ts";
32
+ import { defaultDiscoverRoots, discoverPlugins, pluginResources } from "../lib/plugins.ts";
33
+ import { REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
34
+ import { ccToolRenderers, cutPlainText } from "../lib/tui-render.ts";
35
+ import { LspClient, type LspClientOptions, pathToUri } from "./client.ts";
36
+ import { filterDiagnostics, formatDiagnostics, type LspDiagnostic, type SeverityFilter } from "./format.ts";
37
+ import { describeStartFailure, INSTALL_HINTS } from "./install-hints.ts";
38
+ import { withKeepAlive } from "./keep-alive.ts";
39
+ import {
40
+ pluginLanguageId,
41
+ readManifestLspServers,
42
+ type ResolvedPluginServer,
43
+ resolveExtensionRouting,
44
+ resolvePluginServers,
45
+ } from "./plugin-servers.ts";
23
46
  import { findProjectRoot, serverForPath, typescriptPreflight } from "./servers.ts";
47
+ import { computeDelta, DeliveredTracker, fingerprintDiagnostic, formatNewDiagnostics, markDelivered } from "./watcher.ts";
24
48
 
25
- /** Only errors are pushed unasked; warnings would be noise on every edit. */
26
- const AUTO_APPEND_LIMIT = 10;
27
-
28
- /** The (match, project root, client-map key) triple every entry point needs; undefined when the path has no configured server. */
29
- function resolveServer(
30
- path: string,
31
- cwd: string,
32
- ): { match: NonNullable<ReturnType<typeof serverForPath>>; root: string; key: string } | undefined {
33
- const match = serverForPath(path);
34
- if (!match) return undefined;
35
- const root = findProjectRoot(path, match.config.rootMarkers, cwd);
36
- const key = `${match.languageId}:${root}`;
37
- return { match, root, key };
49
+ /** Everything needed to spawn/reuse the server responsible for a path. */
50
+ interface ResolvedTarget {
51
+ key: string;
52
+ root: string;
53
+ languageId: string;
54
+ command: string;
55
+ args: string[];
56
+ options: LspClientOptions;
57
+ /** Set when a plugin config (not the built-in table) claimed the extension. */
58
+ plugin?: ResolvedPluginServer;
59
+ }
60
+
61
+ interface PluginRouting {
62
+ byExtension: Map<string, ResolvedPluginServer>;
63
+ collisions: string[];
64
+ diagnostics: string[];
38
65
  }
39
66
 
40
67
  export default function lspExtension(pi: ExtensionAPI) {
41
68
  const clients = new Map<string, LspClient>();
42
69
  const startFailures = new Map<string, string>();
43
70
  const warned = new Set<string>();
71
+ const tracker = new DeliveredTracker();
44
72
 
45
- const clientFor = async (path: string, cwd: string): Promise<LspClient | undefined> => {
46
- const resolved = resolveServer(path, cwd);
47
- if (!resolved) return undefined;
48
- const { match, root, key } = resolved;
73
+ // Built lazily on the first file touched — never at session_start
74
+ // (findings §15: serial session_start work delays the prompt).
75
+ let routing: PluginRouting | undefined;
76
+ const pluginRouting = (cwd: string): PluginRouting => (routing ??= buildPluginRouting(cwd));
49
77
 
50
- const failure = startFailures.get(key);
51
- if (failure) return undefined;
78
+ function buildPluginRouting(cwd: string): PluginRouting {
79
+ const discovered = discoverPlugins(defaultDiscoverRoots(getAgentDir(), cwd));
80
+ const diagnostics: string[] = [];
81
+ const servers: ResolvedPluginServer[] = [];
82
+ for (const plugin of discovered.enabledPlugins) {
83
+ const { lspConfig } = pluginResources(plugin);
84
+ const manifestServers = readManifestLspServers(plugin.path);
85
+ if (!lspConfig && manifestServers === undefined) continue;
86
+ const resolved = resolvePluginServers(
87
+ { name: plugin.name, path: plugin.path, dataRoot: plugin.dataRoot },
88
+ lspConfig,
89
+ manifestServers,
90
+ process.env,
91
+ );
92
+ servers.push(...resolved.servers);
93
+ diagnostics.push(...resolved.diagnostics);
94
+ }
95
+ const routed = resolveExtensionRouting(servers);
96
+ return { byExtension: routed.byExtension, collisions: routed.collisions, diagnostics };
97
+ }
98
+
99
+ const resolveTarget = (path: string, cwd: string): ResolvedTarget | undefined => {
100
+ const pluginServer = pluginRouting(cwd).byExtension.get(extname(path).toLowerCase());
101
+ if (pluginServer) {
102
+ return {
103
+ key: pluginServer.key,
104
+ root: pluginServer.workspaceFolder ?? cwd,
105
+ languageId: pluginLanguageId(pluginServer, path),
106
+ command: pluginServer.command,
107
+ args: pluginServer.args,
108
+ options: {
109
+ env: pluginServer.env,
110
+ initializationOptions: pluginServer.initializationOptions,
111
+ settings: pluginServer.settings,
112
+ startupTimeoutMs: pluginServer.startupTimeoutMs,
113
+ },
114
+ plugin: pluginServer,
115
+ };
116
+ }
117
+ const match = serverForPath(path);
118
+ if (!match) return undefined;
119
+ const root = findProjectRoot(path, match.config.rootMarkers, cwd);
120
+ return {
121
+ key: `${match.languageId}:${root}`,
122
+ root,
123
+ languageId: match.languageId,
124
+ command: match.config.command,
125
+ args: match.config.args,
126
+ options: {},
127
+ };
128
+ };
129
+
130
+ /** Servers may be respawned this many times after a mid-session crash; beyond that the language is off for the session. */
131
+ const MAX_RESPAWNS = 2;
132
+ const respawns = new Map<string, number>();
133
+
134
+ const clientFor = async (target: ResolvedTarget): Promise<LspClient | undefined> => {
135
+ const { key } = target;
136
+ if (startFailures.has(key)) return undefined;
52
137
 
53
138
  const existing = clients.get(key);
54
139
  if (existing?.isRunning) return existing;
55
140
  if (existing) {
56
- // Crashed mid-session. Record why so downstream reports the real cause
57
- // (a server that was running and died) instead of falling through to
141
+ // Crashed mid-session. Respawn with a short backoff, up to MAX_RESPAWNS
142
+ // times; after that record why so downstream reports the real cause (a
143
+ // server that was running and died) instead of falling through to
58
144
  // "install <command>" advice for an already-installed server — and so
59
- // the one-time post-edit warning still fires. Don't respawn in a loop.
60
- startFailures.set(key, existing.error ?? "language server stopped unexpectedly");
61
- return undefined;
145
+ // the one-time post-edit warning still fires (review T10).
146
+ const count = (respawns.get(key) ?? 0) + 1;
147
+ respawns.set(key, count);
148
+ if (count > MAX_RESPAWNS) {
149
+ startFailures.set(key, `${existing.error ?? "language server stopped unexpectedly"} (gave up after ${MAX_RESPAWNS} restarts)`);
150
+ return undefined;
151
+ }
152
+ clients.delete(key);
153
+ await new Promise((resolve) => setTimeout(resolve, 500 * count));
62
154
  }
63
155
 
64
- if (match.languageId.startsWith("typescript") || match.languageId.startsWith("javascript")) {
65
- const problem = typescriptPreflight(root);
156
+ // Keyed on the command being spawned, not on where the config came from —
157
+ // a plugin configuring typescript-language-server hits the same TS7 trap.
158
+ if (target.command === "typescript-language-server") {
159
+ const problem = typescriptPreflight(target.root);
66
160
  if (problem) {
67
161
  startFailures.set(key, problem);
68
162
  return undefined;
69
163
  }
70
164
  }
71
165
 
72
- const client = new LspClient(match.languageId, match.config, root);
166
+ if (target.plugin) mkdirSync(target.plugin.dataDir, { recursive: true });
167
+
168
+ const client = new LspClient({ command: target.command, args: target.args }, target.root, target.options);
73
169
  clients.set(key, client);
74
170
  try {
75
171
  await client.start();
76
172
  return client;
77
173
  } catch (error) {
78
- startFailures.set(key, client.error ?? (error as Error).message);
174
+ // A missing binary (spawn ENOENT) is rewritten into install guidance
175
+ // here, once, so the warning, /lsp, and the tool all show it.
176
+ const raw = client.error ?? (error as Error).message;
177
+ startFailures.set(key, describeStartFailure(raw, target.command, target.plugin?.pluginName));
79
178
  return undefined;
80
179
  }
81
180
  };
82
181
 
83
- /** One-time notice per root so a missing server doesn't degrade silently. */
84
- const reportFailureOnce = (path: string, cwd: string, notify: (message: string) => void) => {
85
- const resolved = resolveServer(path, cwd);
86
- if (!resolved) return;
87
- const { match, key } = resolved;
88
- const failure = startFailures.get(key);
89
- if (failure && !warned.has(key)) {
90
- warned.add(key);
182
+ /** One-time notice per server key so a missing server doesn't degrade silently. */
183
+ const reportFailureOnce = (target: ResolvedTarget, notify: (message: string) => void) => {
184
+ const failure = startFailures.get(target.key);
185
+ if (failure && !warned.has(target.key)) {
186
+ warned.add(target.key);
91
187
  // Server errors run to paragraphs; the transcript gets one line and
92
- // /lsp keeps the full status.
188
+ // /lsp keeps the full status. The cap leaves room for a full
189
+ // missing-binary line including its install command.
93
190
  const brief = failure.split("\n")[0].replace(/\s+/g, " ").trim();
94
- const capped = brief.length > 100 ? `${brief.slice(0, 99)}…` : brief;
95
- notify(`LSP unavailable for ${match.languageId}: ${capped} (/lsp for status)`);
191
+ const capped = cutPlainText(brief, 160);
192
+ notify(`LSP unavailable for ${target.languageId}: ${capped} (/lsp for status)`);
96
193
  }
97
194
  };
98
195
 
196
+ /** One-time notice when plugin LSP configs were invalid or collided. */
197
+ const reportRoutingIssuesOnce = (ctx: ExtensionContext) => {
198
+ const { collisions, diagnostics } = pluginRouting(ctx.cwd);
199
+ if ((collisions.length === 0 && diagnostics.length === 0) || warned.has("plugin:routing")) return;
200
+ warned.add("plugin:routing");
201
+ if (!ctx.hasUI) return;
202
+ const lines = [...diagnostics, ...collisions].slice(0, 4);
203
+ ctx.ui.notify(`Plugin LSP config issues (/lsp for the full list):\n${lines.join("\n")}`, "warning");
204
+ };
205
+
206
+ /** All servers' current diagnostics, merged per document uri. */
207
+ const mergedDiagnostics = (): Map<string, LspDiagnostic[]> => {
208
+ const merged = new Map<string, LspDiagnostic[]>();
209
+ for (const client of clients.values()) {
210
+ for (const [uri, list] of client.allDiagnostics()) {
211
+ const existing = merged.get(uri);
212
+ merged.set(uri, existing ? [...existing, ...list] : list);
213
+ }
214
+ }
215
+ return merged;
216
+ };
217
+
218
+ // tool_result fires on EVERY tool call; re-fingerprinting all delivered
219
+ // diagnostics each round is pure waste when nothing republished. -1 forces
220
+ // a scan (used after clear-on-edit, where cached diagnostics become
221
+ // deliverable again without a new publish).
222
+ let lastPublishTally = 0;
223
+ const forceDeltaScan = () => {
224
+ lastPublishTally = -1;
225
+ };
226
+
227
+ /** Format the pending delta and commit it as delivered; undefined when clean. */
228
+ const takePendingDelta = (cwd: string): string | undefined => {
229
+ if (clients.size === 0) return undefined;
230
+ const tally = [...clients.values()].reduce((sum, client) => sum + client.publishCount, 0);
231
+ if (tally === lastPublishTally) return undefined;
232
+ lastPublishTally = tally;
233
+ const delta = computeDelta(mergedDiagnostics(), tracker);
234
+ const text = formatNewDiagnostics(delta, cwd);
235
+ if (!text) return undefined;
236
+ markDelivered(delta, tracker);
237
+ return text;
238
+ };
239
+
240
+ /**
241
+ * Queue the block where the model reads next. Raw (no `<system-reminder>`
242
+ * frame, as in Claude Code) and last-append: from a `tool_result` hook this
243
+ * extension runs after system-reminder's, so the one-shot is pending at the
244
+ * next `context` and pinned to the trailing tool result (process memory,
245
+ * same mechanism as the file-tracker's per-round report); from `agent_start`
246
+ * it pins to the message that opened the run.
247
+ */
248
+ const queueNewDiagnostics = (text: string) => {
249
+ pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append", raw: true } satisfies ReminderPayload);
250
+ };
251
+
99
252
  pi.on("tool_result", async (event, ctx) => {
100
- if (event.isError) return;
101
- if (event.toolName !== "edit" && event.toolName !== "write") return;
102
-
103
- const path = (event.input as { path?: unknown }).path;
104
- if (typeof path !== "string") return;
105
-
106
- const client = await clientFor(path, ctx.cwd);
107
- if (!client) {
108
- reportFailureOnce(path, ctx.cwd, (message) => {
109
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
110
- });
111
- return;
253
+ // An edit/write is the one place a fresh publish can be usefully provoked:
254
+ // sync the file, await the server's next publish, and reset the file's
255
+ // delivered-set so a reintroduced issue resurfaces.
256
+ if (!event.isError && (event.toolName === "edit" || event.toolName === "write")) {
257
+ const path = (event.input as { path?: unknown }).path;
258
+ if (typeof path === "string") {
259
+ const target = resolveTarget(path, ctx.cwd);
260
+ reportRoutingIssuesOnce(ctx);
261
+ if (target) {
262
+ // withKeepAlive: in a one-shot run this await can be the only
263
+ // pending work, and everything the client holds is unref'd —
264
+ // without a ref the loop drains and pi exits mid-tool (keep-alive.ts).
265
+ await withKeepAlive(async () => {
266
+ const client = await clientFor(target);
267
+ if (client) {
268
+ tracker.clear(pathToUri(path));
269
+ forceDeltaScan();
270
+ await client.getDiagnostics(path, target.languageId);
271
+ } else {
272
+ reportFailureOnce(target, (message) => {
273
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
274
+ });
275
+ }
276
+ });
277
+ }
278
+ }
112
279
  }
113
280
 
114
- const errors = filterDiagnostics(await client.getDiagnostics(path), "error");
115
- if (errors.length === 0) return;
281
+ // Every tool round drains whatever is newly known — including dependents
282
+ // of an earlier edit whose diagnostics arrived while other tools ran.
283
+ // Diagnostics never wake an idle agent: a one-shot waits for the next
284
+ // request, whoever opens it.
285
+ const text = takePendingDelta(ctx.cwd);
286
+ if (text) queueNewDiagnostics(text);
287
+ });
116
288
 
117
- const relPath = relative(ctx.cwd, path) || path;
118
- const summary = formatDiagnostics(relPath, errors, AUTO_APPEND_LIMIT);
119
- const existing = event.content ?? [];
120
- return {
121
- content: [...existing, { type: "text", text: `\n<diagnostics>\n${summary}\n</diagnostics>` }],
122
- };
289
+ // Diagnostics that finished publishing while the agent was idle (session
290
+ // resume, a server still typechecking after the turn ended) ride the message
291
+ // that opens the next run. `agent_start`, not `before_agent_start`: pi emits
292
+ // the latter only from `prompt()`, so a run a harness notification opened
293
+ // (a `/loop` tick, an agent report) never fired it (STEERING-REVIEW H1).
294
+ pi.on("agent_start", (_event, ctx) => {
295
+ const text = takePendingDelta(ctx.cwd);
296
+ if (text) queueNewDiagnostics(text);
123
297
  });
124
298
 
125
299
  pi.registerTool({
@@ -127,7 +301,7 @@ export default function lspExtension(pi: ExtensionAPI) {
127
301
  label: "Diagnostics",
128
302
  ...ccToolRenderers("Diagnostics"),
129
303
  description:
130
- "Ask the language server for diagnostics (type errors, warnings) on a file. Reflects the file's current contents. Supported: TypeScript/JavaScript, Python, Go, Rust, Java — when that language's server is installed.",
304
+ "Ask the language server for diagnostics (type errors, warnings) on a file. Reflects the file's current contents. Supported: TypeScript/JavaScript, Python, Go, Rust, Java — plus any language a plugin's .lsp.json configures — when that language's server is installed.",
131
305
  parameters: Type.Object({
132
306
  path: Type.String({ description: "File to analyse (absolute or workspace-relative)" }),
133
307
  severity: Type.Optional(
@@ -136,19 +310,27 @@ export default function lspExtension(pi: ExtensionAPI) {
136
310
  }),
137
311
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
138
312
  const path = params.path.startsWith("/") ? params.path : `${ctx.cwd}/${params.path}`;
139
- const client = await clientFor(path, ctx.cwd);
140
- if (!client) {
141
- const resolved = resolveServer(path, ctx.cwd);
142
- const match = resolved?.match;
143
- const failure = resolved ? startFailures.get(resolved.key) : undefined;
313
+ const target = resolveTarget(path, ctx.cwd);
314
+ // One keep-alive spans both awaits (same pattern as the tool_result hook).
315
+ const fetched = target
316
+ ? await withKeepAlive(async () => {
317
+ const client = await clientFor(target);
318
+ return client && { all: await client.getDiagnostics(path, target.languageId) };
319
+ })
320
+ : undefined;
321
+ if (!target || !fetched) {
322
+ const failure = target ? startFailures.get(target.key) : undefined;
144
323
  return {
145
324
  content: [
146
325
  {
147
326
  type: "text",
148
327
  text:
149
328
  failure ??
150
- (match
151
- ? `No language server available for ${match.languageId}. Install ${match.config.command}.`
329
+ (target
330
+ ? `No language server available for ${target.languageId}. ` +
331
+ (INSTALL_HINTS[target.command]
332
+ ? `Install it with: ${INSTALL_HINTS[target.command]}`
333
+ : `Install ${target.command}.`)
152
334
  : `No language server is configured for this file type.`),
153
335
  },
154
336
  ],
@@ -156,16 +338,19 @@ export default function lspExtension(pi: ExtensionAPI) {
156
338
  // A language was recognised but its server is unavailable/failed —
157
339
  // that is an error, not a clean "no diagnostics". Only a genuinely
158
340
  // unsupported filetype (no match) is a non-error outcome.
159
- isError: Boolean(match),
341
+ isError: Boolean(target),
160
342
  };
161
343
  }
162
344
 
163
- const all = await client.getDiagnostics(path);
345
+ const { all } = fetched;
346
+ // The model sees these in the tool result now; don't re-deliver them
347
+ // as <new-diagnostics> on the next round.
348
+ tracker.markDelivered(pathToUri(path), all.map(fingerprintDiagnostic));
164
349
  const filtered = filterDiagnostics(all, (params.severity ?? "all") as SeverityFilter);
165
350
  const relPath = relative(ctx.cwd, path) || path;
166
351
  return {
167
352
  content: [{ type: "text", text: formatDiagnostics(relPath, filtered) }],
168
- details: { count: filtered.length, languageId: client.languageId },
353
+ details: { count: filtered.length, languageId: target.languageId },
169
354
  };
170
355
  },
171
356
  });
@@ -177,6 +362,10 @@ export default function lspExtension(pi: ExtensionAPI) {
177
362
  ([key, client]) => `${client.isRunning ? "running" : "stopped"} ${key} (${client.diagnosticsCount} diagnostics)`,
178
363
  );
179
364
  for (const [key, failure] of startFailures) lines.push(`failed ${key}: ${failure}`);
365
+ if (routing) {
366
+ for (const line of routing.diagnostics) lines.push(`config ${line}`);
367
+ for (const line of routing.collisions) lines.push(`routing ${line}`);
368
+ }
180
369
  ctx.ui.notify(lines.length ? lines.join("\n") : "No language servers started.", "info");
181
370
  },
182
371
  });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Turn a language-server spawn failure into an actionable message.
3
+ *
4
+ * A missing binary (spawn ENOENT) is the common, fixable case: the built-in
5
+ * table assumes its servers are on PATH, and Claude Code LSP plugins ship
6
+ * config only — never the binary (findings §18.3). The raw
7
+ * "spawn pyright-langserver ENOENT" tells the user nothing about what to do,
8
+ * so it is rewritten here with the install command when we know it, and with
9
+ * PATH/plugin guidance when we don't. Every other failure passes through
10
+ * unchanged. Enrichment happens once, where the failure is recorded, so the
11
+ * one-time warning, /lsp, and the lsp_diagnostics tool all show the same text.
12
+ */
13
+
14
+ /** Install commands for the built-in table's servers (and their npm siblings). */
15
+ export const INSTALL_HINTS: Record<string, string> = {
16
+ "typescript-language-server": "npm install -g typescript-language-server typescript",
17
+ "pyright-langserver": "npm install -g pyright",
18
+ gopls: "go install golang.org/x/tools/gopls@latest",
19
+ "rust-analyzer": "rustup component add rust-analyzer",
20
+ jdtls: "brew install jdtls",
21
+ };
22
+
23
+ /** True when the failure is a spawn ENOENT — the binary itself is absent. */
24
+ export function isMissingBinary(failure: string): boolean {
25
+ return /\bENOENT\b/.test(failure);
26
+ }
27
+
28
+ /**
29
+ * Rewrite a missing-binary failure into install guidance; return anything
30
+ * else verbatim. `pluginName` is set when a plugin's config (not the built-in
31
+ * table) named the command.
32
+ */
33
+ export function describeStartFailure(rawFailure: string, command: string, pluginName?: string): string {
34
+ if (!isMissingBinary(rawFailure)) return rawFailure;
35
+ const from = pluginName ? ` (configured by the ${pluginName} plugin)` : "";
36
+ const hint = INSTALL_HINTS[command];
37
+ if (hint) return `${command} is not installed${from}. Install it with: ${hint}`;
38
+ if (command.startsWith("/")) {
39
+ return `${command} does not exist${from}. ${pluginName ? "Check the plugin's installation." : "Check the configured path."}`;
40
+ }
41
+ const docs = pluginName ? " (the plugin's documentation should say how)" : "";
42
+ return `${command} is not installed${from}. Install it and make sure it is on your PATH${docs}.`;
43
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Hold the event loop open while language-server work is in flight. LspClient
3
+ * unrefs its child process, stdio pipes, and timeout timers (so a finished
4
+ * one-shot `pi -p` run can exit without waiting on an idle server); the flip
5
+ * side is that when an await on the LS is the ONLY pending work — exactly the
6
+ * case while a tool executes in a one-shot run — node's loop drains and the
7
+ * process exits 0 mid-await, silently ending the run right after
8
+ * tool_execution_start. Wrap each outermost LS await in this; keeping the ref
9
+ * at the call sites (not inside the client) preserves drain-on-idle for
10
+ * everything not actively awaited. docs/one-shot-lsp-event-loop-drain.md.
11
+ */
12
+ export async function withKeepAlive<T>(work: () => Promise<T>): Promise<T> {
13
+ const keepAlive = setInterval(() => {}, 1_000);
14
+ try {
15
+ return await work();
16
+ } finally {
17
+ clearInterval(keepAlive);
18
+ }
19
+ }