pi-x-ide 1.15.2 → 1.16.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/CHANGELOG.md +7 -0
- package/dist/src/pi/commands.js +14 -19
- package/dist/src/pi/connection.js +30 -38
- package/dist/src/pi/context.js +16 -21
- package/dist/src/pi/diagnostic-renderer.js +71 -0
- package/dist/src/pi/diagnostics.js +28 -25
- package/dist/src/pi/discovery.js +26 -31
- package/dist/src/pi/ide-host.js +17 -24
- package/dist/src/pi/index.js +63 -64
- package/dist/src/pi/install.js +35 -68
- package/dist/src/pi/reconnect.js +6 -12
- package/dist/src/pi/safety.js +10 -15
- package/dist/src/pi/state.js +1 -4
- package/dist/src/pi/ui.js +28 -34
- package/dist/src/pi/zed.js +64 -74
- package/dist/src/shared/config-options.js +6 -10
- package/dist/src/shared/config.js +16 -24
- package/dist/src/shared/display-width.js +2 -6
- package/dist/src/shared/errors.js +5 -12
- package/dist/src/shared/format.js +17 -28
- package/dist/src/shared/ide-server.js +20 -57
- package/dist/src/shared/lock-file.js +21 -29
- package/dist/src/shared/paths.js +30 -40
- package/dist/src/shared/platform.js +13 -19
- package/dist/src/shared/protocol.js +3 -6
- package/dist/src/shared/schema.js +9 -20
- package/dist/src/shared/version.js +1 -0
- package/dist/src/shared/ws.js +5 -8
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.16.0](https://github.com/balaenis/pi-x-ide/compare/v1.15.2...v1.16.0) (2026-06-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **pi:** render diagnostic-fix requests as custom TUI messages ([82d1821](https://github.com/balaenis/pi-x-ide/commit/82d18219cdfdfbb48689d6812db093396f229f4d))
|
|
9
|
+
|
|
3
10
|
## [1.15.2](https://github.com/balaenis/pi-x-ide/compare/v1.15.1...v1.15.2) (2026-06-29)
|
|
4
11
|
|
|
5
12
|
|
package/dist/src/pi/commands.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const format_1 = require("../shared/format");
|
|
8
|
-
const safety_1 = require("./safety");
|
|
9
|
-
const ui_1 = require("./ui");
|
|
10
|
-
exports.PI_X_IDE_ATTACH_SHORTCUT_ENV = "PI_X_IDE_ATTACH_SHORTCUT";
|
|
11
|
-
exports.DEFAULT_ATTACH_SHORTCUT = "ctrl+alt+k";
|
|
1
|
+
import { resolvePiConfigEnv } from "../shared/config.js";
|
|
2
|
+
import { formatRangeMention } from "../shared/format.js";
|
|
3
|
+
import { runPiBoundary, runPiBoundaryAsync } from "./safety.js";
|
|
4
|
+
import { buildWidget, updateIdeUi } from "./ui.js";
|
|
5
|
+
export const PI_X_IDE_ATTACH_SHORTCUT_ENV = "PI_X_IDE_ATTACH_SHORTCUT";
|
|
6
|
+
export const DEFAULT_ATTACH_SHORTCUT = "ctrl+alt+k";
|
|
12
7
|
const DISABLED_ATTACH_SHORTCUT_VALUES = new Set(["", "0", "false", "off", "none", "disabled"]);
|
|
13
|
-
function registerIdeCommand(pi, runtime, actions, options = {}) {
|
|
8
|
+
export function registerIdeCommand(pi, runtime, actions, options = {}) {
|
|
14
9
|
const attachShortcut = resolveAttachShortcut(options.env);
|
|
15
10
|
if (attachShortcut) {
|
|
16
11
|
pi.registerShortcut(attachShortcut, {
|
|
17
12
|
description: "Attach latest IDE selection to the prompt",
|
|
18
13
|
handler: (ctx) => {
|
|
19
|
-
|
|
14
|
+
runPiBoundary("IDE attach shortcut", runtime, () => {
|
|
20
15
|
runtime.ctx = ctx;
|
|
21
16
|
attachLatest(runtime, ctx);
|
|
22
17
|
}, ctx);
|
|
@@ -37,7 +32,7 @@ function registerIdeCommand(pi, runtime, actions, options = {}) {
|
|
|
37
32
|
const filtered = subcommands.filter((s) => s.value.startsWith(argumentPrefix));
|
|
38
33
|
return filtered.length > 0 ? filtered : null;
|
|
39
34
|
},
|
|
40
|
-
handler: (args, ctx) =>
|
|
35
|
+
handler: (args, ctx) => runPiBoundaryAsync("/ide command", runtime, async () => {
|
|
41
36
|
runtime.ctx = ctx;
|
|
42
37
|
const [subcommand] = args.trim().split(/\s+/, 1);
|
|
43
38
|
switch (subcommand || "") {
|
|
@@ -92,7 +87,7 @@ async function showPicker(runtime, actions, ctx) {
|
|
|
92
87
|
}
|
|
93
88
|
}
|
|
94
89
|
function showStatus(runtime, ctx) {
|
|
95
|
-
const lines =
|
|
90
|
+
const lines = buildWidget(runtime, ctx.cwd) ?? ["IDE: disconnected"];
|
|
96
91
|
ctx.ui.notify(lines.join("\n"), runtime.connectionStatus === "error" ? "error" : "info");
|
|
97
92
|
}
|
|
98
93
|
async function listCandidates(actions, ctx) {
|
|
@@ -105,8 +100,8 @@ async function listCandidates(actions, ctx) {
|
|
|
105
100
|
.map((candidate, index) => `${index + 1}. ${candidate.lock.name} ${candidate.lock.host}:${candidate.lock.port}\n ${candidate.workspaceFolder}\n ${candidate.path}`)
|
|
106
101
|
.join("\n"), "info");
|
|
107
102
|
}
|
|
108
|
-
function resolveAttachShortcut(env = process.env) {
|
|
109
|
-
const value =
|
|
103
|
+
export function resolveAttachShortcut(env = process.env) {
|
|
104
|
+
const value = resolvePiConfigEnv(env)[PI_X_IDE_ATTACH_SHORTCUT_ENV] ?? DEFAULT_ATTACH_SHORTCUT;
|
|
110
105
|
const normalized = value.trim().toLowerCase();
|
|
111
106
|
if (DISABLED_ATTACH_SHORTCUT_VALUES.has(normalized))
|
|
112
107
|
return undefined;
|
|
@@ -117,9 +112,9 @@ function attachLatest(runtime, ctx) {
|
|
|
117
112
|
ctx.ui.notify("No IDE selection is available.", "warning");
|
|
118
113
|
return;
|
|
119
114
|
}
|
|
120
|
-
const mention =
|
|
115
|
+
const mention = formatRangeMention(runtime.latestSelection, { cwd: ctx.cwd });
|
|
121
116
|
ctx.ui.pasteToEditor(mention);
|
|
122
117
|
runtime.attachState = "pending";
|
|
123
|
-
|
|
118
|
+
updateIdeUi(runtime, ctx);
|
|
124
119
|
ctx.ui.notify(`Attached ${mention}`, "info");
|
|
125
120
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const format_1 = require("../shared/format");
|
|
13
|
-
const platform_1 = require("../shared/platform");
|
|
14
|
-
const ws_2 = require("../shared/ws");
|
|
15
|
-
const ide_host_1 = require("./ide-host");
|
|
16
|
-
exports.IDE_CONNECT_TIMEOUT_MS = 5_000;
|
|
17
|
-
class IdeConnectionTimeoutError extends Error {
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { EXT_CONFIG_NAME } from "../shared/config.js";
|
|
3
|
+
import { AUTH_HEADER, PROTOCOL_VERSION, } from "../shared/protocol.js";
|
|
4
|
+
import { isAtMentionedParams, isDiagnosticFixRequestedParams, isSelectionChangedParams, isSelectionClearedParams, } from "../shared/schema.js";
|
|
5
|
+
import { toError, logExtensionError } from "../shared/errors.js";
|
|
6
|
+
import { formatRangeMention } from "../shared/format.js";
|
|
7
|
+
import { normalizeEditorSelectionSnapshotForHost } from "../shared/platform.js";
|
|
8
|
+
import { decodeRawData } from "../shared/ws.js";
|
|
9
|
+
import { resolveIdeHost } from "./ide-host.js";
|
|
10
|
+
export const IDE_CONNECT_TIMEOUT_MS = 5_000;
|
|
11
|
+
export class IdeConnectionTimeoutError extends Error {
|
|
18
12
|
candidate;
|
|
19
13
|
host;
|
|
20
14
|
constructor(candidate, host = candidate.lock.host) {
|
|
@@ -24,8 +18,7 @@ class IdeConnectionTimeoutError extends Error {
|
|
|
24
18
|
this.name = "IdeConnectionTimeoutError";
|
|
25
19
|
}
|
|
26
20
|
}
|
|
27
|
-
|
|
28
|
-
class IdeConnection {
|
|
21
|
+
export class IdeConnection {
|
|
29
22
|
candidate;
|
|
30
23
|
cwd;
|
|
31
24
|
callbacks;
|
|
@@ -40,20 +33,20 @@ class IdeConnection {
|
|
|
40
33
|
this.options = options;
|
|
41
34
|
}
|
|
42
35
|
get isOpen() {
|
|
43
|
-
return this.socket?.readyState ===
|
|
36
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
44
37
|
}
|
|
45
|
-
async connect(timeoutMs =
|
|
38
|
+
async connect(timeoutMs = IDE_CONNECT_TIMEOUT_MS) {
|
|
46
39
|
this.closedByUser = false;
|
|
47
40
|
const { lock } = this.candidate;
|
|
48
|
-
const resolvedHost = await (this.options.resolveHost ??
|
|
49
|
-
const socket = new
|
|
41
|
+
const resolvedHost = await (this.options.resolveHost ?? resolveIdeHost)(lock);
|
|
42
|
+
const socket = new WebSocket(`ws://${resolvedHost}:${lock.port}`, {
|
|
50
43
|
handshakeTimeout: timeoutMs + 1_000,
|
|
51
44
|
headers: {
|
|
52
|
-
[
|
|
45
|
+
[AUTH_HEADER]: lock.authToken,
|
|
53
46
|
},
|
|
54
47
|
});
|
|
55
48
|
this.socket = socket;
|
|
56
|
-
socket.on("message", (raw) => this.runSocketHandler("message", () => this.handleMessage(
|
|
49
|
+
socket.on("message", (raw) => this.runSocketHandler("message", () => this.handleMessage(decodeRawData(raw))));
|
|
57
50
|
socket.on("close", (_code, reason) => {
|
|
58
51
|
this.runSocketHandler("close", () => {
|
|
59
52
|
if (this.socket !== socket)
|
|
@@ -105,8 +98,8 @@ class IdeConnection {
|
|
|
105
98
|
id: this.nextId++,
|
|
106
99
|
method: "initialize",
|
|
107
100
|
params: {
|
|
108
|
-
protocolVersion:
|
|
109
|
-
client: { name:
|
|
101
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
102
|
+
client: { name: EXT_CONFIG_NAME, version: "0.1.0" },
|
|
110
103
|
cwd: this.cwd,
|
|
111
104
|
},
|
|
112
105
|
}));
|
|
@@ -127,20 +120,20 @@ class IdeConnection {
|
|
|
127
120
|
}
|
|
128
121
|
if (!isNotification(parsed))
|
|
129
122
|
return;
|
|
130
|
-
if (parsed.method === "selection_changed" &&
|
|
131
|
-
const params =
|
|
123
|
+
if (parsed.method === "selection_changed" && isSelectionChangedParams(parsed.params)) {
|
|
124
|
+
const params = normalizeEditorSelectionSnapshotForHost(withReceivedAt(parsed.params), this.options.env);
|
|
132
125
|
this.emitCallback("selection changed callback", () => this.callbacks.onSelectionChanged?.(params));
|
|
133
126
|
}
|
|
134
|
-
else if (parsed.method === "selection_cleared" &&
|
|
127
|
+
else if (parsed.method === "selection_cleared" && isSelectionClearedParams(parsed.params)) {
|
|
135
128
|
const params = withReceivedAt(parsed.params);
|
|
136
129
|
this.emitCallback("selection cleared callback", () => this.callbacks.onSelectionCleared?.(params));
|
|
137
130
|
}
|
|
138
|
-
else if (parsed.method === "at_mentioned" &&
|
|
139
|
-
const snapshot =
|
|
140
|
-
const params = { ...snapshot, rangeText:
|
|
131
|
+
else if (parsed.method === "at_mentioned" && isAtMentionedParams(parsed.params)) {
|
|
132
|
+
const snapshot = normalizeEditorSelectionSnapshotForHost(withReceivedAt(parsed.params), this.options.env);
|
|
133
|
+
const params = { ...snapshot, rangeText: formatRangeMention(snapshot, { cwd: this.cwd, env: this.options.env }) };
|
|
141
134
|
this.emitCallback("at mentioned callback", () => this.callbacks.onAtMentioned?.(params));
|
|
142
135
|
}
|
|
143
|
-
else if (parsed.method === "diagnostic_fix_requested" &&
|
|
136
|
+
else if (parsed.method === "diagnostic_fix_requested" && isDiagnosticFixRequestedParams(parsed.params)) {
|
|
144
137
|
const params = withReceivedAt(parsed.params);
|
|
145
138
|
this.emitCallback("diagnostic fix requested callback", () => this.callbacks.onDiagnosticFixRequested?.(params));
|
|
146
139
|
}
|
|
@@ -162,20 +155,19 @@ class IdeConnection {
|
|
|
162
155
|
}
|
|
163
156
|
}
|
|
164
157
|
reportError(label, error) {
|
|
165
|
-
const reported =
|
|
158
|
+
const reported = toError(error, label);
|
|
166
159
|
if (!this.callbacks.onError) {
|
|
167
|
-
|
|
160
|
+
logExtensionError(label, reported);
|
|
168
161
|
return;
|
|
169
162
|
}
|
|
170
163
|
try {
|
|
171
164
|
this.callbacks.onError(reported);
|
|
172
165
|
}
|
|
173
166
|
catch (callbackError) {
|
|
174
|
-
|
|
167
|
+
logExtensionError(`${label} onError callback`, callbackError);
|
|
175
168
|
}
|
|
176
169
|
}
|
|
177
170
|
}
|
|
178
|
-
exports.IdeConnection = IdeConnection;
|
|
179
171
|
function withReceivedAt(params) {
|
|
180
172
|
return { ...params, receivedAt: params.receivedAt ?? Date.now() };
|
|
181
173
|
}
|
package/dist/src/pi/context.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const diagnostics_1 = require("./diagnostics");
|
|
8
|
-
const safety_1 = require("./safety");
|
|
9
|
-
const ui_1 = require("./ui");
|
|
10
|
-
function registerContextHandlers(pi, runtime) {
|
|
11
|
-
pi.on("before_agent_start", (_event, ctx) => (0, safety_1.runPiBoundary)("Pi before_agent_start", runtime, () => {
|
|
1
|
+
import { formatEditorContext, snapshotKey, SYSTEM_REMINDER_TAG, SELECTED_CONTEXT_MARKER } from "../shared/format.js";
|
|
2
|
+
import { DIAGNOSTIC_CONTEXT_MARKER } from "./diagnostics.js";
|
|
3
|
+
import { runPiBoundary } from "./safety.js";
|
|
4
|
+
import { updateIdeUi } from "./ui.js";
|
|
5
|
+
export function registerContextHandlers(pi, runtime) {
|
|
6
|
+
pi.on("before_agent_start", (_event, ctx) => runPiBoundary("Pi before_agent_start", runtime, () => {
|
|
12
7
|
runtime.ctx = ctx;
|
|
13
8
|
if (!runtime.enabled)
|
|
14
9
|
return;
|
|
@@ -18,7 +13,7 @@ function registerContextHandlers(pi, runtime) {
|
|
|
18
13
|
return;
|
|
19
14
|
runtime.turnSelection = runtime.latestSelection;
|
|
20
15
|
}, ctx));
|
|
21
|
-
pi.on("message_end", (event, ctx) =>
|
|
16
|
+
pi.on("message_end", (event, ctx) => runPiBoundary("Pi message_end", runtime, () => {
|
|
22
17
|
runtime.ctx = ctx;
|
|
23
18
|
if (!runtime.enabled || !runtime.turnSelection)
|
|
24
19
|
return;
|
|
@@ -28,29 +23,29 @@ function registerContextHandlers(pi, runtime) {
|
|
|
28
23
|
runtime.turnSelection = undefined;
|
|
29
24
|
return;
|
|
30
25
|
}
|
|
31
|
-
const text = `${
|
|
26
|
+
const text = `${formatEditorContext(runtime.turnSelection)}\n`;
|
|
32
27
|
const message = mergeIntoUserMessage(event.message, text);
|
|
33
28
|
runtime.attachState = "sent";
|
|
34
29
|
runtime.turnSelection = undefined;
|
|
35
|
-
|
|
30
|
+
updateIdeUi(runtime, ctx);
|
|
36
31
|
return { message };
|
|
37
32
|
}, ctx));
|
|
38
33
|
}
|
|
39
|
-
function setLatestSelection(runtime, snapshot, ctx) {
|
|
40
|
-
const key =
|
|
34
|
+
export function setLatestSelection(runtime, snapshot, ctx) {
|
|
35
|
+
const key = snapshotKey(snapshot);
|
|
41
36
|
runtime.latestSelection = snapshot;
|
|
42
37
|
if (runtime.latestSelectionKey !== key) {
|
|
43
38
|
runtime.latestSelectionKey = key;
|
|
44
39
|
runtime.attachState = "pending";
|
|
45
40
|
}
|
|
46
|
-
|
|
41
|
+
updateIdeUi(runtime, ctx);
|
|
47
42
|
}
|
|
48
|
-
function clearLatestSelection(runtime, ctx) {
|
|
43
|
+
export function clearLatestSelection(runtime, ctx) {
|
|
49
44
|
runtime.latestSelection = undefined;
|
|
50
45
|
runtime.latestSelectionKey = undefined;
|
|
51
46
|
runtime.turnSelection = undefined;
|
|
52
47
|
runtime.attachState = "idle";
|
|
53
|
-
|
|
48
|
+
updateIdeUi(runtime, ctx);
|
|
54
49
|
}
|
|
55
50
|
function mergeIntoUserMessage(message, text) {
|
|
56
51
|
return {
|
|
@@ -63,6 +58,6 @@ function normalizeUserContent(content) {
|
|
|
63
58
|
}
|
|
64
59
|
function messageContainsMarker(message) {
|
|
65
60
|
const serialized = JSON.stringify(message);
|
|
66
|
-
return (serialized.includes(`<${
|
|
67
|
-
serialized.includes(
|
|
61
|
+
return (serialized.includes(`<${SYSTEM_REMINDER_TAG}><${SELECTED_CONTEXT_MARKER}>`) ||
|
|
62
|
+
serialized.includes(DIAGNOSTIC_CONTEXT_MARKER));
|
|
68
63
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { toRelativeDisplayPath } from "../shared/paths.js";
|
|
3
|
+
import { formatDiagnosticCode, formatRange } from "./diagnostics.js";
|
|
4
|
+
export const DIAGNOSTIC_FIX_CUSTOM_TYPE = "pi-x-ide/diagnostic-fix";
|
|
5
|
+
export function registerDiagnosticRenderer(pi) {
|
|
6
|
+
pi.registerMessageRenderer(DIAGNOSTIC_FIX_CUSTOM_TYPE, (message, { expanded }, theme) => {
|
|
7
|
+
const details = message.details;
|
|
8
|
+
if (!details)
|
|
9
|
+
return undefined;
|
|
10
|
+
return buildComponent(details, theme, expanded);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function buildComponent(details, theme, expanded) {
|
|
14
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
15
|
+
box.addChild(new Text(buildText(details, theme, expanded), 0, 0));
|
|
16
|
+
return box;
|
|
17
|
+
}
|
|
18
|
+
function buildText(details, theme, expanded) {
|
|
19
|
+
const severityColor = pickSeverityColor(details.diagnostics);
|
|
20
|
+
const label = theme.fg(severityColor, theme.bold("[DIAGNOSTIC]"));
|
|
21
|
+
const sourceLabel = describeSource(details.source);
|
|
22
|
+
const relPath = toRelativeDisplayPath(details.filePath, details.workspaceFolder, details.cwd);
|
|
23
|
+
const location = theme.fg("dim", `${relPath}:${formatRange(details.triggerRange)}`);
|
|
24
|
+
const count = details.diagnostics.length;
|
|
25
|
+
const noun = count === 1 ? "diagnostic" : "diagnostics";
|
|
26
|
+
const header = `${label} Fix ${count} ${noun} from ${sourceLabel} — ${location}`;
|
|
27
|
+
if (!expanded) {
|
|
28
|
+
return `${header}\n${theme.fg("dim", "ctrl+o to expand")}`;
|
|
29
|
+
}
|
|
30
|
+
const lines = [header];
|
|
31
|
+
for (const [index, diagnostic] of details.diagnostics.entries()) {
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push(formatDiagnostic(diagnostic, index, details, theme));
|
|
34
|
+
}
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
function formatDiagnostic(diagnostic, index, details, theme) {
|
|
38
|
+
const color = diagnostic.severity === "error" ? "error" : "warning";
|
|
39
|
+
const headerText = `${diagnostic.severity.toUpperCase()} #${index + 1}`;
|
|
40
|
+
const lines = [`${theme.fg(color, headerText)} ${theme.fg("dim", formatRange(diagnostic.range))}`];
|
|
41
|
+
if (diagnostic.code !== undefined) {
|
|
42
|
+
lines.push(theme.fg("dim", ` code: ${formatDiagnosticCode(diagnostic.code)}`));
|
|
43
|
+
}
|
|
44
|
+
if (diagnostic.source) {
|
|
45
|
+
lines.push(theme.fg("dim", ` source: ${diagnostic.source}`));
|
|
46
|
+
}
|
|
47
|
+
lines.push(` ${diagnostic.message}`);
|
|
48
|
+
if (diagnostic.contextLines.length > 0) {
|
|
49
|
+
for (const contextLine of diagnostic.contextLines) {
|
|
50
|
+
const marker = contextLine.isPrimary ? theme.fg(color, ">") : " ";
|
|
51
|
+
const lineNumber = theme.fg("dim", `${contextLine.line + 1}:`.padStart(4, " "));
|
|
52
|
+
lines.push(` ${marker} ${lineNumber} ${contextLine.text}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (diagnostic.relatedInformation && diagnostic.relatedInformation.length > 0) {
|
|
56
|
+
lines.push(theme.fg("dim", " related:"));
|
|
57
|
+
for (const related of diagnostic.relatedInformation) {
|
|
58
|
+
const relatedPath = toRelativeDisplayPath(related.filePath, details.workspaceFolder, details.cwd);
|
|
59
|
+
lines.push(theme.fg("dim", ` ${relatedPath} ${formatRange(related.range)}: ${related.message}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
function pickSeverityColor(diagnostics) {
|
|
65
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "error" : "warning";
|
|
66
|
+
}
|
|
67
|
+
function describeSource(source) {
|
|
68
|
+
if (source === "vscode")
|
|
69
|
+
return "VS Code";
|
|
70
|
+
return source;
|
|
71
|
+
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.buildDiagnosticContextMessage = buildDiagnosticContextMessage;
|
|
6
|
-
exports.formatDiagnosticContext = formatDiagnosticContext;
|
|
7
|
-
exports.handleDiagnosticFixRequested = handleDiagnosticFixRequested;
|
|
8
|
-
const config_1 = require("../shared/config");
|
|
9
|
-
exports.DIAGNOSTIC_CONTEXT_MARKER = "pi-x-ide/diagnostic-context";
|
|
10
|
-
function buildDiagnosticFixPrompt(params, options = {}) {
|
|
1
|
+
import { readPiConfigFixPrompt } from "../shared/config.js";
|
|
2
|
+
import { DIAGNOSTIC_FIX_CUSTOM_TYPE } from "./diagnostic-renderer.js";
|
|
3
|
+
export const DIAGNOSTIC_CONTEXT_MARKER = "pi-x-ide/diagnostic-context";
|
|
4
|
+
export function buildDiagnosticFixPrompt(params, options = {}) {
|
|
11
5
|
const context = buildDiagnosticContextMessage(params);
|
|
12
6
|
const template = options.fixPrompt;
|
|
13
7
|
if (!template) {
|
|
@@ -20,13 +14,13 @@ ${context}`;
|
|
|
20
14
|
return `${template}
|
|
21
15
|
${context}`;
|
|
22
16
|
}
|
|
23
|
-
function buildDiagnosticContextMessage(params) {
|
|
24
|
-
return `<!-- ${
|
|
17
|
+
export function buildDiagnosticContextMessage(params) {
|
|
18
|
+
return `<!-- ${DIAGNOSTIC_CONTEXT_MARKER} -->
|
|
25
19
|
${formatDiagnosticContext(params)}
|
|
26
|
-
<!-- ${
|
|
20
|
+
<!-- ${DIAGNOSTIC_CONTEXT_MARKER} -->
|
|
27
21
|
`;
|
|
28
22
|
}
|
|
29
|
-
function formatDiagnosticContext(params) {
|
|
23
|
+
export function formatDiagnosticContext(params) {
|
|
30
24
|
const filePath = params.filePath;
|
|
31
25
|
const lines = [`File: ${filePath}`, `Source: ${params.source}`];
|
|
32
26
|
if (params.documentVersion !== undefined)
|
|
@@ -61,7 +55,7 @@ function formatDiagnosticContext(params) {
|
|
|
61
55
|
}
|
|
62
56
|
return lines.join("\n");
|
|
63
57
|
}
|
|
64
|
-
function handleDiagnosticFixRequested(pi, runtime, params) {
|
|
58
|
+
export function handleDiagnosticFixRequested(pi, runtime, params) {
|
|
65
59
|
const ctx = runtime.ctx;
|
|
66
60
|
if (!ctx)
|
|
67
61
|
return;
|
|
@@ -72,25 +66,34 @@ function handleDiagnosticFixRequested(pi, runtime, params) {
|
|
|
72
66
|
ctx.ui.notify("VS Code diagnostic context added to input.", "info");
|
|
73
67
|
return;
|
|
74
68
|
}
|
|
75
|
-
const fixPrompt =
|
|
69
|
+
const fixPrompt = readPiConfigFixPrompt();
|
|
76
70
|
const prompt = buildDiagnosticFixPrompt(params, { fixPrompt });
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
const details = {
|
|
72
|
+
source: params.source,
|
|
73
|
+
filePath: params.filePath,
|
|
74
|
+
workspaceFolder: params.workspaceFolder,
|
|
75
|
+
triggerRange: params.triggerRange,
|
|
76
|
+
diagnostics: params.diagnostics,
|
|
77
|
+
cwd: ctx.cwd,
|
|
78
|
+
};
|
|
79
|
+
pi.sendMessage({
|
|
80
|
+
customType: DIAGNOSTIC_FIX_CUSTOM_TYPE,
|
|
81
|
+
content: prompt,
|
|
82
|
+
display: true,
|
|
83
|
+
details,
|
|
84
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
85
|
+
if (!ctx.isIdle() && ctx.hasUI)
|
|
83
86
|
ctx.ui.notify("VS Code diagnostic fix request queued.", "info");
|
|
84
87
|
}
|
|
85
|
-
function formatRange(range) {
|
|
88
|
+
export function formatRange(range) {
|
|
86
89
|
const start = formatPosition(range.start);
|
|
87
90
|
const end = formatPosition(range.end);
|
|
88
91
|
return start === end ? start : `${start}-${end}`;
|
|
89
92
|
}
|
|
90
|
-
function formatPosition(position) {
|
|
93
|
+
export function formatPosition(position) {
|
|
91
94
|
return `L${position.line + 1}:C${position.character + 1}`;
|
|
92
95
|
}
|
|
93
|
-
function formatDiagnosticCode(code) {
|
|
96
|
+
export function formatDiagnosticCode(code) {
|
|
94
97
|
if (typeof code === "string" || typeof code === "number")
|
|
95
98
|
return String(code);
|
|
96
99
|
return code.target ? `${code.value} (${code.target})` : String(code.value);
|
package/dist/src/pi/discovery.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const protocol_1 = require("../shared/protocol");
|
|
9
|
-
const paths_1 = require("../shared/paths");
|
|
10
|
-
const schema_1 = require("../shared/schema");
|
|
11
|
-
const platform_1 = require("../shared/platform");
|
|
12
|
-
const ide_host_1 = require("./ide-host");
|
|
1
|
+
import { readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LOCK_FILE_EXTENSION } from "../shared/protocol.js";
|
|
4
|
+
import { relationshipMatchLength, resolveLockDirs } from "../shared/paths.js";
|
|
5
|
+
import { parseLockFileContent } from "../shared/schema.js";
|
|
6
|
+
import { isWsl, normalizePathForHost } from "../shared/platform.js";
|
|
7
|
+
import { IDE_HOST_TCP_PROBE_TIMEOUT_MS, resolveIdeHost, tcpReachable } from "./ide-host.js";
|
|
13
8
|
function isProcessAlive(pid) {
|
|
14
9
|
try {
|
|
15
10
|
process.kill(pid, 0);
|
|
@@ -22,21 +17,21 @@ function isProcessAlive(pid) {
|
|
|
22
17
|
function bestWorkspaceMatch(lock, cwd, env) {
|
|
23
18
|
let best;
|
|
24
19
|
for (const workspaceFolder of lock.workspaceFolders) {
|
|
25
|
-
const matchLength =
|
|
20
|
+
const matchLength = relationshipMatchLength(workspaceFolder, cwd, env);
|
|
26
21
|
if (matchLength <= 0)
|
|
27
22
|
continue;
|
|
28
23
|
if (!best || matchLength > best.matchLength) {
|
|
29
|
-
best = { matchLength, workspaceFolder:
|
|
24
|
+
best = { matchLength, workspaceFolder: normalizePathForHost(workspaceFolder, env) };
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
return best;
|
|
33
28
|
}
|
|
34
|
-
function sortCandidates(candidates) {
|
|
29
|
+
export function sortCandidates(candidates) {
|
|
35
30
|
return [...candidates].sort((a, b) => b.matchLength - a.matchLength || b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path));
|
|
36
31
|
}
|
|
37
|
-
async function discoverIdeCandidates(options) {
|
|
32
|
+
export async function discoverIdeCandidates(options) {
|
|
38
33
|
const env = options.env ?? process.env;
|
|
39
|
-
const lockDirs =
|
|
34
|
+
const lockDirs = resolveLockDirs({
|
|
40
35
|
lockDir: options.lockDir,
|
|
41
36
|
homeLockDir: options.homeLockDir,
|
|
42
37
|
env,
|
|
@@ -54,20 +49,20 @@ async function discoverIdeCandidatesInDir(lockDir, options, env) {
|
|
|
54
49
|
const checkPid = options.checkPid ?? true;
|
|
55
50
|
let entries;
|
|
56
51
|
try {
|
|
57
|
-
entries = await
|
|
52
|
+
entries = await readdir(lockDir);
|
|
58
53
|
}
|
|
59
54
|
catch {
|
|
60
55
|
return [];
|
|
61
56
|
}
|
|
62
57
|
const candidates = [];
|
|
63
58
|
for (const entry of entries) {
|
|
64
|
-
if (!entry.endsWith(
|
|
59
|
+
if (!entry.endsWith(LOCK_FILE_EXTENSION))
|
|
65
60
|
continue;
|
|
66
|
-
const path =
|
|
61
|
+
const path = join(lockDir, entry);
|
|
67
62
|
let content;
|
|
68
63
|
let mtimeMs;
|
|
69
64
|
try {
|
|
70
|
-
const [fileContent, fileStat] = await Promise.all([
|
|
65
|
+
const [fileContent, fileStat] = await Promise.all([readFile(path, "utf8"), stat(path)]);
|
|
71
66
|
content = fileContent;
|
|
72
67
|
mtimeMs = fileStat.mtimeMs;
|
|
73
68
|
}
|
|
@@ -75,31 +70,31 @@ async function discoverIdeCandidatesInDir(lockDir, options, env) {
|
|
|
75
70
|
continue;
|
|
76
71
|
}
|
|
77
72
|
if (now - mtimeMs > maxAgeMs) {
|
|
78
|
-
await
|
|
73
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
79
74
|
continue;
|
|
80
75
|
}
|
|
81
|
-
const lock =
|
|
76
|
+
const lock = parseLockFileContent(content);
|
|
82
77
|
if (!lock) {
|
|
83
|
-
await
|
|
78
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
84
79
|
continue;
|
|
85
80
|
}
|
|
86
81
|
if (checkPid && typeof lock.pid === "number" && !isProcessAlive(lock.pid)) {
|
|
87
|
-
if (lock.runningInWindows === true &&
|
|
82
|
+
if (lock.runningInWindows === true && isWsl(env)) {
|
|
88
83
|
const reachable = await isDeadWindowsPidLockReachable(lock, options, env);
|
|
89
84
|
if (!reachable) {
|
|
90
|
-
await
|
|
85
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
91
86
|
continue;
|
|
92
87
|
}
|
|
93
88
|
}
|
|
94
89
|
else {
|
|
95
|
-
await
|
|
90
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
96
91
|
continue;
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
94
|
const match = bestWorkspaceMatch(lock, options.cwd, env);
|
|
100
95
|
const { matchLength, workspaceFolder } = match ?? {
|
|
101
96
|
matchLength: 0,
|
|
102
|
-
workspaceFolder: lock.workspaceFolders[0] ?
|
|
97
|
+
workspaceFolder: lock.workspaceFolders[0] ? normalizePathForHost(lock.workspaceFolders[0], env) : "",
|
|
103
98
|
};
|
|
104
99
|
candidates.push({ path, lock, mtimeMs, matchLength, workspaceFolder });
|
|
105
100
|
}
|
|
@@ -107,13 +102,13 @@ async function discoverIdeCandidatesInDir(lockDir, options, env) {
|
|
|
107
102
|
}
|
|
108
103
|
async function isDeadWindowsPidLockReachable(lock, options, env) {
|
|
109
104
|
try {
|
|
110
|
-
const host = await (options.resolveHost ?? ((ideLock) =>
|
|
111
|
-
return await (options.tcpProbe ??
|
|
105
|
+
const host = await (options.resolveHost ?? ((ideLock) => resolveIdeHost(ideLock, { env, tcpProbe: options.tcpProbe })))(lock);
|
|
106
|
+
return await (options.tcpProbe ?? tcpReachable)(host, lock.port, IDE_HOST_TCP_PROBE_TIMEOUT_MS);
|
|
112
107
|
}
|
|
113
108
|
catch {
|
|
114
109
|
return false;
|
|
115
110
|
}
|
|
116
111
|
}
|
|
117
|
-
async function resolveBestIdeCandidate(options) {
|
|
112
|
+
export async function resolveBestIdeCandidate(options) {
|
|
118
113
|
return (await discoverIdeCandidates(options))[0];
|
|
119
114
|
}
|