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.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
package/extensions/lsp/index.ts
CHANGED
|
@@ -1,125 +1,299 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lsp extension — Claude Code
|
|
2
|
+
* lsp extension — language-server diagnostics, the way Claude Code surfaces them.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* a
|
|
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.
|
|
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 {
|
|
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
|
|
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 {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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.
|
|
57
|
-
//
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
const reportFailureOnce = (
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
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
|
|
95
|
-
notify(`LSP unavailable for ${
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
(
|
|
151
|
-
? `No language server available for ${
|
|
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(
|
|
341
|
+
isError: Boolean(target),
|
|
160
342
|
};
|
|
161
343
|
}
|
|
162
344
|
|
|
163
|
-
const all =
|
|
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:
|
|
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
|
+
}
|