@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +119 -0
  3. package/CONTRIBUTING.zh-CN.md +106 -0
  4. package/LICENSE +201 -0
  5. package/README.md +33 -2
  6. package/README.zh-CN.md +34 -0
  7. package/RELEASE.md +213 -0
  8. package/config.example.json +9 -0
  9. package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
  10. package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
  11. package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
  12. package/dist/src/pi/chunks/runtime-services-TUOTPMCH.js +19569 -0
  13. package/dist/src/pi/commands.js +126 -0
  14. package/dist/src/pi/config-ui.js +127 -0
  15. package/dist/src/pi/connection.js +237 -0
  16. package/dist/src/pi/context.js +30 -0
  17. package/dist/src/pi/diagnostic-renderer.js +326 -0
  18. package/dist/src/pi/diagnostic-types.js +1 -0
  19. package/dist/src/pi/diagnostics.js +104 -0
  20. package/dist/src/pi/discovery.js +135 -0
  21. package/dist/src/pi/effect-boundary.js +5 -0
  22. package/dist/src/pi/ide-host.js +68 -0
  23. package/dist/src/pi/index.js +636 -0
  24. package/dist/src/pi/install.js +308 -0
  25. package/dist/src/pi/reconnect.js +57 -0
  26. package/dist/src/pi/runtime-loader.js +38 -0
  27. package/dist/src/pi/runtime-services.js +356 -0
  28. package/dist/src/pi/safety.js +61 -0
  29. package/dist/src/pi/selection.js +18 -0
  30. package/dist/src/pi/state.js +13 -0
  31. package/dist/src/pi/ui.js +304 -0
  32. package/dist/src/pi/zed.js +409 -0
  33. package/dist/src/shared/config-options.js +45 -0
  34. package/dist/src/shared/config.js +183 -0
  35. package/dist/src/shared/display-width.js +99 -0
  36. package/dist/src/shared/effect-errors.js +36 -0
  37. package/dist/src/shared/effect-runtime.js +36 -0
  38. package/dist/src/shared/effect-schema.js +120 -0
  39. package/dist/src/shared/errors.js +48 -0
  40. package/dist/src/shared/format.js +92 -0
  41. package/dist/src/shared/ide-server.js +167 -0
  42. package/dist/src/shared/jsonrpc-guard.js +12 -0
  43. package/dist/src/shared/lock-file-heartbeat.js +58 -0
  44. package/dist/src/shared/lock-file.js +50 -0
  45. package/dist/src/shared/paths.js +79 -0
  46. package/dist/src/shared/platform.js +57 -0
  47. package/dist/src/shared/protocol.js +3 -0
  48. package/dist/src/shared/schema.js +37 -0
  49. package/dist/src/shared/version.js +1 -0
  50. package/dist/src/shared/ws.js +8 -0
  51. package/docs/explanation/architecture.md +123 -0
  52. package/docs/explanation/discovery.md +113 -0
  53. package/docs/explanation/effect.md +127 -0
  54. package/docs/how-to/install-jetbrains.md +58 -0
  55. package/docs/how-to/install-neovim.md +138 -0
  56. package/docs/how-to/install-vscode.md +75 -0
  57. package/docs/how-to/install-zed.md +70 -0
  58. package/docs/how-to/troubleshoot-connection.md +103 -0
  59. package/docs/how-to/use-wsl2.md +90 -0
  60. package/docs/reference/commands.md +59 -0
  61. package/docs/reference/configuration.md +101 -0
  62. package/docs/reference/feature-parity.md +26 -0
  63. package/docs/reference/protocol.md +71 -0
  64. package/docs/specs/ide-protocol.md +215 -0
  65. package/docs/tutorials/get-started.md +133 -0
  66. package/docs/zh-CN/explanation/architecture.md +93 -0
  67. package/docs/zh-CN/explanation/discovery.md +71 -0
  68. package/docs/zh-CN/explanation/effect.md +90 -0
  69. package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
  70. package/docs/zh-CN/how-to/install-neovim.md +126 -0
  71. package/docs/zh-CN/how-to/install-vscode.md +67 -0
  72. package/docs/zh-CN/how-to/install-zed.md +56 -0
  73. package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
  74. package/docs/zh-CN/how-to/use-wsl2.md +75 -0
  75. package/docs/zh-CN/reference/commands.md +54 -0
  76. package/docs/zh-CN/reference/configuration.md +94 -0
  77. package/docs/zh-CN/reference/feature-parity.md +21 -0
  78. package/docs/zh-CN/reference/protocol.md +54 -0
  79. package/docs/zh-CN/tutorials/get-started.md +101 -0
  80. package/package.json +86 -4
  81. package/schemas/config.json +77 -0
