@dyyz1993/pi-coding-agent 0.74.24 → 0.74.27
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/CHANGELOG.md +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +5 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +8 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +310 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +446 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ChannelContract } from "@dyyz1993/pi-coding-agent";
|
|
2
|
+
import type { DiagnosticsModeName } from "./hooks/diagnostics-mode.js";
|
|
3
|
+
import type { LspDiagnostic } from "./utils/lsp-helpers.js";
|
|
4
|
+
|
|
5
|
+
export const LSP_CHANNEL_NAME = "lsp";
|
|
6
|
+
|
|
7
|
+
export interface LspServerSummary {
|
|
8
|
+
name: string;
|
|
9
|
+
fileTypes?: string[];
|
|
10
|
+
state: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
transport?: string;
|
|
13
|
+
activeCommand?: string[];
|
|
14
|
+
configuredCommand?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LspStatusResult {
|
|
18
|
+
state: string;
|
|
19
|
+
servers: LspServerSummary[];
|
|
20
|
+
mode: DiagnosticsModeName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LspSetModeResult {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
mode?: DiagnosticsModeName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LspStartupServerEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
state: string;
|
|
31
|
+
fileTypes?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LspServerStatusEntry {
|
|
35
|
+
name: string;
|
|
36
|
+
fileTypes?: string[];
|
|
37
|
+
status: {
|
|
38
|
+
state: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
transport?: string;
|
|
41
|
+
activeCommand?: string[];
|
|
42
|
+
configuredCommand?: string[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LspChannelContract extends ChannelContract {
|
|
47
|
+
methods: {
|
|
48
|
+
"lsp.setMode": {
|
|
49
|
+
params: { mode: string };
|
|
50
|
+
return: LspSetModeResult;
|
|
51
|
+
};
|
|
52
|
+
getActiveLanguages: {
|
|
53
|
+
params: Record<string, never>;
|
|
54
|
+
return: { languages: string[] };
|
|
55
|
+
};
|
|
56
|
+
getStatus: {
|
|
57
|
+
params: Record<string, never>;
|
|
58
|
+
return: LspStatusResult;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
events: {
|
|
62
|
+
diagnostics_update: {
|
|
63
|
+
event: "diagnostics_update";
|
|
64
|
+
timestamp: number;
|
|
65
|
+
filePath: string;
|
|
66
|
+
diagnostics: LspDiagnostic[];
|
|
67
|
+
};
|
|
68
|
+
mode_changed: {
|
|
69
|
+
event: "mode_changed";
|
|
70
|
+
timestamp: number;
|
|
71
|
+
mode: DiagnosticsModeName;
|
|
72
|
+
};
|
|
73
|
+
startup_begin: {
|
|
74
|
+
event: "startup_begin";
|
|
75
|
+
timestamp: number;
|
|
76
|
+
servers: LspStartupServerEntry[];
|
|
77
|
+
totalServers: number;
|
|
78
|
+
};
|
|
79
|
+
server_ready: {
|
|
80
|
+
event: "server_ready" | "server_error";
|
|
81
|
+
timestamp: number;
|
|
82
|
+
serverName: string;
|
|
83
|
+
servers: LspServerStatusEntry[];
|
|
84
|
+
};
|
|
85
|
+
language_activated: {
|
|
86
|
+
event: "language_activated";
|
|
87
|
+
timestamp: number;
|
|
88
|
+
serverName: string;
|
|
89
|
+
languages: string[];
|
|
90
|
+
};
|
|
91
|
+
status_changed: {
|
|
92
|
+
event: "status_changed";
|
|
93
|
+
timestamp: number;
|
|
94
|
+
servers: LspServerStatusEntry[];
|
|
95
|
+
state: string;
|
|
96
|
+
};
|
|
97
|
+
startup_complete: {
|
|
98
|
+
event: "startup_complete";
|
|
99
|
+
timestamp: number;
|
|
100
|
+
servers: LspServerStatusEntry[];
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readFile as fsReadFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
5
|
+
import type { FileTracker } from "../client/file-tracker.js";
|
|
6
|
+
import type { LspRuntimeRegistry } from "../client/registry.js";
|
|
7
|
+
import type { DependencyResolver } from "../utils/dependency-resolver.js";
|
|
8
|
+
import { waitForPushDiagnostics } from "../utils/diagnostics-wait.js";
|
|
9
|
+
import { extractPullDiagnostics, type LspDiagnostic, languageIdFromPath } from "../utils/lsp-helpers.js";
|
|
10
|
+
import type { DiagnosticsMode } from "./diagnostics-mode.js";
|
|
11
|
+
|
|
12
|
+
export interface FileDiagnostics {
|
|
13
|
+
filePath: string;
|
|
14
|
+
diagnostics: LspDiagnostic[];
|
|
15
|
+
source: "touched" | "dependent";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentEndHook {
|
|
19
|
+
register(pi: ExtensionAPI): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createAgentEndHook(
|
|
23
|
+
runtime: LspRuntimeRegistry,
|
|
24
|
+
mode: DiagnosticsMode,
|
|
25
|
+
fileTracker?: FileTracker,
|
|
26
|
+
dependencyResolver?: DependencyResolver,
|
|
27
|
+
onDiagnostics?: (results: FileDiagnostics[]) => void,
|
|
28
|
+
): AgentEndHook {
|
|
29
|
+
return {
|
|
30
|
+
register(pi: ExtensionAPI): void {
|
|
31
|
+
pi.on("agent_end", async () => {
|
|
32
|
+
await handleAgentEnd();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function handleAgentEnd(): Promise<void> {
|
|
38
|
+
if (mode.get() !== "agent_end") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const touchedFiles = mode.getTouchedFiles();
|
|
43
|
+
if (touchedFiles.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const allFilesToCheck = new Set<string>(touchedFiles);
|
|
48
|
+
|
|
49
|
+
if (dependencyResolver) {
|
|
50
|
+
try {
|
|
51
|
+
const dependents = await dependencyResolver.resolveDependents(touchedFiles);
|
|
52
|
+
for (const dep of dependents) {
|
|
53
|
+
allFilesToCheck.add(dep);
|
|
54
|
+
}
|
|
55
|
+
if (dependents.length > 0) {
|
|
56
|
+
console.debug(
|
|
57
|
+
`[lsp] dependency expansion: ${touchedFiles.length} touched -> ${dependents.length} dependents -> ${allFilesToCheck.size} total`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.debug("[lsp] dependency resolution failed:", err instanceof Error ? err.message : err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results: FileDiagnostics[] = [];
|
|
66
|
+
|
|
67
|
+
for (const filePath of allFilesToCheck) {
|
|
68
|
+
try {
|
|
69
|
+
const source = touchedFiles.includes(filePath) ? "touched" as const : "dependent" as const;
|
|
70
|
+
const diagnostics = await runDiagnosticsForFile(filePath);
|
|
71
|
+
if (diagnostics.length > 0) {
|
|
72
|
+
results.push({ filePath, diagnostics, source });
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.debug("[lsp] diagnostics for file failed:", filePath, err instanceof Error ? err.message : err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mode.clearTouchedFiles();
|
|
80
|
+
|
|
81
|
+
if (results.length > 0) {
|
|
82
|
+
onDiagnostics?.(results);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runDiagnosticsForFile(filePath: string): Promise<LspDiagnostic[]> {
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
const uri = pathToFileURL(resolve(cwd, filePath)).href;
|
|
89
|
+
|
|
90
|
+
if (fileTracker) {
|
|
91
|
+
fileTracker.open(filePath, (evictedFile) => {
|
|
92
|
+
const evictedUri = pathToFileURL(resolve(cwd, evictedFile)).href;
|
|
93
|
+
runtime.notify("textDocument/didClose", { textDocument: { uri: evictedUri } }, { path: evictedFile });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let fileContent = "";
|
|
98
|
+
try {
|
|
99
|
+
fileContent = await fsReadFile(resolve(cwd, filePath), "utf8");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.debug("[lsp] file read for diagnostics failed:", err instanceof Error ? err.message : err);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
runtime.notify(
|
|
105
|
+
"textDocument/didOpen",
|
|
106
|
+
{
|
|
107
|
+
textDocument: {
|
|
108
|
+
uri,
|
|
109
|
+
languageId: languageIdFromPath(filePath),
|
|
110
|
+
version: Date.now(),
|
|
111
|
+
text: fileContent,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{ path: filePath },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await waitForPushDiagnostics(runtime, filePath);
|
|
118
|
+
|
|
119
|
+
let diagnostics = runtime.getPublishedDiagnostics(filePath);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const allResults = await runtime.requestAll(
|
|
123
|
+
"textDocument/diagnostic",
|
|
124
|
+
{ textDocument: { uri } },
|
|
125
|
+
{ path: filePath, timeoutMs: 8000 },
|
|
126
|
+
);
|
|
127
|
+
for (const result of allResults) {
|
|
128
|
+
if (!result) continue;
|
|
129
|
+
const pulled = extractPullDiagnostics(result);
|
|
130
|
+
if (pulled.length > 0) {
|
|
131
|
+
diagnostics = diagnostics.concat(pulled);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.debug("[lsp] pull diagnostics request failed:", err instanceof Error ? err.message : err);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return diagnostics;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function summarizeDiagnostics(diagnostics: Array<{ severity?: number }>): string {
|
|
143
|
+
if (diagnostics.length === 0) {
|
|
144
|
+
return "no diagnostics";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let errors = 0;
|
|
148
|
+
let warnings = 0;
|
|
149
|
+
let infos = 0;
|
|
150
|
+
for (const diagnostic of diagnostics) {
|
|
151
|
+
switch (diagnostic.severity) {
|
|
152
|
+
case 1:
|
|
153
|
+
errors += 1;
|
|
154
|
+
break;
|
|
155
|
+
case 2:
|
|
156
|
+
warnings += 1;
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
infos += 1;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const parts = [`${diagnostics.length} diagnostics`];
|
|
165
|
+
if (errors > 0) parts.push(`${errors} errors`);
|
|
166
|
+
if (warnings > 0) parts.push(`${warnings} warnings`);
|
|
167
|
+
if (infos > 0) parts.push(`${infos} info`);
|
|
168
|
+
return parts.join(", ");
|
|
169
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DiagnosticsModeName = "agent_end" | "edit_write" | "disabled";
|
|
2
|
+
export interface DiagnosticsMode {
|
|
3
|
+
get(): DiagnosticsModeName;
|
|
4
|
+
set(mode: DiagnosticsModeName): void;
|
|
5
|
+
addTouchedFile(filePath: string): void;
|
|
6
|
+
getTouchedFiles(): string[];
|
|
7
|
+
clearTouchedFiles(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createDiagnosticsMode(initial?: DiagnosticsModeName): DiagnosticsMode;
|
|
10
|
+
//# sourceMappingURL=diagnostics-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics-mode.d.ts","sourceRoot":"","sources":["diagnostics-mode.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,CAAC;AAI1E,MAAM,WAAW,eAAe;IAC/B,GAAG,IAAI,mBAAmB,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACrC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,eAAe,IAAI,MAAM,EAAE,CAAC;IAC5B,iBAAiB,IAAI,IAAI,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,eAAe,CA4BpF","sourcesContent":["export type DiagnosticsModeName = \"agent_end\" | \"edit_write\" | \"disabled\";\n\nconst VALID_MODES: DiagnosticsModeName[] = [\"agent_end\", \"edit_write\", \"disabled\"];\n\nexport interface DiagnosticsMode {\n\tget(): DiagnosticsModeName;\n\tset(mode: DiagnosticsModeName): void;\n\taddTouchedFile(filePath: string): void;\n\tgetTouchedFiles(): string[];\n\tclearTouchedFiles(): void;\n}\n\nexport function createDiagnosticsMode(initial?: DiagnosticsModeName): DiagnosticsMode {\n\tlet current: DiagnosticsModeName = VALID_MODES.includes(initial!) ? initial! : \"agent_end\";\n\tconst touchedFiles: string[] = [];\n\tconst touchedSet = new Set<string>();\n\n\treturn {\n\t\tget(): DiagnosticsModeName {\n\t\t\treturn current;\n\t\t},\n\t\tset(mode: DiagnosticsModeName): void {\n\t\t\tif (VALID_MODES.includes(mode)) {\n\t\t\t\tcurrent = mode;\n\t\t\t}\n\t\t},\n\t\taddTouchedFile(filePath: string): void {\n\t\t\tif (!touchedSet.has(filePath)) {\n\t\t\t\ttouchedSet.add(filePath);\n\t\t\t\ttouchedFiles.push(filePath);\n\t\t\t}\n\t\t},\n\t\tgetTouchedFiles(): string[] {\n\t\t\treturn [...touchedFiles];\n\t\t},\n\t\tclearTouchedFiles(): void {\n\t\t\ttouchedFiles.length = 0;\n\t\t\ttouchedSet.clear();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const VALID_MODES = ["agent_end", "edit_write", "disabled"];
|
|
2
|
+
export function createDiagnosticsMode(initial) {
|
|
3
|
+
let current = VALID_MODES.includes(initial) ? initial : "agent_end";
|
|
4
|
+
const touchedFiles = [];
|
|
5
|
+
const touchedSet = new Set();
|
|
6
|
+
return {
|
|
7
|
+
get() {
|
|
8
|
+
return current;
|
|
9
|
+
},
|
|
10
|
+
set(mode) {
|
|
11
|
+
if (VALID_MODES.includes(mode)) {
|
|
12
|
+
current = mode;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
addTouchedFile(filePath) {
|
|
16
|
+
if (!touchedSet.has(filePath)) {
|
|
17
|
+
touchedSet.add(filePath);
|
|
18
|
+
touchedFiles.push(filePath);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
getTouchedFiles() {
|
|
22
|
+
return [...touchedFiles];
|
|
23
|
+
},
|
|
24
|
+
clearTouchedFiles() {
|
|
25
|
+
touchedFiles.length = 0;
|
|
26
|
+
touchedSet.clear();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=diagnostics-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics-mode.js","sourceRoot":"","sources":["diagnostics-mode.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAA0B,CAAC,WAAW,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAUnF,MAAM,UAAU,qBAAqB,CAAC,OAA6B,EAAmB;IACrF,IAAI,OAAO,GAAwB,WAAW,CAAC,QAAQ,CAAC,OAAQ,CAAC,CAAC,CAAC,CAAC,OAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;IAC3F,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IAErC,OAAO;QACN,GAAG,GAAwB;YAC1B,OAAO,OAAO,CAAC;QAAA,CACf;QACD,GAAG,CAAC,IAAyB,EAAQ;YACpC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChC,OAAO,GAAG,IAAI,CAAC;YAChB,CAAC;QAAA,CACD;QACD,cAAc,CAAC,QAAgB,EAAQ;YACtC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACzB,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,CAAC;QAAA,CACD;QACD,eAAe,GAAa;YAC3B,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC;QAAA,CACzB;QACD,iBAAiB,GAAS;YACzB,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;YACxB,UAAU,CAAC,KAAK,EAAE,CAAC;QAAA,CACnB;KACD,CAAC;AAAA,CACF","sourcesContent":["export type DiagnosticsModeName = \"agent_end\" | \"edit_write\" | \"disabled\";\n\nconst VALID_MODES: DiagnosticsModeName[] = [\"agent_end\", \"edit_write\", \"disabled\"];\n\nexport interface DiagnosticsMode {\n\tget(): DiagnosticsModeName;\n\tset(mode: DiagnosticsModeName): void;\n\taddTouchedFile(filePath: string): void;\n\tgetTouchedFiles(): string[];\n\tclearTouchedFiles(): void;\n}\n\nexport function createDiagnosticsMode(initial?: DiagnosticsModeName): DiagnosticsMode {\n\tlet current: DiagnosticsModeName = VALID_MODES.includes(initial!) ? initial! : \"agent_end\";\n\tconst touchedFiles: string[] = [];\n\tconst touchedSet = new Set<string>();\n\n\treturn {\n\t\tget(): DiagnosticsModeName {\n\t\t\treturn current;\n\t\t},\n\t\tset(mode: DiagnosticsModeName): void {\n\t\t\tif (VALID_MODES.includes(mode)) {\n\t\t\t\tcurrent = mode;\n\t\t\t}\n\t\t},\n\t\taddTouchedFile(filePath: string): void {\n\t\t\tif (!touchedSet.has(filePath)) {\n\t\t\t\ttouchedSet.add(filePath);\n\t\t\t\ttouchedFiles.push(filePath);\n\t\t\t}\n\t\t},\n\t\tgetTouchedFiles(): string[] {\n\t\t\treturn [...touchedFiles];\n\t\t},\n\t\tclearTouchedFiles(): void {\n\t\t\ttouchedFiles.length = 0;\n\t\t\ttouchedSet.clear();\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type DiagnosticsModeName = "agent_end" | "edit_write" | "disabled";
|
|
2
|
+
|
|
3
|
+
const VALID_MODES: DiagnosticsModeName[] = ["agent_end", "edit_write", "disabled"];
|
|
4
|
+
|
|
5
|
+
export interface DiagnosticsMode {
|
|
6
|
+
get(): DiagnosticsModeName;
|
|
7
|
+
set(mode: DiagnosticsModeName): void;
|
|
8
|
+
addTouchedFile(filePath: string): void;
|
|
9
|
+
getTouchedFiles(): string[];
|
|
10
|
+
clearTouchedFiles(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createDiagnosticsMode(initial?: DiagnosticsModeName): DiagnosticsMode {
|
|
14
|
+
let current: DiagnosticsModeName = VALID_MODES.includes(initial!) ? initial! : "agent_end";
|
|
15
|
+
const touchedFiles: string[] = [];
|
|
16
|
+
const touchedSet = new Set<string>();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
get(): DiagnosticsModeName {
|
|
20
|
+
return current;
|
|
21
|
+
},
|
|
22
|
+
set(mode: DiagnosticsModeName): void {
|
|
23
|
+
if (VALID_MODES.includes(mode)) {
|
|
24
|
+
current = mode;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
addTouchedFile(filePath: string): void {
|
|
28
|
+
if (!touchedSet.has(filePath)) {
|
|
29
|
+
touchedSet.add(filePath);
|
|
30
|
+
touchedFiles.push(filePath);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
getTouchedFiles(): string[] {
|
|
34
|
+
return [...touchedFiles];
|
|
35
|
+
},
|
|
36
|
+
clearTouchedFiles(): void {
|
|
37
|
+
touchedFiles.length = 0;
|
|
38
|
+
touchedSet.clear();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@dyyz1993/pi-coding-agent";
|
|
5
|
+
import type { FileTracker } from "../client/file-tracker.js";
|
|
6
|
+
import type { LspRuntimeRegistry } from "../client/registry.js";
|
|
7
|
+
import {
|
|
8
|
+
extractPullDiagnostics,
|
|
9
|
+
type LspDiagnostic,
|
|
10
|
+
type LspPosition,
|
|
11
|
+
type LspRange,
|
|
12
|
+
languageIdFromPath,
|
|
13
|
+
normalizeRange,
|
|
14
|
+
} from "../utils/lsp-helpers.js";
|
|
15
|
+
import type { DiagnosticsMode } from "./diagnostics-mode.js";
|
|
16
|
+
import { waitForPushDiagnostics } from "../utils/diagnostics-wait.js";
|
|
17
|
+
|
|
18
|
+
interface LspTextEdit {
|
|
19
|
+
range: LspRange;
|
|
20
|
+
newText: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WriteThroughHooks {
|
|
24
|
+
register(pi: ExtensionAPI): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WriteThroughOptions {
|
|
28
|
+
cwd?: string;
|
|
29
|
+
formatOnWrite?: boolean;
|
|
30
|
+
diagnosticsOnWrite?: boolean;
|
|
31
|
+
formattingOptions?: {
|
|
32
|
+
tabSize?: number;
|
|
33
|
+
insertSpaces?: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createWriteThroughHooks(
|
|
38
|
+
runtime: LspRuntimeRegistry,
|
|
39
|
+
options: WriteThroughOptions = {},
|
|
40
|
+
mode?: DiagnosticsMode,
|
|
41
|
+
fileTracker?: FileTracker,
|
|
42
|
+
): WriteThroughHooks {
|
|
43
|
+
const cwd = options.cwd ?? process.cwd();
|
|
44
|
+
const formatOnWrite = options.formatOnWrite ?? true;
|
|
45
|
+
const diagnosticsOnWrite = options.diagnosticsOnWrite ?? true;
|
|
46
|
+
const formattingOptions = {
|
|
47
|
+
tabSize: options.formattingOptions?.tabSize ?? 2,
|
|
48
|
+
insertSpaces: options.formattingOptions?.insertSpaces ?? true,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
register(pi: ExtensionAPI): void {
|
|
53
|
+
pi.on("tool_result", async (event: any, ctx: any) => {
|
|
54
|
+
return await maybeHandleWriteThrough(event, ctx);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async function maybeHandleWriteThrough(event: ToolResultEvent, ctx: ExtensionContext) {
|
|
60
|
+
if (event.isError) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.toolName !== "write" && event.toolName !== "edit") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentMode = mode?.get() ?? "edit_write";
|
|
68
|
+
|
|
69
|
+
if (currentMode === "disabled") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const filePath = getToolInputPath(event.input);
|
|
74
|
+
if (!filePath) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pathStatus = runtime.getStatusForPath(filePath);
|
|
79
|
+
if (!pathStatus || pathStatus.state !== "ready") {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const uri = pathToFileURL(resolve(cwd, filePath)).href;
|
|
84
|
+
const summaries: string[] = [];
|
|
85
|
+
|
|
86
|
+
if (fileTracker) {
|
|
87
|
+
fileTracker.open(filePath, (evictedFile) => {
|
|
88
|
+
const evictedUri = pathToFileURL(resolve(cwd, evictedFile)).href;
|
|
89
|
+
runtime.notify("textDocument/didClose", { textDocument: { uri: evictedUri } }, { path: evictedFile });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const fileContent = await fsReadFile(resolve(cwd, filePath), "utf8");
|
|
95
|
+
runtime.notify(
|
|
96
|
+
"textDocument/didOpen",
|
|
97
|
+
{
|
|
98
|
+
textDocument: {
|
|
99
|
+
uri,
|
|
100
|
+
languageId: languageIdFromPath(filePath),
|
|
101
|
+
version: Date.now(),
|
|
102
|
+
text: fileContent,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{ path: filePath },
|
|
106
|
+
);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.debug("[lsp] didOpen failed:", err instanceof Error ? err.message : err);
|
|
109
|
+
summaries.push("LSP didOpen failed");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (formatOnWrite) {
|
|
113
|
+
try {
|
|
114
|
+
const rawEdits = await runtime.request(
|
|
115
|
+
"textDocument/formatting",
|
|
116
|
+
{
|
|
117
|
+
textDocument: { uri },
|
|
118
|
+
options: formattingOptions,
|
|
119
|
+
},
|
|
120
|
+
{ path: filePath },
|
|
121
|
+
);
|
|
122
|
+
const edits = normalizeTextEdits(rawEdits);
|
|
123
|
+
const applied = await applyFormattingEdits(filePath, edits, cwd);
|
|
124
|
+
summaries.push(applied > 0 ? `formatted (${applied} edits)` : "no formatting changes");
|
|
125
|
+
} catch (error) {
|
|
126
|
+
summaries.push(`format failed: ${toErrorMessage(error)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (currentMode === "agent_end") {
|
|
131
|
+
mode?.addTouchedFile(filePath);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (diagnosticsOnWrite) {
|
|
136
|
+
await waitForPushDiagnostics(runtime, filePath);
|
|
137
|
+
let diagnostics = runtime.getPublishedDiagnostics(filePath);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const results = await runtime.requestAll(
|
|
141
|
+
"textDocument/diagnostic",
|
|
142
|
+
{ textDocument: { uri } },
|
|
143
|
+
{ path: filePath, timeoutMs: 8000 },
|
|
144
|
+
);
|
|
145
|
+
for (const result of results) {
|
|
146
|
+
if (!result) continue;
|
|
147
|
+
const pulled = extractPullDiagnostics(result);
|
|
148
|
+
if (pulled.length > 0) {
|
|
149
|
+
diagnostics = diagnostics.concat(pulled);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.debug("[lsp] pull diagnostics failed:", err instanceof Error ? err.message : err);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const diagSummary = summarizeDiagnostics(diagnostics);
|
|
157
|
+
summaries.push(diagSummary);
|
|
158
|
+
|
|
159
|
+
if (diagnostics.length > 0) {
|
|
160
|
+
const diagText = formatDiagnosticsForToolResult(filePath, diagnostics);
|
|
161
|
+
const fileSummaries = [
|
|
162
|
+
{
|
|
163
|
+
filePath,
|
|
164
|
+
summary: diagSummary,
|
|
165
|
+
issues: diagnostics.map((d) => ({
|
|
166
|
+
severity: d.severity,
|
|
167
|
+
line: d.range.start.line + 1,
|
|
168
|
+
message: d.message,
|
|
169
|
+
source: d.source,
|
|
170
|
+
code: d.code,
|
|
171
|
+
})),
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
return {
|
|
175
|
+
content: [...event.content, { type: "text" as const, text: `\n[LSP] ${diagSummary}\n${diagText}` }],
|
|
176
|
+
details: { files: fileSummaries },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getToolInputPath(input: Record<string, unknown>): string | undefined {
|
|
184
|
+
const value = input.path;
|
|
185
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeTextEdits(raw: unknown): LspTextEdit[] {
|
|
189
|
+
if (!Array.isArray(raw)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const edits: LspTextEdit[] = [];
|
|
194
|
+
for (const entry of raw) {
|
|
195
|
+
if (!entry || typeof entry !== "object") {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const edit = entry as Record<string, unknown>;
|
|
199
|
+
if (typeof edit.newText !== "string") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const range = normalizeRange(edit.range);
|
|
203
|
+
if (!range) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
edits.push({ range, newText: edit.newText });
|
|
207
|
+
}
|
|
208
|
+
return edits;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function applyFormattingEdits(filePath: string, edits: LspTextEdit[], cwd: string): Promise<number> {
|
|
212
|
+
if (edits.length === 0) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fullPath = resolve(cwd, filePath);
|
|
217
|
+
const originalText = await fsReadFile(fullPath, "utf8");
|
|
218
|
+
const nextText = applyTextEdits(originalText, edits);
|
|
219
|
+
if (nextText === originalText) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fsWriteFile(fullPath, nextText, "utf8");
|
|
224
|
+
return edits.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyTextEdits(text: string, edits: LspTextEdit[]): string {
|
|
228
|
+
const lineStarts = computeLineStarts(text);
|
|
229
|
+
const normalized = edits
|
|
230
|
+
.map((edit) => {
|
|
231
|
+
const start = positionToOffset(lineStarts, text.length, edit.range.start);
|
|
232
|
+
const end = positionToOffset(lineStarts, text.length, edit.range.end);
|
|
233
|
+
return {
|
|
234
|
+
start,
|
|
235
|
+
end,
|
|
236
|
+
newText: edit.newText,
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
.filter((edit) => edit.start <= edit.end)
|
|
240
|
+
.sort((left, right) => {
|
|
241
|
+
if (left.start !== right.start) {
|
|
242
|
+
return right.start - left.start;
|
|
243
|
+
}
|
|
244
|
+
return right.end - left.end;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (normalized.length === 0) {
|
|
248
|
+
return text;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const parts: string[] = [];
|
|
252
|
+
let cursor = text.length;
|
|
253
|
+
for (const edit of normalized) {
|
|
254
|
+
if (edit.end > cursor) {
|
|
255
|
+
// Defensive fallback for malformed/overlapping edit ranges.
|
|
256
|
+
let output = text;
|
|
257
|
+
for (const fallbackEdit of normalized) {
|
|
258
|
+
output = `${output.slice(0, fallbackEdit.start)}${fallbackEdit.newText}${output.slice(fallbackEdit.end)}`;
|
|
259
|
+
}
|
|
260
|
+
return output;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
parts.push(text.slice(edit.end, cursor));
|
|
264
|
+
parts.push(edit.newText);
|
|
265
|
+
cursor = edit.start;
|
|
266
|
+
}
|
|
267
|
+
parts.push(text.slice(0, cursor));
|
|
268
|
+
return parts.reverse().join("");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function computeLineStarts(text: string): number[] {
|
|
272
|
+
const lineStarts = [0];
|
|
273
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
274
|
+
if (text[index] === "\n") {
|
|
275
|
+
lineStarts.push(index + 1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return lineStarts;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function positionToOffset(lineStarts: number[], textLength: number, position: LspPosition): number {
|
|
282
|
+
const safeLine = Math.max(0, position.line);
|
|
283
|
+
const safeChar = Math.max(0, position.character);
|
|
284
|
+
const lineStart = safeLine < lineStarts.length ? lineStarts[safeLine] : textLength;
|
|
285
|
+
return Math.min(textLength, lineStart + safeChar);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function summarizeDiagnostics(diagnostics: LspDiagnostic[]): string {
|
|
289
|
+
if (diagnostics.length === 0) {
|
|
290
|
+
return "no diagnostics";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let errors = 0;
|
|
294
|
+
let warnings = 0;
|
|
295
|
+
let infos = 0;
|
|
296
|
+
for (const diagnostic of diagnostics) {
|
|
297
|
+
switch (diagnostic.severity) {
|
|
298
|
+
case 1:
|
|
299
|
+
errors += 1;
|
|
300
|
+
break;
|
|
301
|
+
case 2:
|
|
302
|
+
warnings += 1;
|
|
303
|
+
break;
|
|
304
|
+
default:
|
|
305
|
+
infos += 1;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const parts = [`${diagnostics.length} diagnostics`];
|
|
311
|
+
if (errors > 0) parts.push(`${errors} errors`);
|
|
312
|
+
if (warnings > 0) parts.push(`${warnings} warnings`);
|
|
313
|
+
if (infos > 0) parts.push(`${infos} info`);
|
|
314
|
+
return parts.join(", ");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toErrorMessage(error: unknown): string {
|
|
318
|
+
if (error instanceof Error) {
|
|
319
|
+
return error.message;
|
|
320
|
+
}
|
|
321
|
+
return String(error);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const SEVERITY_LABELS: Record<number, string> = {
|
|
325
|
+
1: "Error",
|
|
326
|
+
2: "Warning",
|
|
327
|
+
3: "Information",
|
|
328
|
+
4: "Hint",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function formatDiagnosticsForToolResult(filePath: string, diagnostics: LspDiagnostic[]): string {
|
|
332
|
+
const lines = [`File: ${filePath}`, `Found ${diagnostics.length} issue(s):`];
|
|
333
|
+
for (const diag of diagnostics) {
|
|
334
|
+
const severity = SEVERITY_LABELS[diag.severity ?? 1] ?? "Issue";
|
|
335
|
+
const loc = `L${diag.range.start.line + 1}:C${diag.range.start.character}`;
|
|
336
|
+
const source = diag.source ? ` [${diag.source}]` : "";
|
|
337
|
+
const code = diag.code !== undefined ? ` (${diag.code})` : "";
|
|
338
|
+
lines.push(` ${severity}${source}${code}: ${loc} - ${diag.message}`);
|
|
339
|
+
}
|
|
340
|
+
lines.push("Consider fixing these issues before proceeding.");
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|