@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.2
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 +328 -0
- package/CONTRIBUTING.md +119 -0
- package/CONTRIBUTING.zh-CN.md +106 -0
- package/LICENSE +201 -0
- package/README.md +33 -2
- package/README.zh-CN.md +34 -0
- package/RELEASE.md +213 -0
- package/config.example.json +9 -0
- package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
- package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
- package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
- package/dist/src/pi/chunks/runtime-services-BVQBAAWT.js +19569 -0
- package/dist/src/pi/commands.js +126 -0
- package/dist/src/pi/config-ui.js +127 -0
- package/dist/src/pi/connection.js +237 -0
- package/dist/src/pi/context.js +30 -0
- package/dist/src/pi/diagnostic-renderer.js +326 -0
- package/dist/src/pi/diagnostic-types.js +1 -0
- package/dist/src/pi/diagnostics.js +104 -0
- package/dist/src/pi/discovery.js +135 -0
- package/dist/src/pi/effect-boundary.js +5 -0
- package/dist/src/pi/ide-host.js +68 -0
- package/dist/src/pi/index.js +636 -0
- package/dist/src/pi/install.js +308 -0
- package/dist/src/pi/reconnect.js +57 -0
- package/dist/src/pi/runtime-loader.js +38 -0
- package/dist/src/pi/runtime-services.js +356 -0
- package/dist/src/pi/safety.js +61 -0
- package/dist/src/pi/selection.js +18 -0
- package/dist/src/pi/state.js +13 -0
- package/dist/src/pi/ui.js +304 -0
- package/dist/src/pi/zed.js +409 -0
- package/dist/src/shared/config-options.js +45 -0
- package/dist/src/shared/config.js +183 -0
- package/dist/src/shared/display-width.js +99 -0
- package/dist/src/shared/effect-errors.js +36 -0
- package/dist/src/shared/effect-runtime.js +36 -0
- package/dist/src/shared/effect-schema.js +120 -0
- package/dist/src/shared/errors.js +48 -0
- package/dist/src/shared/format.js +92 -0
- package/dist/src/shared/ide-server.js +167 -0
- package/dist/src/shared/jsonrpc-guard.js +12 -0
- package/dist/src/shared/lock-file-heartbeat.js +58 -0
- package/dist/src/shared/lock-file.js +50 -0
- package/dist/src/shared/paths.js +79 -0
- package/dist/src/shared/platform.js +57 -0
- package/dist/src/shared/protocol.js +3 -0
- package/dist/src/shared/schema.js +37 -0
- package/dist/src/shared/version.js +1 -0
- package/dist/src/shared/ws.js +8 -0
- package/docs/explanation/architecture.md +123 -0
- package/docs/explanation/discovery.md +113 -0
- package/docs/explanation/effect.md +127 -0
- package/docs/how-to/install-jetbrains.md +58 -0
- package/docs/how-to/install-neovim.md +138 -0
- package/docs/how-to/install-vscode.md +75 -0
- package/docs/how-to/install-zed.md +70 -0
- package/docs/how-to/troubleshoot-connection.md +103 -0
- package/docs/how-to/use-wsl2.md +90 -0
- package/docs/reference/commands.md +59 -0
- package/docs/reference/configuration.md +101 -0
- package/docs/reference/feature-parity.md +26 -0
- package/docs/reference/protocol.md +71 -0
- package/docs/specs/ide-protocol.md +215 -0
- package/docs/tutorials/get-started.md +133 -0
- package/docs/zh-CN/explanation/architecture.md +93 -0
- package/docs/zh-CN/explanation/discovery.md +71 -0
- package/docs/zh-CN/explanation/effect.md +90 -0
- package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
- package/docs/zh-CN/how-to/install-neovim.md +126 -0
- package/docs/zh-CN/how-to/install-vscode.md +67 -0
- package/docs/zh-CN/how-to/install-zed.md +56 -0
- package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
- package/docs/zh-CN/how-to/use-wsl2.md +75 -0
- package/docs/zh-CN/reference/commands.md +54 -0
- package/docs/zh-CN/reference/configuration.md +94 -0
- package/docs/zh-CN/reference/feature-parity.md +21 -0
- package/docs/zh-CN/reference/protocol.md +54 -0
- package/docs/zh-CN/tutorials/get-started.md +101 -0
- package/package.json +86 -4
- package/schemas/config.json +77 -0
|
@@ -0,0 +1,126 @@
|
|
|
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";
|
|
7
|
+
const DISABLED_ATTACH_SHORTCUT_VALUES = new Set(["", "0", "false", "off", "none", "disabled"]);
|
|
8
|
+
export function registerIdeCommand(pi, runtime, actions, options = {}) {
|
|
9
|
+
const attachShortcut = resolveAttachShortcut(options.env);
|
|
10
|
+
if (attachShortcut) {
|
|
11
|
+
pi.registerShortcut(attachShortcut, {
|
|
12
|
+
description: "Attach latest IDE selection to the prompt",
|
|
13
|
+
handler: (ctx) => {
|
|
14
|
+
runPiBoundary("IDE attach shortcut", runtime, () => {
|
|
15
|
+
runtime.ctx = ctx;
|
|
16
|
+
attachLatest(runtime, ctx);
|
|
17
|
+
}, ctx);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
pi.registerCommand("ide", {
|
|
22
|
+
description: "Manage IDE connection and editor selection context",
|
|
23
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
24
|
+
const subcommands = [
|
|
25
|
+
{ value: "status", label: "status", description: "Show IDE connection status" },
|
|
26
|
+
{ value: "list", label: "list", description: "List available IDE connections" },
|
|
27
|
+
{ value: "auto", label: "auto", description: "Auto-connect to the most recent IDE" },
|
|
28
|
+
{ value: "off", label: "off", description: "Disable IDE integration" },
|
|
29
|
+
{ value: "attach", label: "attach", description: "Attach latest IDE selection to the prompt" },
|
|
30
|
+
{ value: "install", label: "install", description: "Install or update the IDE extension" },
|
|
31
|
+
{ value: "settings", label: "settings", description: "Open IDE settings (Display, AutoInstall, ...)" },
|
|
32
|
+
];
|
|
33
|
+
const filtered = subcommands.filter((s) => s.value.startsWith(argumentPrefix));
|
|
34
|
+
return filtered.length > 0 ? filtered : null;
|
|
35
|
+
},
|
|
36
|
+
handler: (args, ctx) => runPiBoundaryAsync("/ide command", runtime, async () => {
|
|
37
|
+
runtime.ctx = ctx;
|
|
38
|
+
const [subcommand] = args.trim().split(/\s+/, 1);
|
|
39
|
+
switch (subcommand || "") {
|
|
40
|
+
case "":
|
|
41
|
+
await showPicker(runtime, actions, ctx);
|
|
42
|
+
return;
|
|
43
|
+
case "status":
|
|
44
|
+
showStatus(runtime, ctx);
|
|
45
|
+
return;
|
|
46
|
+
case "list":
|
|
47
|
+
await listCandidates(actions, ctx);
|
|
48
|
+
return;
|
|
49
|
+
case "auto":
|
|
50
|
+
runtime.enabled = true;
|
|
51
|
+
await actions.connectAuto(ctx);
|
|
52
|
+
return;
|
|
53
|
+
case "off":
|
|
54
|
+
await actions.disconnect(ctx, true);
|
|
55
|
+
return;
|
|
56
|
+
case "attach":
|
|
57
|
+
attachLatest(runtime, ctx);
|
|
58
|
+
return;
|
|
59
|
+
case "install":
|
|
60
|
+
await actions.installExtension(ctx);
|
|
61
|
+
return;
|
|
62
|
+
case "settings": {
|
|
63
|
+
const { showIdeSettings } = await import("./config-ui.js");
|
|
64
|
+
await showIdeSettings(runtime, ctx);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
ctx.ui.notify("Usage: /ide [status|list|auto|off|attach|install|settings]", "warning");
|
|
69
|
+
}
|
|
70
|
+
}, ctx),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function showPicker(runtime, actions, ctx) {
|
|
74
|
+
const candidates = await actions.refreshCandidates(ctx);
|
|
75
|
+
if (candidates.length === 0) {
|
|
76
|
+
ctx.ui.notify("No matching IDE connections found.", "warning");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const labels = candidates.map((candidate, index) => `${index + 1}. ${candidate.lock.name} — ${candidate.workspaceFolder}`);
|
|
80
|
+
labels.push("Disable IDE integration");
|
|
81
|
+
const choice = await ctx.ui.select("Select IDE connection", labels);
|
|
82
|
+
if (!choice)
|
|
83
|
+
return;
|
|
84
|
+
if (choice === "Disable IDE integration") {
|
|
85
|
+
await actions.disconnect(ctx, true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const index = labels.indexOf(choice);
|
|
89
|
+
const candidate = candidates[index];
|
|
90
|
+
if (candidate) {
|
|
91
|
+
runtime.enabled = true;
|
|
92
|
+
await actions.connectCandidate(candidate, ctx);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function showStatus(runtime, ctx) {
|
|
96
|
+
const lines = buildWidget(runtime, ctx.cwd) ?? ["IDE: disconnected"];
|
|
97
|
+
ctx.ui.notify(lines.join("\n"), runtime.connectionStatus === "error" ? "error" : "info");
|
|
98
|
+
}
|
|
99
|
+
async function listCandidates(actions, ctx) {
|
|
100
|
+
const candidates = await actions.refreshCandidates(ctx);
|
|
101
|
+
if (candidates.length === 0) {
|
|
102
|
+
ctx.ui.notify("No matching IDE lock files found.", "info");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
ctx.ui.notify(candidates
|
|
106
|
+
.map((candidate, index) => `${index + 1}. ${candidate.lock.name} ${candidate.lock.host}:${candidate.lock.port}\n ${candidate.workspaceFolder}\n ${candidate.path}`)
|
|
107
|
+
.join("\n"), "info");
|
|
108
|
+
}
|
|
109
|
+
export function resolveAttachShortcut(env = process.env) {
|
|
110
|
+
const value = resolvePiConfigEnv(env)[PI_X_IDE_ATTACH_SHORTCUT_ENV] ?? DEFAULT_ATTACH_SHORTCUT;
|
|
111
|
+
const normalized = value.trim().toLowerCase();
|
|
112
|
+
if (DISABLED_ATTACH_SHORTCUT_VALUES.has(normalized))
|
|
113
|
+
return undefined;
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function attachLatest(runtime, ctx) {
|
|
117
|
+
if (!runtime.latestSelection) {
|
|
118
|
+
ctx.ui.notify("No IDE selection is available.", "warning");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const mention = formatRangeMention(runtime.latestSelection, { cwd: ctx.cwd });
|
|
122
|
+
ctx.ui.pasteToEditor(mention + "\n");
|
|
123
|
+
runtime.attachState = "pending";
|
|
124
|
+
updateIdeUi(runtime, ctx);
|
|
125
|
+
ctx.ui.notify(`Attached ${mention}`, "info");
|
|
126
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Container, Key, matchesKey, setKeybindings, SettingsList, Text, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { resolveIdeConfigSettings, writePiConfigSettings, } from "../shared/config.js";
|
|
3
|
+
import { DEFAULT_STATUS_DISPLAY, STATUS_DISPLAY_VALUES } from "../shared/config-options.js";
|
|
4
|
+
import { logExtensionError } from "../shared/errors.js";
|
|
5
|
+
import { updateIdeUi } from "./ui.js";
|
|
6
|
+
const DISPLAY_SETTING_ID = "display";
|
|
7
|
+
const AUTO_INSTALL_SETTING_ID = "autoInstall";
|
|
8
|
+
const BOOLEAN_SETTING_VALUES = ["true", "false"];
|
|
9
|
+
const BORDER_CHAR = "\u2500";
|
|
10
|
+
const SETTINGS_LIST_MAX_VISIBLE_ROWS = 10;
|
|
11
|
+
const SETTINGS_LIST_EXTRA_ROWS = 2;
|
|
12
|
+
export async function showStatusDisplayConfig(runtime, ctx, options = {}) {
|
|
13
|
+
return showIdeSettings(runtime, ctx, options);
|
|
14
|
+
}
|
|
15
|
+
export async function showIdeConfig(runtime, ctx, options = {}) {
|
|
16
|
+
return showIdeSettings(runtime, ctx, options);
|
|
17
|
+
}
|
|
18
|
+
export async function showIdeSettings(runtime, ctx, options = {}) {
|
|
19
|
+
const resolved = resolveIdeConfigSettings({ projectDir: ctx.cwd, home: options.home });
|
|
20
|
+
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
21
|
+
setKeybindings(keybindings);
|
|
22
|
+
const draft = {
|
|
23
|
+
display: resolved.settings.display ?? DEFAULT_STATUS_DISPLAY,
|
|
24
|
+
autoInstall: resolved.settings.autoInstall,
|
|
25
|
+
};
|
|
26
|
+
const items = [
|
|
27
|
+
{
|
|
28
|
+
id: DISPLAY_SETTING_ID,
|
|
29
|
+
label: "Display",
|
|
30
|
+
description: "IDE status placement: widget (above editor) or statusline (footer)",
|
|
31
|
+
currentValue: draft.display,
|
|
32
|
+
values: [...STATUS_DISPLAY_VALUES],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: AUTO_INSTALL_SETTING_ID,
|
|
36
|
+
label: "AutoInstall",
|
|
37
|
+
description: "Auto-install VS Code-family extension on Pi startup (PI_X_IDE_AUTO_INSTALL)",
|
|
38
|
+
currentValue: draft.autoInstall ? "true" : "false",
|
|
39
|
+
values: [...BOOLEAN_SETTING_VALUES],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const settingsList = new SettingsList(items, Math.min(items.length + SETTINGS_LIST_EXTRA_ROWS, SETTINGS_LIST_MAX_VISIBLE_ROWS), createSettingsListTheme(theme), (id, newValue) => {
|
|
43
|
+
if (id === DISPLAY_SETTING_ID && isStatusDisplayValue(newValue)) {
|
|
44
|
+
draft.display = newValue;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (id === AUTO_INSTALL_SETTING_ID && isBooleanSettingValue(newValue)) {
|
|
48
|
+
draft.autoInstall = newValue === "true";
|
|
49
|
+
}
|
|
50
|
+
}, () => done({ action: "cancel" }), { enableSearch: false });
|
|
51
|
+
const title = theme.fg("accent", theme.bold("Settings:"));
|
|
52
|
+
const help = theme.fg("dim", "↑↓ navigate · space cycle · ctrl+s save global · ctrl+p save project · esc cancel");
|
|
53
|
+
const container = new Container();
|
|
54
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
55
|
+
container.addChild(new Text(title, 1, 0));
|
|
56
|
+
container.addChild(settingsList);
|
|
57
|
+
container.addChild(new Text(help, 1, 0));
|
|
58
|
+
container.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
|
59
|
+
const saveCurrent = (scope) => {
|
|
60
|
+
done({
|
|
61
|
+
action: "save",
|
|
62
|
+
settings: { display: draft.display, autoInstall: draft.autoInstall },
|
|
63
|
+
scope,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
render: (width) => container.render(width),
|
|
68
|
+
invalidate: () => container.invalidate(),
|
|
69
|
+
handleInput: (data) => {
|
|
70
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
71
|
+
saveCurrent("global");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (matchesKey(data, Key.ctrl("p"))) {
|
|
75
|
+
saveCurrent("project");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
settingsList.handleInput(data);
|
|
79
|
+
tui.requestRender();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
if (!result || result.action === "cancel")
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
const saved = writePiConfigSettings(result.settings, {
|
|
87
|
+
scope: result.scope,
|
|
88
|
+
projectDir: ctx.cwd,
|
|
89
|
+
home: options.home,
|
|
90
|
+
});
|
|
91
|
+
updateIdeUi(runtime, ctx, result.settings.display);
|
|
92
|
+
ctx.ui.notify([
|
|
93
|
+
`Saved to ${saved.scope}: ${saved.path}`,
|
|
94
|
+
`Display=${result.settings.display}`,
|
|
95
|
+
`AutoInstall=${result.settings.autoInstall}`,
|
|
96
|
+
].join("\n"), "info");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
logExtensionError("save ide settings", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isStatusDisplayValue(value) {
|
|
103
|
+
return STATUS_DISPLAY_VALUES.includes(value);
|
|
104
|
+
}
|
|
105
|
+
function isBooleanSettingValue(value) {
|
|
106
|
+
return BOOLEAN_SETTING_VALUES.includes(value);
|
|
107
|
+
}
|
|
108
|
+
function createSettingsListTheme(theme) {
|
|
109
|
+
return {
|
|
110
|
+
label: (text, selected) => (selected ? theme.fg("accent", text) : text),
|
|
111
|
+
value: (text, selected) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)),
|
|
112
|
+
description: (text) => theme.fg("dim", text),
|
|
113
|
+
cursor: theme.fg("accent", "→ "),
|
|
114
|
+
hint: (text) => theme.fg("dim", text),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export class DynamicBorder {
|
|
118
|
+
color;
|
|
119
|
+
constructor(color) {
|
|
120
|
+
this.color = color;
|
|
121
|
+
}
|
|
122
|
+
invalidate() {
|
|
123
|
+
}
|
|
124
|
+
render(width) {
|
|
125
|
+
return [this.color(BORDER_CHAR.repeat(Math.max(0, width)))];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import { EXT_CONFIG_NAME } from "../shared/config.js";
|
|
6
|
+
import { AUTH_HEADER, PROTOCOL_VERSION, } from "../shared/protocol.js";
|
|
7
|
+
import { isAtMentionedParams, isDiagnosticFixRequestedParams, isSelectionChangedParams, isSelectionClearedParams, } from "../shared/schema.js";
|
|
8
|
+
import { IdeConnectTimeoutError } from "../shared/effect-errors.js";
|
|
9
|
+
import { toError, logExtensionError } from "../shared/errors.js";
|
|
10
|
+
import { formatRangeMention } from "../shared/format.js";
|
|
11
|
+
import { normalizeEditorSelectionSnapshotForHost } from "../shared/platform.js";
|
|
12
|
+
import { decodeRawData } from "../shared/ws.js";
|
|
13
|
+
import { resolveIdeHost } from "./ide-host.js";
|
|
14
|
+
export const IDE_CONNECT_TIMEOUT_MS = 5_000;
|
|
15
|
+
const IDE_CONNECT_HANDSHAKE_TIMEOUT_PADDING_MS = 1_000;
|
|
16
|
+
export class IdeConnectionTimeoutError extends Error {
|
|
17
|
+
candidate;
|
|
18
|
+
host;
|
|
19
|
+
constructor(candidate, host = candidate.lock.host) {
|
|
20
|
+
super(`Timed out connecting to ${candidate.lock.name} at ${host}:${candidate.lock.port}`);
|
|
21
|
+
this.candidate = candidate;
|
|
22
|
+
this.host = host;
|
|
23
|
+
this.name = "IdeConnectionTimeoutError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class IdeConnection {
|
|
27
|
+
candidate;
|
|
28
|
+
cwd;
|
|
29
|
+
callbacks;
|
|
30
|
+
options;
|
|
31
|
+
socket;
|
|
32
|
+
nextId = 1;
|
|
33
|
+
closedByUser = false;
|
|
34
|
+
constructor(candidate, cwd, callbacks, options = {}) {
|
|
35
|
+
this.candidate = candidate;
|
|
36
|
+
this.cwd = cwd;
|
|
37
|
+
this.callbacks = callbacks;
|
|
38
|
+
this.options = options;
|
|
39
|
+
}
|
|
40
|
+
get isOpen() {
|
|
41
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
42
|
+
}
|
|
43
|
+
async connect(timeoutMs = IDE_CONNECT_TIMEOUT_MS) {
|
|
44
|
+
const exit = await Effect.runPromiseExit(this.connectEffect(timeoutMs));
|
|
45
|
+
if (Exit.isSuccess(exit))
|
|
46
|
+
return;
|
|
47
|
+
const failure = Cause.squash(exit.cause);
|
|
48
|
+
if (isIdeConnectTimeoutError(failure)) {
|
|
49
|
+
throw new IdeConnectionTimeoutError(this.candidate, failure.host);
|
|
50
|
+
}
|
|
51
|
+
throw failure instanceof Error ? failure : toError(failure);
|
|
52
|
+
}
|
|
53
|
+
connectEffect(timeoutMs) {
|
|
54
|
+
return Effect.gen(this, function* () {
|
|
55
|
+
this.closedByUser = false;
|
|
56
|
+
const { lock } = this.candidate;
|
|
57
|
+
const resolvedHost = yield* Effect.tryPromise({
|
|
58
|
+
try: () => (this.options.resolveHost ?? resolveIdeHost)(lock),
|
|
59
|
+
catch: (cause) => toError(cause, "resolve host"),
|
|
60
|
+
});
|
|
61
|
+
const socket = new WebSocket(`ws://${resolvedHost}:${lock.port}`, {
|
|
62
|
+
handshakeTimeout: timeoutMs + IDE_CONNECT_HANDSHAKE_TIMEOUT_PADDING_MS,
|
|
63
|
+
headers: {
|
|
64
|
+
[AUTH_HEADER]: lock.authToken,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
this.socket = socket;
|
|
68
|
+
socket.on("message", (raw) => this.runSocketHandler("message", () => this.handleMessage(decodeRawData(raw))));
|
|
69
|
+
socket.on("close", (_code, reason) => {
|
|
70
|
+
this.runSocketHandler("close", () => {
|
|
71
|
+
if (this.socket !== socket)
|
|
72
|
+
return;
|
|
73
|
+
this.socket = undefined;
|
|
74
|
+
this.emitCallback("disconnected callback", () => this.callbacks.onDisconnected?.(reason.toString("utf8") || (this.closedByUser ? "closed" : "disconnected")));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
socket.on("error", (error) => {
|
|
78
|
+
if (this.socket === socket)
|
|
79
|
+
this.reportError("websocket error", error);
|
|
80
|
+
});
|
|
81
|
+
yield* Effect.async((resume) => {
|
|
82
|
+
let settled = false;
|
|
83
|
+
const finish = (result) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
cleanup();
|
|
88
|
+
resume(result);
|
|
89
|
+
};
|
|
90
|
+
const timeout = setTimeout(() => {
|
|
91
|
+
cleanup();
|
|
92
|
+
if (this.socket === socket)
|
|
93
|
+
this.socket = undefined;
|
|
94
|
+
socket.terminate();
|
|
95
|
+
if (settled)
|
|
96
|
+
return;
|
|
97
|
+
settled = true;
|
|
98
|
+
resume(Effect.fail(new IdeConnectTimeoutError({
|
|
99
|
+
name: this.candidate.lock.name,
|
|
100
|
+
host: resolvedHost,
|
|
101
|
+
port: lock.port,
|
|
102
|
+
})));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
const onOpen = () => {
|
|
105
|
+
finish(Effect.succeed(undefined));
|
|
106
|
+
this.sendInitialize();
|
|
107
|
+
};
|
|
108
|
+
const onError = (error) => {
|
|
109
|
+
if (this.socket === socket)
|
|
110
|
+
this.socket = undefined;
|
|
111
|
+
finish(Effect.fail(error));
|
|
112
|
+
};
|
|
113
|
+
const cleanup = () => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
socket.off("open", onOpen);
|
|
116
|
+
socket.off("error", onError);
|
|
117
|
+
};
|
|
118
|
+
socket.once("open", onOpen);
|
|
119
|
+
socket.once("error", onError);
|
|
120
|
+
return Effect.sync(cleanup);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
disconnect() {
|
|
125
|
+
this.closedByUser = true;
|
|
126
|
+
this.socket?.close();
|
|
127
|
+
this.socket = undefined;
|
|
128
|
+
}
|
|
129
|
+
sendInitialize() {
|
|
130
|
+
this.socket?.send(JSON.stringify({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
id: this.nextId++,
|
|
133
|
+
method: "initialize",
|
|
134
|
+
params: {
|
|
135
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
136
|
+
client: { name: EXT_CONFIG_NAME, version: "0.1.0" },
|
|
137
|
+
cwd: this.cwd,
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
handleMessage(text) {
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(text);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (isRpcResponse(parsed)) {
|
|
150
|
+
const server = getServerInfo(parsed);
|
|
151
|
+
if (server)
|
|
152
|
+
this.emitCallback("connected callback", () => this.callbacks.onConnected?.(server));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!isNotification(parsed))
|
|
156
|
+
return;
|
|
157
|
+
if (parsed.method === "selection_changed" && isSelectionChangedParams(parsed.params)) {
|
|
158
|
+
const params = normalizeEditorSelectionSnapshotForHost(withReceivedAt(parsed.params), this.options.env);
|
|
159
|
+
this.emitCallback("selection changed callback", () => this.callbacks.onSelectionChanged?.(params));
|
|
160
|
+
}
|
|
161
|
+
else if (parsed.method === "selection_cleared" && isSelectionClearedParams(parsed.params)) {
|
|
162
|
+
const params = withReceivedAt(parsed.params);
|
|
163
|
+
this.emitCallback("selection cleared callback", () => this.callbacks.onSelectionCleared?.(params));
|
|
164
|
+
}
|
|
165
|
+
else if (parsed.method === "at_mentioned" && isAtMentionedParams(parsed.params)) {
|
|
166
|
+
const snapshot = normalizeEditorSelectionSnapshotForHost(withReceivedAt(parsed.params), this.options.env);
|
|
167
|
+
const params = { ...snapshot, rangeText: formatRangeMention(snapshot, { cwd: this.cwd, env: this.options.env }) };
|
|
168
|
+
this.emitCallback("at mentioned callback", () => this.callbacks.onAtMentioned?.(params));
|
|
169
|
+
}
|
|
170
|
+
else if (parsed.method === "diagnostic_fix_requested" && isDiagnosticFixRequestedParams(parsed.params)) {
|
|
171
|
+
const params = withReceivedAt(parsed.params);
|
|
172
|
+
this.emitCallback("diagnostic fix requested callback", () => this.callbacks.onDiagnosticFixRequested?.(params));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
runSocketHandler(label, action) {
|
|
176
|
+
try {
|
|
177
|
+
action();
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.reportError(`websocket ${label}`, error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
emitCallback(label, action) {
|
|
184
|
+
try {
|
|
185
|
+
action();
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
this.reportError(label, error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
reportError(label, error) {
|
|
192
|
+
const reported = toError(error, label);
|
|
193
|
+
if (!this.callbacks.onError) {
|
|
194
|
+
logExtensionError(label, reported);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
this.callbacks.onError(reported);
|
|
199
|
+
}
|
|
200
|
+
catch (callbackError) {
|
|
201
|
+
logExtensionError(`${label} onError callback`, callbackError);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function isIdeConnectTimeoutError(value) {
|
|
206
|
+
return value instanceof IdeConnectTimeoutError;
|
|
207
|
+
}
|
|
208
|
+
function withReceivedAt(params) {
|
|
209
|
+
return { ...params, receivedAt: params.receivedAt ?? Date.now() };
|
|
210
|
+
}
|
|
211
|
+
function isNotification(value) {
|
|
212
|
+
return (typeof value === "object" &&
|
|
213
|
+
value !== null &&
|
|
214
|
+
value.jsonrpc === "2.0" &&
|
|
215
|
+
typeof value.method === "string");
|
|
216
|
+
}
|
|
217
|
+
function isRpcResponse(value) {
|
|
218
|
+
return (typeof value === "object" && value !== null && value.jsonrpc === "2.0" && "id" in value);
|
|
219
|
+
}
|
|
220
|
+
function getServerInfo(response) {
|
|
221
|
+
const result = response.result;
|
|
222
|
+
if (!result || typeof result !== "object")
|
|
223
|
+
return undefined;
|
|
224
|
+
const server = result.server;
|
|
225
|
+
if (!server || typeof server !== "object")
|
|
226
|
+
return undefined;
|
|
227
|
+
const name = server.name;
|
|
228
|
+
if (typeof name !== "string")
|
|
229
|
+
return undefined;
|
|
230
|
+
const version = server.version;
|
|
231
|
+
const ide = server.ide;
|
|
232
|
+
return {
|
|
233
|
+
name,
|
|
234
|
+
version: typeof version === "string" ? version : undefined,
|
|
235
|
+
ide: typeof ide === "string" ? ide : undefined,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatEditorContextMessage } 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 { clearLatestSelection, setLatestSelection } from "./selection.js";
|
|
6
|
+
export function registerContextHandlers(pi, runtime) {
|
|
7
|
+
pi.on("before_agent_start", (event, ctx) => runPiBoundary("Pi before_agent_start", runtime, () => {
|
|
8
|
+
runtime.ctx = ctx;
|
|
9
|
+
if (!runtime.enabled || !runtime.latestSelection || runtime.attachState !== "pending")
|
|
10
|
+
return;
|
|
11
|
+
const snapshot = runtime.latestSelection;
|
|
12
|
+
runtime.attachState = "sent";
|
|
13
|
+
updateIdeUi(runtime, ctx);
|
|
14
|
+
if (promptContainsContextMarker(event.prompt, snapshot))
|
|
15
|
+
return;
|
|
16
|
+
return {
|
|
17
|
+
message: {
|
|
18
|
+
customType: IDE_CONTEXT_MESSAGE_TYPE,
|
|
19
|
+
content: formatEditorContextMessage(snapshot),
|
|
20
|
+
display: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}, ctx));
|
|
24
|
+
}
|
|
25
|
+
export const IDE_CONTEXT_MESSAGE_TYPE = "pi-x-ide/context";
|
|
26
|
+
function promptContainsContextMarker(prompt, snapshot) {
|
|
27
|
+
return (prompt.includes(DIAGNOSTIC_CONTEXT_MARKER) ||
|
|
28
|
+
(prompt.includes("<system-reminder><selected-context>") &&
|
|
29
|
+
(prompt.includes(snapshot.filePath) || (!!snapshot.workspaceFolder && prompt.includes(snapshot.workspaceFolder)))));
|
|
30
|
+
}
|