@@ -0,0 +1,326 @@
1
+ import { visibleWidth } from "../shared/display-width.js";
2
+ import { toRelativeDisplayPath } from "../shared/paths.js";
3
+ import { formatDiagnosticCode, formatRange } from "./diagnostics.js";
4
+ import { DIAGNOSTIC_FIX_CUSTOM_TYPE } from "./diagnostic-types.js";
5
+ export { DIAGNOSTIC_FIX_CUSTOM_TYPE } from "./diagnostic-types.js";
6
+ const DIAGNOSTIC_BOX_PADDING_X = 1;
7
+ const DIAGNOSTIC_BOX_PADDING_Y = 1;
8
+ const MIN_CONTENT_WIDTH = 1;
9
+ const TAB_REPLACEMENT = " ";
10
+ const ESCAPE_CHARACTER = String.fromCharCode(27);
11
+ const ANSI_SEQUENCE_AT_START = new RegExp(`^${ESCAPE_CHARACTER}\\[[0-?]*[ -/]*[@-~]`);
12
+ const GRAPHEME_SEGMENTER = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : undefined;
13
+ const SGR_CODES = {
14
+ reset: 0,
15
+ bold: 1,
16
+ dim: 2,
17
+ italic: 3,
18
+ underline: 4,
19
+ slowBlink: 5,
20
+ rapidBlink: 6,
21
+ inverse: 7,
22
+ conceal: 8,
23
+ strikethrough: 9,
24
+ doubleUnderline: 21,
25
+ intensityReset: 22,
26
+ italicReset: 23,
27
+ underlineReset: 24,
28
+ blinkReset: 25,
29
+ inverseReset: 27,
30
+ concealReset: 28,
31
+ strikethroughReset: 29,
32
+ foregroundStart: 30,
33
+ foregroundEnd: 37,
34
+ extendedForeground: 38,
35
+ foregroundReset: 39,
36
+ backgroundStart: 40,
37
+ backgroundEnd: 47,
38
+ extendedBackground: 48,
39
+ backgroundReset: 49,
40
+ overline: 53,
41
+ overlineReset: 55,
42
+ extendedUnderlineColor: 58,
43
+ underlineColorReset: 59,
44
+ brightForegroundStart: 90,
45
+ brightForegroundEnd: 97,
46
+ brightBackgroundStart: 100,
47
+ brightBackgroundEnd: 107,
48
+ };
49
+ const SGR_INDEXED_COLOR_MODE = 5;
50
+ const SGR_TRUE_COLOR_MODE = 2;
51
+ const SGR_INDEXED_COLOR_PARAMETER_COUNT = 2;
52
+ const SGR_TRUE_COLOR_PARAMETER_COUNT = 4;
53
+ const SGR_STYLE_GROUPS = new Map([
54
+ [SGR_CODES.bold, "bold"],
55
+ [SGR_CODES.dim, "dim"],
56
+ [SGR_CODES.italic, "italic"],
57
+ [SGR_CODES.underline, "underline"],
58
+ [SGR_CODES.slowBlink, "blink"],
59
+ [SGR_CODES.rapidBlink, "blink"],
60
+ [SGR_CODES.inverse, "inverse"],
61
+ [SGR_CODES.conceal, "conceal"],
62
+ [SGR_CODES.strikethrough, "strikethrough"],
63
+ [SGR_CODES.doubleUnderline, "underline"],
64
+ [SGR_CODES.overline, "overline"],
65
+ ]);
66
+ const SGR_STYLE_RESETS = new Map([
67
+ [SGR_CODES.intensityReset, ["bold", "dim"]],
68
+ [SGR_CODES.italicReset, ["italic"]],
69
+ [SGR_CODES.underlineReset, ["underline"]],
70
+ [SGR_CODES.blinkReset, ["blink"]],
71
+ [SGR_CODES.inverseReset, ["inverse"]],
72
+ [SGR_CODES.concealReset, ["conceal"]],
73
+ [SGR_CODES.strikethroughReset, ["strikethrough"]],
74
+ [SGR_CODES.overlineReset, ["overline"]],
75
+ ]);
76
+ export function registerDiagnosticRenderer(pi) {
77
+ pi.registerMessageRenderer(DIAGNOSTIC_FIX_CUSTOM_TYPE, (message, { expanded }, theme) => {
78
+ const details = message.details;
79
+ if (!details)
80
+ return undefined;
81
+ return createDiagnosticFixComponent(details, theme, expanded);
82
+ });
83
+ }
84
+ export function createDiagnosticFixComponent(details, theme, expanded) {
85
+ return new DiagnosticMessageComponent(buildText(details, theme, expanded), (text) => theme.bg("customMessageBg", text));
86
+ }
87
+ class DiagnosticMessageComponent {
88
+ text;
89
+ bgFn;
90
+ cache;
91
+ constructor(text, bgFn) {
92
+ this.text = text;
93
+ this.bgFn = bgFn;
94
+ }
95
+ invalidate() {
96
+ this.cache = undefined;
97
+ }
98
+ render(width) {
99
+ if (this.cache?.width === width)
100
+ return this.cache.lines;
101
+ const contentWidth = Math.max(MIN_CONTENT_WIDTH, width - DIAGNOSTIC_BOX_PADDING_X * 2);
102
+ const leftPad = " ".repeat(DIAGNOSTIC_BOX_PADDING_X);
103
+ const lines = [];
104
+ for (let index = 0; index < DIAGNOSTIC_BOX_PADDING_Y; index += 1) {
105
+ lines.push(this.padAndApplyBackground("", width));
106
+ }
107
+ for (const wrappedLine of wrapAnsiText(this.text, contentWidth)) {
108
+ lines.push(this.padAndApplyBackground(leftPad + wrappedLine, width));
109
+ }
110
+ for (let index = 0; index < DIAGNOSTIC_BOX_PADDING_Y; index += 1) {
111
+ lines.push(this.padAndApplyBackground("", width));
112
+ }
113
+ this.cache = { width, lines };
114
+ return lines;
115
+ }
116
+ padAndApplyBackground(line, width) {
117
+ const padNeeded = Math.max(0, width - visibleWidth(line));
118
+ return this.bgFn(line + " ".repeat(padNeeded));
119
+ }
120
+ }
121
+ export function wrapAnsiText(text, width) {
122
+ const normalizedText = text.replaceAll("\t", TAB_REPLACEMENT);
123
+ const lines = [];
124
+ const sgrState = new SgrState();
125
+ for (const logicalLine of normalizedText.split("\n")) {
126
+ lines.push(...wrapAnsiLine(logicalLine, width, sgrState));
127
+ }
128
+ return lines.length > 0 ? lines : [""];
129
+ }
130
+ function wrapAnsiLine(line, width, sgrState) {
131
+ const lines = [];
132
+ let currentLine = sgrState.toAnsi();
133
+ let currentWidth = 0;
134
+ let index = 0;
135
+ while (index < line.length) {
136
+ const ansiSequence = readAnsiSequence(line, index);
137
+ if (ansiSequence) {
138
+ currentLine += ansiSequence;
139
+ sgrState.process(ansiSequence);
140
+ index += ansiSequence.length;
141
+ continue;
142
+ }
143
+ if (line[index] === ESCAPE_CHARACTER) {
144
+ currentLine += ESCAPE_CHARACTER;
145
+ index += 1;
146
+ continue;
147
+ }
148
+ const nextEscape = line.indexOf(ESCAPE_CHARACTER, index);
149
+ const textEnd = nextEscape === -1 ? line.length : nextEscape;
150
+ const textChunk = line.slice(index, textEnd);
151
+ for (const grapheme of segmentGraphemes(textChunk)) {
152
+ const graphemeWidth = visibleWidth(grapheme);
153
+ if (currentWidth > 0 && currentWidth + graphemeWidth > width) {
154
+ lines.push(currentLine + sgrState.lineEndReset());
155
+ currentLine = sgrState.toAnsi();
156
+ currentWidth = 0;
157
+ }
158
+ currentLine += grapheme;
159
+ currentWidth += graphemeWidth;
160
+ }
161
+ index = textEnd;
162
+ }
163
+ lines.push(currentLine + sgrState.lineEndReset());
164
+ return lines;
165
+ }
166
+ function readAnsiSequence(text, index) {
167
+ if (text[index] !== ESCAPE_CHARACTER)
168
+ return undefined;
169
+ return ANSI_SEQUENCE_AT_START.exec(text.slice(index))?.[0];
170
+ }
171
+ class SgrState {
172
+ activeCodes = new Map();
173
+ process(sequence) {
174
+ if (!sequence.endsWith("m"))
175
+ return;
176
+ const rawParameters = sequence.slice(2, -1);
177
+ const parameters = rawParameters === "" ? [SGR_CODES.reset] : rawParameters.split(";").map(Number);
178
+ for (let index = 0; index < parameters.length; index += 1) {
179
+ const code = parameters[index];
180
+ if (!Number.isInteger(code))
181
+ continue;
182
+ if (code === SGR_CODES.reset) {
183
+ this.activeCodes.clear();
184
+ continue;
185
+ }
186
+ const styleGroup = SGR_STYLE_GROUPS.get(code);
187
+ if (styleGroup) {
188
+ this.activeCodes.set(styleGroup, formatSgrSequence([code]));
189
+ continue;
190
+ }
191
+ const resetGroups = SGR_STYLE_RESETS.get(code);
192
+ if (resetGroups) {
193
+ for (const resetGroup of resetGroups)
194
+ this.activeCodes.delete(resetGroup);
195
+ continue;
196
+ }
197
+ if (isStandardForegroundCode(code)) {
198
+ this.activeCodes.set("foreground", formatSgrSequence([code]));
199
+ continue;
200
+ }
201
+ if (code === SGR_CODES.foregroundReset) {
202
+ this.activeCodes.delete("foreground");
203
+ continue;
204
+ }
205
+ if (isStandardBackgroundCode(code)) {
206
+ this.activeCodes.set("background", formatSgrSequence([code]));
207
+ continue;
208
+ }
209
+ if (code === SGR_CODES.backgroundReset) {
210
+ this.activeCodes.delete("background");
211
+ continue;
212
+ }
213
+ if (code === SGR_CODES.underlineColorReset) {
214
+ this.activeCodes.delete("underline-color");
215
+ continue;
216
+ }
217
+ const extendedGroup = getExtendedColorGroup(code);
218
+ if (extendedGroup) {
219
+ const parameterCount = getExtendedColorParameterCount(parameters[index + 1]);
220
+ if (parameterCount === 0 || index + parameterCount >= parameters.length)
221
+ continue;
222
+ const colorParameters = parameters.slice(index, index + parameterCount + 1);
223
+ this.activeCodes.set(extendedGroup, formatSgrSequence(colorParameters));
224
+ index += parameterCount;
225
+ }
226
+ }
227
+ }
228
+ toAnsi() {
229
+ return [...this.activeCodes.values()].join("");
230
+ }
231
+ lineEndReset() {
232
+ return this.activeCodes.has("underline") ? formatSgrSequence([SGR_CODES.underlineReset]) : "";
233
+ }
234
+ }
235
+ function isStandardForegroundCode(code) {
236
+ return ((code >= SGR_CODES.foregroundStart && code <= SGR_CODES.foregroundEnd) ||
237
+ (code >= SGR_CODES.brightForegroundStart && code <= SGR_CODES.brightForegroundEnd));
238
+ }
239
+ function isStandardBackgroundCode(code) {
240
+ return ((code >= SGR_CODES.backgroundStart && code <= SGR_CODES.backgroundEnd) ||
241
+ (code >= SGR_CODES.brightBackgroundStart && code <= SGR_CODES.brightBackgroundEnd));
242
+ }
243
+ function getExtendedColorGroup(code) {
244
+ if (code === SGR_CODES.extendedForeground)
245
+ return "foreground";
246
+ if (code === SGR_CODES.extendedBackground)
247
+ return "background";
248
+ if (code === SGR_CODES.extendedUnderlineColor)
249
+ return "underline-color";
250
+ return undefined;
251
+ }
252
+ function getExtendedColorParameterCount(mode) {
253
+ if (mode === SGR_INDEXED_COLOR_MODE)
254
+ return SGR_INDEXED_COLOR_PARAMETER_COUNT;
255
+ if (mode === SGR_TRUE_COLOR_MODE)
256
+ return SGR_TRUE_COLOR_PARAMETER_COUNT;
257
+ return 0;
258
+ }
259
+ function formatSgrSequence(parameters) {
260
+ return `${ESCAPE_CHARACTER}[${parameters.join(";")}m`;
261
+ }
262
+ function* segmentGraphemes(text) {
263
+ if (GRAPHEME_SEGMENTER) {
264
+ for (const segment of GRAPHEME_SEGMENTER.segment(text))
265
+ yield segment.segment;
266
+ return;
267
+ }
268
+ yield* text;
269
+ }
270
+ export function buildDiagnosticFixText(details, theme, expanded) {
271
+ return buildText(details, theme, expanded);
272
+ }
273
+ function buildText(details, theme, expanded) {
274
+ const severityColor = pickSeverityColor(details.diagnostics);
275
+ const label = theme.fg(severityColor, theme.bold("[DIAGNOSTIC]"));
276
+ const sourceLabel = describeSource(details.source);
277
+ const relPath = toRelativeDisplayPath(details.filePath, details.workspaceFolder, details.cwd);
278
+ const location = theme.fg("dim", `${relPath}:${formatRange(details.triggerRange)}`);
279
+ const count = details.diagnostics.length;
280
+ const noun = count === 1 ? "diagnostic" : "diagnostics";
281
+ const header = `${label} Fix ${count} ${noun} from ${sourceLabel} — ${location}`;
282
+ if (!expanded) {
283
+ return `${header}\n${theme.fg("dim", "ctrl+o to expand")}`;
284
+ }
285
+ const lines = [header];
286
+ for (const [index, diagnostic] of details.diagnostics.entries()) {
287
+ lines.push("");
288
+ lines.push(formatDiagnostic(diagnostic, index, details, theme));
289
+ }
290
+ return lines.join("\n");
291
+ }
292
+ function formatDiagnostic(diagnostic, index, details, theme) {
293
+ const color = diagnostic.severity === "error" ? "error" : "warning";
294
+ const headerText = `${diagnostic.severity.toUpperCase()} #${index + 1}`;
295
+ const lines = [`${theme.fg(color, headerText)} ${theme.fg("dim", formatRange(diagnostic.range))}`];
296
+ if (diagnostic.code !== undefined) {
297
+ lines.push(theme.fg("dim", ` code: ${formatDiagnosticCode(diagnostic.code)}`));
298
+ }
299
+ if (diagnostic.source) {
300
+ lines.push(theme.fg("dim", ` source: ${diagnostic.source}`));
301
+ }
302
+ lines.push(` ${diagnostic.message}`);
303
+ if (diagnostic.contextLines.length > 0) {
304
+ for (const contextLine of diagnostic.contextLines) {
305
+ const marker = contextLine.isPrimary ? theme.fg(color, ">") : " ";
306
+ const lineNumber = theme.fg("dim", `${contextLine.line + 1}:`.padStart(4, " "));
307
+ lines.push(` ${marker} ${lineNumber} ${contextLine.text}`);
308
+ }
309
+ }
310
+ if (diagnostic.relatedInformation && diagnostic.relatedInformation.length > 0) {
311
+ lines.push(theme.fg("dim", " related:"));
312
+ for (const related of diagnostic.relatedInformation) {
313
+ const relatedPath = toRelativeDisplayPath(related.filePath, details.workspaceFolder, details.cwd);
314
+ lines.push(theme.fg("dim", ` ${relatedPath} ${formatRange(related.range)}: ${related.message}`));
315
+ }
316
+ }
317
+ return lines.join("\n");
318
+ }
319
+ function pickSeverityColor(diagnostics) {
320
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "error" : "warning";
321
+ }
322
+ function describeSource(source) {
323
+ if (source === "vscode")
324
+ return "VS Code";
325
+ return source;
326
+ }
@@ -0,0 +1 @@
1
+ export const DIAGNOSTIC_FIX_CUSTOM_TYPE = "pi-x-ide/diagnostic-fix";
@@ -0,0 +1,104 @@
1
+ import { readPiConfigFixPrompt } from "../shared/config.js";
2
+ import { DIAGNOSTIC_FIX_CUSTOM_TYPE } from "./diagnostic-types.js";
3
+ export const DIAGNOSTIC_CONTEXT_MARKER = "pi-x-ide/diagnostic-context";
4
+ export function buildDiagnosticFixPrompt(params, options = {}) {
5
+ const context = buildDiagnosticContextMessage(params);
6
+ const template = options.fixPrompt;
7
+ if (!template) {
8
+ return `Analyze the errors and warnings at the following location, and try to fix them:
9
+ ${context}`;
10
+ }
11
+ if (template.includes("{DIAGNOSTIC}")) {
12
+ return template.replaceAll("{DIAGNOSTIC}", context);
13
+ }
14
+ return `${template}
15
+ ${context}`;
16
+ }
17
+ export function buildDiagnosticContextMessage(params) {
18
+ return `<!-- ${DIAGNOSTIC_CONTEXT_MARKER} -->
19
+ ${formatDiagnosticContext(params)}
20
+ <!-- ${DIAGNOSTIC_CONTEXT_MARKER} -->
21
+ `;
22
+ }
23
+ export function formatDiagnosticContext(params) {
24
+ const filePath = params.filePath;
25
+ const lines = [`File: ${filePath}`, `Source: ${params.source}`];
26
+ if (params.documentVersion !== undefined)
27
+ lines.push(`Document version: ${params.documentVersion}`);
28
+ lines.push(`Trigger range: ${formatRange(params.triggerRange)}`);
29
+ for (const [index, diagnostic] of params.diagnostics.entries()) {
30
+ lines.push("", `Diagnostic ${index + 1}:`);
31
+ lines.push(`- Severity: ${diagnostic.severity}`);
32
+ if (diagnostic.source)
33
+ lines.push(`- Source: ${diagnostic.source}`);
34
+ if (diagnostic.code !== undefined)
35
+ lines.push(`- Code: ${formatDiagnosticCode(diagnostic.code)}`);
36
+ lines.push(`- Range: ${formatRange(diagnostic.range)}`);
37
+ lines.push(`- Message: ${diagnostic.message}`);
38
+ if (diagnostic.selectedText.length > 0) {
39
+ lines.push("- Selected text:", fence(diagnostic.selectedText));
40
+ }
41
+ if (diagnostic.contextLines.length > 0) {
42
+ lines.push("- Context lines:");
43
+ for (const contextLine of diagnostic.contextLines) {
44
+ const marker = contextLine.isPrimary ? "\\>" : " ";
45
+ lines.push(` ${marker} ${contextLine.line + 1}: ${contextLine.text}`);
46
+ }
47
+ }
48
+ if (diagnostic.relatedInformation && diagnostic.relatedInformation.length > 0) {
49
+ lines.push("- Related information:");
50
+ for (const related of diagnostic.relatedInformation) {
51
+ const relatedPath = related.filePath;
52
+ lines.push(` - ${relatedPath} ${formatRange(related.range)}: ${related.message}`);
53
+ }
54
+ }
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+ export function handleDiagnosticFixRequested(pi, runtime, params) {
59
+ const ctx = runtime.ctx;
60
+ if (!ctx)
61
+ return;
62
+ if (params.action === "send-diagnostic") {
63
+ if (!ctx.hasUI)
64
+ return;
65
+ ctx.ui.pasteToEditor(buildDiagnosticContextMessage(params));
66
+ ctx.ui.notify("VS Code diagnostic context added to input.", "info");
67
+ return;
68
+ }
69
+ const fixPrompt = readPiConfigFixPrompt();
70
+ const prompt = buildDiagnosticFixPrompt(params, { fixPrompt });
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)
86
+ ctx.ui.notify("VS Code diagnostic fix request queued.", "info");
87
+ }
88
+ export function formatRange(range) {
89
+ const start = formatPosition(range.start);
90
+ const end = formatPosition(range.end);
91
+ return start === end ? start : `${start}-${end}`;
92
+ }
93
+ export function formatPosition(position) {
94
+ return `L${position.line + 1}:C${position.character + 1}`;
95
+ }
96
+ export function formatDiagnosticCode(code) {
97
+ if (typeof code === "string" || typeof code === "number")
98
+ return String(code);
99
+ return code.target ? `${code.value} (${code.target})` : String(code.value);
100
+ }
101
+ function fence(text) {
102
+ const fenceMarker = text.includes("```") ? "````" : "```";
103
+ return `${fenceMarker}\n${text}\n${fenceMarker}`;
104
+ }
@@ -0,0 +1,135 @@
1
+ import { readdir, readFile, rm, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import * as Effect from "effect/Effect";
4
+ import { LOCK_FILE_EXTENSION } from "../shared/protocol.js";
5
+ import { relationshipMatchLength, resolveLockDirs } from "../shared/paths.js";
6
+ import { parseLockFileContent } from "../shared/schema.js";
7
+ import { isWsl, normalizePathForHost } from "../shared/platform.js";
8
+ import { IDE_HOST_TCP_PROBE_TIMEOUT_MS, resolveIdeHost, tcpReachable } from "./ide-host.js";
9
+ function isProcessAlive(pid) {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function isUsableLocalPid(pid) {
19
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0;
20
+ }
21
+ function bestWorkspaceMatch(lock, cwd, env) {
22
+ let best;
23
+ for (const workspaceFolder of lock.workspaceFolders) {
24
+ const matchLength = relationshipMatchLength(workspaceFolder, cwd, env);
25
+ if (matchLength <= 0)
26
+ continue;
27
+ if (!best || matchLength > best.matchLength) {
28
+ best = { matchLength, workspaceFolder: normalizePathForHost(workspaceFolder, env) };
29
+ }
30
+ }
31
+ return best;
32
+ }
33
+ export function sortCandidates(candidates) {
34
+ return [...candidates].sort((a, b) => b.matchLength - a.matchLength || b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path));
35
+ }
36
+ function removeLockFile(path) {
37
+ return Effect.promise(() => rm(path, { force: true }).then(() => undefined, () => undefined));
38
+ }
39
+ function isWindowsLockReachable(lock, options, env) {
40
+ return Effect.promise(async () => {
41
+ try {
42
+ const host = await (options.resolveHost ?? ((ideLock) => resolveIdeHost(ideLock, { env, tcpProbe: options.tcpProbe })))(lock);
43
+ return await (options.tcpProbe ?? tcpReachable)(host, lock.port, IDE_HOST_TCP_PROBE_TIMEOUT_MS);
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ });
49
+ }
50
+ function discoverIdeCandidatesInDirEffect(lockDir, options, env) {
51
+ return Effect.gen(function* () {
52
+ const now = options.now ?? Date.now();
53
+ const maxAgeMs = options.maxAgeMs ?? 24 * 60 * 60 * 1000;
54
+ const checkPid = options.checkPid ?? true;
55
+ const entries = yield* Effect.promise(async () => {
56
+ try {
57
+ return await readdir(lockDir);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ });
63
+ const candidates = [];
64
+ for (const entry of entries) {
65
+ if (!entry.endsWith(LOCK_FILE_EXTENSION))
66
+ continue;
67
+ const path = join(lockDir, entry);
68
+ const file = yield* Effect.promise(async () => {
69
+ try {
70
+ const [content, fileStat] = await Promise.all([readFile(path, "utf8"), stat(path)]);
71
+ return { content, mtimeMs: fileStat.mtimeMs };
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ });
77
+ if (!file)
78
+ continue;
79
+ const lock = parseLockFileContent(file.content);
80
+ if (!lock) {
81
+ yield* removeLockFile(path);
82
+ continue;
83
+ }
84
+ const isWindowsSideWsl = lock.runningInWindows === true && isWsl(env);
85
+ const pid = lock.pid;
86
+ if (checkPid && isWindowsSideWsl) {
87
+ const reachable = yield* isWindowsLockReachable(lock, options, env);
88
+ if (!reachable) {
89
+ yield* removeLockFile(path);
90
+ continue;
91
+ }
92
+ }
93
+ else if (checkPid && isUsableLocalPid(pid)) {
94
+ if (!isProcessAlive(pid)) {
95
+ yield* removeLockFile(path);
96
+ continue;
97
+ }
98
+ }
99
+ else if (now - file.mtimeMs > maxAgeMs) {
100
+ yield* removeLockFile(path);
101
+ continue;
102
+ }
103
+ const match = bestWorkspaceMatch(lock, options.cwd, env);
104
+ const { matchLength, workspaceFolder } = match ?? {
105
+ matchLength: 0,
106
+ workspaceFolder: lock.workspaceFolders[0] ? normalizePathForHost(lock.workspaceFolders[0], env) : "",
107
+ };
108
+ candidates.push({ path, lock, mtimeMs: file.mtimeMs, matchLength, workspaceFolder });
109
+ }
110
+ return candidates;
111
+ });
112
+ }
113
+ export function discoverIdeCandidatesEffect(options) {
114
+ return Effect.gen(function* () {
115
+ const env = options.env ?? process.env;
116
+ const lockDirs = resolveLockDirs({
117
+ lockDir: options.lockDir,
118
+ homeLockDir: options.homeLockDir,
119
+ env,
120
+ windowsUsersRoot: options.windowsUsersRoot,
121
+ });
122
+ const candidates = [];
123
+ for (const lockDir of lockDirs) {
124
+ const dirCandidates = yield* discoverIdeCandidatesInDirEffect(lockDir, options, env);
125
+ candidates.push(...dirCandidates);
126
+ }
127
+ return sortCandidates(candidates);
128
+ });
129
+ }
130
+ export async function discoverIdeCandidates(options) {
131
+ return Effect.runPromise(discoverIdeCandidatesEffect(options));
132
+ }
133
+ export async function resolveBestIdeCandidate(options) {
134
+ return (await discoverIdeCandidates(options))[0];
135
+ }
@@ -0,0 +1,5 @@
1
+ import { runEffect } from "../shared/effect-runtime.js";
2
+ import { containPiError } from "./safety.js";
3
+ export function runPiEffect(scope, runtime, effect, ctx) {
4
+ return runEffect(scope, effect, (error) => containPiError(runtime, scope, error, ctx));
5
+ }
@@ -0,0 +1,68 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createConnection } from "node:net";
3
+ import { promisify } from "node:util";
4
+ import * as Effect from "effect/Effect";
5
+ import { resolvePiConfigEnv } from "../shared/config.js";
6
+ import { isWsl } from "../shared/platform.js";
7
+ export const PI_X_IDE_HOST_OVERRIDE_ENV = "PI_X_IDE_HOST_OVERRIDE";
8
+ export const IDE_HOST_TCP_PROBE_TIMEOUT_MS = 500;
9
+ const execFileAsync = promisify(execFile);
10
+ export function resolveIdeHostEffect(lock, options = {}) {
11
+ return Effect.gen(function* () {
12
+ const env = resolvePiConfigEnv(options.env ?? process.env);
13
+ const override = env[PI_X_IDE_HOST_OVERRIDE_ENV]?.trim();
14
+ if (override)
15
+ return override;
16
+ if (lock.runningInWindows === true && isWsl(env)) {
17
+ const gateway = yield* Effect.promise(() => resolveWslDefaultGateway(options.runCommand ?? defaultRunCommand));
18
+ if (gateway) {
19
+ const reachable = yield* Effect.promise(() => (options.tcpProbe ?? tcpReachable)(gateway, lock.port, options.timeoutMs ?? IDE_HOST_TCP_PROBE_TIMEOUT_MS));
20
+ if (reachable)
21
+ return gateway;
22
+ }
23
+ }
24
+ return lock.host || "127.0.0.1";
25
+ });
26
+ }
27
+ export async function resolveIdeHost(lock, options = {}) {
28
+ return Effect.runPromise(resolveIdeHostEffect(lock, options));
29
+ }
30
+ export async function resolveWslDefaultGateway(runCommand = defaultRunCommand) {
31
+ try {
32
+ const { stdout } = await runCommand("ip", ["route", "show"]);
33
+ return parseDefaultGateway(stdout);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ export function parseDefaultGateway(output) {
40
+ for (const line of output.split(/\r?\n/)) {
41
+ const match = line.match(/^default\s+via\s+(\d{1,3}(?:\.\d{1,3}){3})(?:\s|$)/);
42
+ if (match)
43
+ return match[1];
44
+ }
45
+ return undefined;
46
+ }
47
+ async function defaultRunCommand(command, args) {
48
+ const { stdout } = await execFileAsync(command, args, { encoding: "utf8" });
49
+ return { stdout };
50
+ }
51
+ export function tcpReachable(host, port, timeoutMs = IDE_HOST_TCP_PROBE_TIMEOUT_MS) {
52
+ return new Promise((resolve) => {
53
+ const socket = createConnection({ host, port });
54
+ let settled = false;
55
+ const finish = (reachable) => {
56
+ if (settled)
57
+ return;
58
+ settled = true;
59
+ socket.removeAllListeners();
60
+ socket.destroy();
61
+ resolve(reachable);
62
+ };
63
+ socket.setTimeout(timeoutMs);
64
+ socket.once("connect", () => finish(true));
65
+ socket.once("timeout", () => finish(false));
66
+ socket.once("error", () => finish(false));
67
+ });
68
+ }