@dyyz1993/pi-coding-agent 0.74.45 → 0.74.47
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
- package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
- package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
- package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
- package/dist/extensions/auto-memory/contract.d.ts +16 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
- package/dist/extensions/auto-memory/contract.js.map +1 -1
- package/dist/extensions/auto-memory/contract.ts +16 -0
- package/dist/extensions/auto-memory/index.ts +134 -13
- package/dist/extensions/auto-memory/prompts.ts +10 -0
- package/dist/extensions/auto-memory/skip-rules.ts +2 -0
- package/dist/extensions/bash-ext/index.ts +855 -845
- package/dist/extensions/claude-hooks-compat/index.ts +12 -7
- package/dist/extensions/coordinator/handler.test.ts +388 -123
- package/dist/extensions/coordinator/handler.ts +78 -12
- package/dist/extensions/coordinator/index.ts +267 -198
- package/dist/extensions/coordinator/types.d.ts +16 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -1
- package/dist/extensions/coordinator/types.js.map +1 -1
- package/dist/extensions/coordinator/types.ts +57 -49
- package/dist/extensions/lsp/lsp/index.ts +15 -9
- package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
- package/dist/extensions/message-bridge/index.ts +14 -11
- package/dist/extensions/session-supervisor/index.ts +14 -8
- package/dist/extensions/subagent-v2/index.ts +58 -42
- package/dist/extensions/todo-ext/index.ts +7 -3
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +9 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ export interface CoordinatorChannelContract extends ChannelContract {
|
|
|
37
37
|
params: {
|
|
38
38
|
task: string;
|
|
39
39
|
title?: string;
|
|
40
|
+
projectPath?: string;
|
|
40
41
|
};
|
|
41
42
|
return: DelegateCreateResult;
|
|
42
43
|
};
|
|
@@ -65,11 +66,26 @@ export interface CoordinatorChannelContract extends ChannelContract {
|
|
|
65
66
|
ok: boolean;
|
|
66
67
|
};
|
|
67
68
|
};
|
|
69
|
+
session_delegate_remove: {
|
|
70
|
+
params: {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
};
|
|
73
|
+
return: {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
session_delegate_clear_stopped: {
|
|
78
|
+
params: Record<string, never>;
|
|
79
|
+
return: {
|
|
80
|
+
removed: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
68
83
|
session_delegate_fork: {
|
|
69
84
|
params: {
|
|
70
85
|
sessionId: string;
|
|
71
86
|
task: string;
|
|
72
87
|
title?: string;
|
|
88
|
+
projectPath?: string;
|
|
73
89
|
};
|
|
74
90
|
return: DelegateCreateResult;
|
|
75
91
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,eAAO,MAAM,wBAAwB,gBAAgB,CAAC;AAEtD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAE3E,MAAM,WAAW,aAAa;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChannelContract } from \"@dyyz1993/pi-coding-agent\";\n\nexport const COORDINATOR_CHANNEL_NAME = \"coordinator\";\n\nexport type SessionStatus = \"idle\" | \"streaming\" | \"stopped\" | \"completed\";\n\nexport interface DelegatedTask {\n sessionId: string;\n title: string;\n task: string;\n projectPath: string;\n dispatchedAt: number;\n status: SessionStatus;\n completedAt?: number;\n result?: string;\n}\n\nexport interface DelegateCreateResult {\n sessionId: string;\n status: \"started\" | \"already_running\";\n}\n\nexport interface DelegateSendResult {\n delivered: boolean;\n targetStatus: \"active\" | \"started\" | \"not_found\";\n}\n\nexport interface DelegateListResult {\n tasks: DelegatedTask[];\n}\n\nexport interface DelegateStatusExt {\n task: DelegatedTask | null;\n isCompacting?: boolean;\n contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null };\n}\n\nexport interface CoordinatorChannelContract extends ChannelContract {\n methods: {\n session_delegate: {\n params: { task: string; title?: string; projectPath?: string };\n return: DelegateCreateResult;\n };\n session_delegate_send: {\n params: { targetSessionId: string; message: string };\n return: DelegateSendResult;\n };\n session_delegate_status: {\n params: { sessionId: string };\n return: DelegateStatusExt;\n };\n session_delegate_list: {\n params: Record<string, never>;\n return: DelegateListResult;\n };\n session_delegate_stop: {\n params: { sessionId: string };\n return: { ok: boolean };\n };\n session_delegate_remove: {\n params: { sessionId: string };\n return: { ok: boolean };\n };\n session_delegate_clear_stopped: {\n params: Record<string, never>;\n return: { removed: number };\n };\n session_delegate_fork: {\n params: { sessionId: string; task: string; title?: string; projectPath?: string };\n return: DelegateCreateResult;\n };\n };\n events: {\n message_received: { fromSessionId: string; message: string };\n task_started: { sessionId: string; title: string; task: string };\n task_stopped: { sessionId: string };\n task_completed: { sessionId: string; result?: string };\n task_error: { sessionId: string; error: string };\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAC","sourcesContent":["import type { ChannelContract } from \"@dyyz1993/pi-coding-agent\";\n\nexport const COORDINATOR_CHANNEL_NAME = \"coordinator\";\n\nexport type SessionStatus = \"idle\" | \"streaming\" | \"stopped\" | \"completed\";\n\nexport interface DelegatedTask {\n
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAC","sourcesContent":["import type { ChannelContract } from \"@dyyz1993/pi-coding-agent\";\n\nexport const COORDINATOR_CHANNEL_NAME = \"coordinator\";\n\nexport type SessionStatus = \"idle\" | \"streaming\" | \"stopped\" | \"completed\";\n\nexport interface DelegatedTask {\n sessionId: string;\n title: string;\n task: string;\n projectPath: string;\n dispatchedAt: number;\n status: SessionStatus;\n completedAt?: number;\n result?: string;\n}\n\nexport interface DelegateCreateResult {\n sessionId: string;\n status: \"started\" | \"already_running\";\n}\n\nexport interface DelegateSendResult {\n delivered: boolean;\n targetStatus: \"active\" | \"started\" | \"not_found\";\n}\n\nexport interface DelegateListResult {\n tasks: DelegatedTask[];\n}\n\nexport interface DelegateStatusExt {\n task: DelegatedTask | null;\n isCompacting?: boolean;\n contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null };\n}\n\nexport interface CoordinatorChannelContract extends ChannelContract {\n methods: {\n session_delegate: {\n params: { task: string; title?: string; projectPath?: string };\n return: DelegateCreateResult;\n };\n session_delegate_send: {\n params: { targetSessionId: string; message: string };\n return: DelegateSendResult;\n };\n session_delegate_status: {\n params: { sessionId: string };\n return: DelegateStatusExt;\n };\n session_delegate_list: {\n params: Record<string, never>;\n return: DelegateListResult;\n };\n session_delegate_stop: {\n params: { sessionId: string };\n return: { ok: boolean };\n };\n session_delegate_remove: {\n params: { sessionId: string };\n return: { ok: boolean };\n };\n session_delegate_clear_stopped: {\n params: Record<string, never>;\n return: { removed: number };\n };\n session_delegate_fork: {\n params: { sessionId: string; task: string; title?: string; projectPath?: string };\n return: DelegateCreateResult;\n };\n };\n events: {\n message_received: { fromSessionId: string; message: string };\n task_started: { sessionId: string; title: string; task: string };\n task_stopped: { sessionId: string };\n task_completed: { sessionId: string; result?: string };\n task_error: { sessionId: string; error: string };\n };\n}\n"]}
|
|
@@ -5,68 +5,76 @@ export const COORDINATOR_CHANNEL_NAME = "coordinator";
|
|
|
5
5
|
export type SessionStatus = "idle" | "streaming" | "stopped" | "completed";
|
|
6
6
|
|
|
7
7
|
export interface DelegatedTask {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
sessionId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
task: string;
|
|
11
|
+
projectPath: string;
|
|
12
|
+
dispatchedAt: number;
|
|
13
|
+
status: SessionStatus;
|
|
14
|
+
completedAt?: number;
|
|
15
|
+
result?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface DelegateCreateResult {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
sessionId: string;
|
|
20
|
+
status: "started" | "already_running";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface DelegateSendResult {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
delivered: boolean;
|
|
25
|
+
targetStatus: "active" | "started" | "not_found";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface DelegateListResult {
|
|
29
|
-
|
|
29
|
+
tasks: DelegatedTask[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface DelegateStatusExt {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
task: DelegatedTask | null;
|
|
34
|
+
isCompacting?: boolean;
|
|
35
|
+
contextUsage?: { tokens: number | null; contextWindow: number; percent: number | null };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface CoordinatorChannelContract extends ChannelContract {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
39
|
+
methods: {
|
|
40
|
+
session_delegate: {
|
|
41
|
+
params: { task: string; title?: string; projectPath?: string };
|
|
42
|
+
return: DelegateCreateResult;
|
|
43
|
+
};
|
|
44
|
+
session_delegate_send: {
|
|
45
|
+
params: { targetSessionId: string; message: string };
|
|
46
|
+
return: DelegateSendResult;
|
|
47
|
+
};
|
|
48
|
+
session_delegate_status: {
|
|
49
|
+
params: { sessionId: string };
|
|
50
|
+
return: DelegateStatusExt;
|
|
51
|
+
};
|
|
52
|
+
session_delegate_list: {
|
|
53
|
+
params: Record<string, never>;
|
|
54
|
+
return: DelegateListResult;
|
|
55
|
+
};
|
|
56
|
+
session_delegate_stop: {
|
|
57
|
+
params: { sessionId: string };
|
|
58
|
+
return: { ok: boolean };
|
|
59
|
+
};
|
|
60
|
+
session_delegate_remove: {
|
|
61
|
+
params: { sessionId: string };
|
|
62
|
+
return: { ok: boolean };
|
|
63
|
+
};
|
|
64
|
+
session_delegate_clear_stopped: {
|
|
65
|
+
params: Record<string, never>;
|
|
66
|
+
return: { removed: number };
|
|
67
|
+
};
|
|
68
|
+
session_delegate_fork: {
|
|
69
|
+
params: { sessionId: string; task: string; title?: string; projectPath?: string };
|
|
70
|
+
return: DelegateCreateResult;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
events: {
|
|
74
|
+
message_received: { fromSessionId: string; message: string };
|
|
75
|
+
task_started: { sessionId: string; title: string; task: string };
|
|
76
|
+
task_stopped: { sessionId: string };
|
|
77
|
+
task_completed: { sessionId: string; result?: string };
|
|
78
|
+
task_error: { sessionId: string; error: string };
|
|
79
|
+
};
|
|
72
80
|
}
|
|
@@ -71,15 +71,21 @@ export default function lspExtension(pi: ExtensionAPI): void {
|
|
|
71
71
|
})),
|
|
72
72
|
}));
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
try {
|
|
75
|
+
pi.sendMessage(
|
|
76
|
+
{
|
|
77
|
+
customType: "lsp_diagnostics",
|
|
78
|
+
content: `[LSP] Post-edit diagnostics found issues in ${results.length} file(s): ${summary}.\nPlease review and fix the issues listed below.`,
|
|
79
|
+
display: true,
|
|
80
|
+
details: { files: fileSummaries },
|
|
81
|
+
},
|
|
82
|
+
{ triggerTurn: true },
|
|
83
|
+
);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
if (msg.includes("stale")) return;
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
83
89
|
});
|
|
84
90
|
|
|
85
91
|
let idleCleanupTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it, vi, beforeAll, afterAll } from "vitest";
|
|
5
|
+
import type { ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
6
|
+
import lspExtensionDefault from "./index.js";
|
|
7
|
+
|
|
8
|
+
const TEST_DIR = "/tmp/lsp-clangd-test";
|
|
9
|
+
const TEST_FILE = "test.c";
|
|
10
|
+
|
|
11
|
+
function createMockPi() {
|
|
12
|
+
const handlers: Record<string, Array<(event: any, ctx: any) => any>> = {};
|
|
13
|
+
const registeredTools = new Map<string, any>();
|
|
14
|
+
const channelSendFn = vi.fn();
|
|
15
|
+
const registerCommandFn = vi.fn();
|
|
16
|
+
let channelOnReceiveHandler: ((data: unknown) => void) | null = null;
|
|
17
|
+
let currentChannel: {
|
|
18
|
+
name: string;
|
|
19
|
+
send: (data: unknown) => void;
|
|
20
|
+
onReceive: (handler: (data: unknown) => void) => () => void;
|
|
21
|
+
invoke: (data: unknown, timeoutMs?: number) => Promise<unknown>;
|
|
22
|
+
call: (method: string, params: Record<string, unknown>, timeoutMs?: number) => Promise<unknown>;
|
|
23
|
+
} | null = null;
|
|
24
|
+
|
|
25
|
+
const pi = {
|
|
26
|
+
on: vi.fn((event: string, handler: any) => {
|
|
27
|
+
if (!handlers[event]) handlers[event] = [];
|
|
28
|
+
handlers[event].push(handler);
|
|
29
|
+
}),
|
|
30
|
+
callLLM: vi.fn(async () => "{}"),
|
|
31
|
+
callLLMStructured: vi.fn(async () => ({})),
|
|
32
|
+
forkAgent: vi.fn(async () => ({ text: "", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 } })),
|
|
33
|
+
once: vi.fn(),
|
|
34
|
+
emit: vi.fn(),
|
|
35
|
+
setStatus: vi.fn(),
|
|
36
|
+
registerProvider: vi.fn(),
|
|
37
|
+
unregisterProvider: vi.fn(),
|
|
38
|
+
events: { on: vi.fn(), off: vi.fn(), emit: vi.fn(), once: vi.fn() },
|
|
39
|
+
registerChannel: vi.fn(() => {
|
|
40
|
+
currentChannel = {
|
|
41
|
+
name: "lsp",
|
|
42
|
+
send: channelSendFn,
|
|
43
|
+
onReceive: vi.fn((handler: (data: unknown) => void) => {
|
|
44
|
+
channelOnReceiveHandler = handler;
|
|
45
|
+
return () => { channelOnReceiveHandler = null; };
|
|
46
|
+
}),
|
|
47
|
+
invoke: vi.fn(async (data: unknown) => {
|
|
48
|
+
if (!channelOnReceiveHandler) return {};
|
|
49
|
+
const msg = data as Record<string, unknown>;
|
|
50
|
+
const invokeId = msg.__invokeId as string;
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const orig = channelSendFn.getMockImplementation() ?? channelSendFn;
|
|
53
|
+
channelSendFn.mockImplementation((response: unknown) => {
|
|
54
|
+
const resp = response as Record<string, unknown>;
|
|
55
|
+
if (resp?.invokeId === invokeId) {
|
|
56
|
+
channelSendFn.mockImplementation(orig as any);
|
|
57
|
+
resolve(response);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
channelOnReceiveHandler!(data);
|
|
61
|
+
});
|
|
62
|
+
}),
|
|
63
|
+
call: vi.fn(async (method: string, params: Record<string, unknown>, _timeoutMs?: number) => {
|
|
64
|
+
if (!channelOnReceiveHandler) return {};
|
|
65
|
+
const invokeId = `invoke_${method}_${Date.now()}`;
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const orig = channelSendFn.getMockImplementation() ?? channelSendFn;
|
|
68
|
+
channelSendFn.mockImplementation((response: unknown) => {
|
|
69
|
+
const resp = response as Record<string, unknown>;
|
|
70
|
+
if (resp?.invokeId === invokeId) {
|
|
71
|
+
channelSendFn.mockImplementation(orig as any);
|
|
72
|
+
resolve(response);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
channelOnReceiveHandler!({ __call: method, invokeId, ...params });
|
|
76
|
+
});
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
return currentChannel;
|
|
80
|
+
}),
|
|
81
|
+
registerTool: vi.fn((tool: any) => {
|
|
82
|
+
registeredTools.set(tool.name, tool);
|
|
83
|
+
}),
|
|
84
|
+
registerCommand: registerCommandFn,
|
|
85
|
+
appendEntry: vi.fn(),
|
|
86
|
+
sendMessage: vi.fn(),
|
|
87
|
+
off: vi.fn(),
|
|
88
|
+
} as unknown as ExtensionAPI;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
pi,
|
|
92
|
+
handlers,
|
|
93
|
+
registeredTools,
|
|
94
|
+
channelSend: channelSendFn,
|
|
95
|
+
registerCommandFn,
|
|
96
|
+
getCurrentChannel: () => currentChannel,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fireSessionStart(
|
|
101
|
+
mock: ReturnType<typeof createMockPi>,
|
|
102
|
+
cwd: string,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
for (const h of mock.handlers.session_start ?? []) {
|
|
105
|
+
await h(
|
|
106
|
+
{},
|
|
107
|
+
{
|
|
108
|
+
sessionManager: { getBranch: () => [] },
|
|
109
|
+
hasUI: false,
|
|
110
|
+
ui: { notify: vi.fn() },
|
|
111
|
+
cwd,
|
|
112
|
+
isIdle: () => true,
|
|
113
|
+
signal: undefined,
|
|
114
|
+
abort: () => {},
|
|
115
|
+
hasPendingMessages: () => false,
|
|
116
|
+
shutdown: () => {},
|
|
117
|
+
getContextUsage: () => undefined,
|
|
118
|
+
compact: () => {},
|
|
119
|
+
getSystemPrompt: () => "",
|
|
120
|
+
model: undefined,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function fireSessionShutdown(mock: ReturnType<typeof createMockPi>): Promise<void> {
|
|
127
|
+
for (const h of mock.handlers.session_shutdown ?? []) {
|
|
128
|
+
await h({}, {});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fireToolResult(
|
|
133
|
+
mock: ReturnType<typeof createMockPi>,
|
|
134
|
+
filePath: string,
|
|
135
|
+
toolName: "write" | "edit" = "write",
|
|
136
|
+
): Promise<any> {
|
|
137
|
+
const results: any[] = [];
|
|
138
|
+
for (const h of mock.handlers.tool_result ?? []) {
|
|
139
|
+
const result = await h(
|
|
140
|
+
{
|
|
141
|
+
type: "tool_result",
|
|
142
|
+
toolCallId: "tc_e2e_1",
|
|
143
|
+
toolName,
|
|
144
|
+
input: { path: filePath },
|
|
145
|
+
content: [{ type: "text", text: `File written: ${filePath}` }],
|
|
146
|
+
isError: false,
|
|
147
|
+
details: undefined,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
cwd: TEST_DIR,
|
|
151
|
+
ui: { notify: vi.fn() },
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
if (result) results.push(result);
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
describe("clangd E2E integration", () => {
|
|
160
|
+
const originalCwd = process.cwd();
|
|
161
|
+
|
|
162
|
+
beforeAll(async () => {
|
|
163
|
+
await mkdir(join(TEST_DIR, ".pi"), { recursive: true });
|
|
164
|
+
await writeFile(join(TEST_DIR, ".pi", "lsp.json"), JSON.stringify({
|
|
165
|
+
servers: [
|
|
166
|
+
{
|
|
167
|
+
name: "clangd",
|
|
168
|
+
command: ["clangd"],
|
|
169
|
+
fileTypes: [".c", ".h", ".cpp", ".hpp", ".cc", ".cxx"],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}));
|
|
173
|
+
await writeFile(join(TEST_DIR, TEST_FILE), `#include <stdio.h>\n\nint main() {\n int x = "hello";\n printf("%d\\n", x);\n return 0;\n}\n`);
|
|
174
|
+
process.chdir(TEST_DIR);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterAll(async () => {
|
|
178
|
+
process.chdir(originalCwd);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it(
|
|
182
|
+
"starts clangd, detects type error in test.c via diagnostics",
|
|
183
|
+
async () => {
|
|
184
|
+
const mock = createMockPi();
|
|
185
|
+
lspExtensionDefault(mock.pi);
|
|
186
|
+
|
|
187
|
+
await fireSessionStart(mock, TEST_DIR);
|
|
188
|
+
|
|
189
|
+
const channel = mock.getCurrentChannel();
|
|
190
|
+
expect(channel).not.toBeNull();
|
|
191
|
+
|
|
192
|
+
const statusResult = await channel!.call("getStatus", {});
|
|
193
|
+
console.log("[e2e] Status after session_start:", JSON.stringify(statusResult, null, 2));
|
|
194
|
+
|
|
195
|
+
const status = statusResult as any;
|
|
196
|
+
expect(status.state).toBeDefined();
|
|
197
|
+
|
|
198
|
+
const readyServers = (status.servers as any[])?.filter((s: any) => s.state === "ready") ?? [];
|
|
199
|
+
console.log(`[e2e] Ready servers: ${readyServers.length}`);
|
|
200
|
+
for (const s of readyServers) {
|
|
201
|
+
console.log(`[e2e] - ${s.name} [${(s.fileTypes ?? []).join(",")}] state=${s.state}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (readyServers.length === 0) {
|
|
205
|
+
console.log("[e2e] No clangd server became ready — skipping diagnostics check");
|
|
206
|
+
console.log("[e2e] All servers:", JSON.stringify(status.servers, null, 2));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
expect(readyServers.length).toBeGreaterThanOrEqual(1);
|
|
210
|
+
|
|
211
|
+
const toolResults = await fireToolResult(mock, TEST_FILE);
|
|
212
|
+
console.log("[e2e] tool_result handler results:", JSON.stringify(toolResults, null, 2));
|
|
213
|
+
|
|
214
|
+
const diagnosticsContent = toolResults.find(
|
|
215
|
+
(r: any) => r?.content?.some?.((c: any) => c.text?.includes("[LSP]")),
|
|
216
|
+
);
|
|
217
|
+
if (diagnosticsContent) {
|
|
218
|
+
console.log("[e2e] Diagnostics found in tool_result response:");
|
|
219
|
+
for (const c of diagnosticsContent.content) {
|
|
220
|
+
console.log(c.text);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await fireSessionShutdown(mock);
|
|
225
|
+
console.log("[e2e] Session shutdown complete");
|
|
226
|
+
},
|
|
227
|
+
30_000,
|
|
228
|
+
);
|
|
229
|
+
});
|
|
@@ -139,6 +139,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
139
139
|
|
|
140
140
|
pi.on("ui", async (event: any, ctx: any) => {
|
|
141
141
|
if (event.method === "notify") {
|
|
142
|
+
if (event.message == null) return undefined;
|
|
142
143
|
pushAndWait(event.message, sessionId).catch((err) => console.debug("[message-bridge] notify push failed:", err instanceof Error ? err.message : err));
|
|
143
144
|
return undefined;
|
|
144
145
|
}
|
|
@@ -148,7 +149,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
148
149
|
pushAndWait(question, sessionId)
|
|
149
150
|
.then((answer) => {
|
|
150
151
|
const confirmed = parseConfirmAnswer(answer);
|
|
151
|
-
ctx.respondUI(event.id, { action: "responded", confirmed });
|
|
152
|
+
try { ctx.respondUI(event.id, { action: "responded", confirmed }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
152
153
|
})
|
|
153
154
|
.catch((err) => console.debug("[message-bridge] confirm push failed:", err instanceof Error ? err.message : err));
|
|
154
155
|
return undefined;
|
|
@@ -160,13 +161,15 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
160
161
|
const question = buildSelectQuestion(event.title, options, multiple);
|
|
161
162
|
pushAndWait(question, sessionId)
|
|
162
163
|
.then((answer) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
try {
|
|
165
|
+
if (multiple) {
|
|
166
|
+
const values = parseMultiSelectAnswer(answer, options);
|
|
167
|
+
ctx.respondUI(event.id, { action: "responded", value: values });
|
|
168
|
+
} else {
|
|
169
|
+
const value = parseSelectAnswer(answer);
|
|
170
|
+
ctx.respondUI(event.id, { action: "responded", value });
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
170
173
|
})
|
|
171
174
|
.catch((err) => console.debug("[message-bridge] select push failed:", err instanceof Error ? err.message : err));
|
|
172
175
|
return undefined;
|
|
@@ -178,7 +181,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
178
181
|
: event.title;
|
|
179
182
|
pushAndWait(question, sessionId)
|
|
180
183
|
.then((answer) => {
|
|
181
|
-
ctx.respondUI(event.id, { action: "responded", value: answer });
|
|
184
|
+
try { ctx.respondUI(event.id, { action: "responded", value: answer }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
182
185
|
})
|
|
183
186
|
.catch((err) => console.debug("[message-bridge] input push failed:", err instanceof Error ? err.message : err));
|
|
184
187
|
return undefined;
|
|
@@ -190,7 +193,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
190
193
|
: event.title;
|
|
191
194
|
pushAndWait(question, sessionId)
|
|
192
195
|
.then((answer) => {
|
|
193
|
-
ctx.respondUI(event.id, { action: "responded", value: answer });
|
|
196
|
+
try { ctx.respondUI(event.id, { action: "responded", value: answer }); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
194
197
|
})
|
|
195
198
|
.catch((err) => console.debug("[message-bridge] editor push failed:", err instanceof Error ? err.message : err));
|
|
196
199
|
return undefined;
|
|
@@ -214,7 +217,7 @@ export default function messageBridgeExtension(pi: any) {
|
|
|
214
217
|
.then((id) => pullAnswer(id))
|
|
215
218
|
.then((answer) => {
|
|
216
219
|
if (answer?.trim()) {
|
|
217
|
-
pi.sendUserMessage(answer.trim());
|
|
220
|
+
try { pi.sendUserMessage(answer.trim()); } catch (e) { if (!/stale/i.test(e instanceof Error ? e.message : "")) throw e; }
|
|
218
221
|
}
|
|
219
222
|
})
|
|
220
223
|
.catch((err) => console.debug("[message-bridge] agent_end push failed:", err instanceof Error ? err.message : err));
|
|
@@ -369,14 +369,20 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
369
369
|
|
|
370
370
|
currentState = "continuing";
|
|
371
371
|
emitStatusChanged();
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
try {
|
|
373
|
+
pi.sendMessage(
|
|
374
|
+
{
|
|
375
|
+
customType: "supervisor_continue",
|
|
376
|
+
content: continueMessage,
|
|
377
|
+
display: true,
|
|
378
|
+
},
|
|
379
|
+
{ triggerTurn: true },
|
|
380
|
+
);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
if (/stale|abort/i.test(msg)) return;
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
380
386
|
});
|
|
381
387
|
}
|
|
382
388
|
|