@docyrus/docyrus 0.0.20 → 0.0.21
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/agent-loader.js +32 -1
- package/agent-loader.js.map +2 -2
- package/main.js +321 -70
- package/main.js.map +4 -4
- package/package.json +12 -2
- package/resources/chrome-tools/browser-content.js +103 -0
- package/resources/chrome-tools/browser-cookies.js +35 -0
- package/resources/chrome-tools/browser-eval.js +53 -0
- package/resources/chrome-tools/browser-hn-scraper.js +108 -0
- package/resources/chrome-tools/browser-nav.js +44 -0
- package/resources/chrome-tools/browser-pick.js +162 -0
- package/resources/chrome-tools/browser-screenshot.js +34 -0
- package/resources/chrome-tools/browser-start.js +86 -0
- package/resources/pi-agent/extensions/answer.ts +532 -0
- package/resources/pi-agent/extensions/context.ts +578 -0
- package/resources/pi-agent/extensions/control.ts +1779 -0
- package/resources/pi-agent/extensions/diff.ts +218 -0
- package/resources/pi-agent/extensions/files.ts +199 -0
- package/resources/pi-agent/extensions/loop.ts +446 -0
- package/resources/pi-agent/extensions/multi-edit.ts +835 -0
- package/resources/pi-agent/extensions/notify.ts +88 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
- package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
- package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
- package/resources/pi-agent/extensions/redraws.ts +24 -0
- package/resources/pi-agent/extensions/review.ts +2160 -0
- package/resources/pi-agent/extensions/todos.ts +2076 -0
- package/resources/pi-agent/extensions/tps.ts +47 -0
- package/resources/pi-agent/extensions/whimsical.ts +474 -0
- package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
- package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +51 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { McpExtensionState } from "./state.js";
|
|
4
|
+
import {
|
|
5
|
+
extractUiPromptText,
|
|
6
|
+
UI_STREAM_HOST_CONTEXT_KEY,
|
|
7
|
+
UI_STREAM_REQUEST_META_KEY,
|
|
8
|
+
UI_STREAM_STRUCTURED_CONTENT_KEY,
|
|
9
|
+
type UiHostContext,
|
|
10
|
+
type UiMessageParams,
|
|
11
|
+
type UiModelContextParams,
|
|
12
|
+
type UiStreamMode,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import { logger } from "./logger.js";
|
|
15
|
+
import { startUiServer, type UiServerHandle } from "./ui-server.js";
|
|
16
|
+
import { isGlimpseAvailable, openGlimpseWindow } from "./glimpse-ui.js";
|
|
17
|
+
|
|
18
|
+
let activeGlimpseWindow: { close(): void } | null = null;
|
|
19
|
+
|
|
20
|
+
export interface UiSessionRequest {
|
|
21
|
+
serverName: string;
|
|
22
|
+
toolName: string;
|
|
23
|
+
toolArgs: Record<string, unknown>;
|
|
24
|
+
uiResourceUri: string;
|
|
25
|
+
streamMode?: UiStreamMode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UiSessionRuntime {
|
|
29
|
+
serverName: string;
|
|
30
|
+
toolName: string;
|
|
31
|
+
reused: boolean;
|
|
32
|
+
streamId?: string;
|
|
33
|
+
streamToken?: string;
|
|
34
|
+
streamMode?: UiStreamMode;
|
|
35
|
+
requestMeta?: Record<string, unknown>;
|
|
36
|
+
url: string;
|
|
37
|
+
isActive: () => boolean;
|
|
38
|
+
sendToolResult: (result: CallToolResult) => void;
|
|
39
|
+
sendResultPatch: (result: CallToolResult) => void;
|
|
40
|
+
sendToolCancelled: (reason: string) => void;
|
|
41
|
+
close: (reason?: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MAX_COMPLETED_SESSIONS = 10;
|
|
45
|
+
|
|
46
|
+
function withStreamEnvelope(
|
|
47
|
+
result: CallToolResult,
|
|
48
|
+
streamId: string | undefined,
|
|
49
|
+
sequence: number,
|
|
50
|
+
): CallToolResult {
|
|
51
|
+
if (!streamId) {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const structuredContent = result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent)
|
|
56
|
+
? { ...result.structuredContent }
|
|
57
|
+
: {};
|
|
58
|
+
|
|
59
|
+
const rawEnvelope = structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY];
|
|
60
|
+
const envelope = rawEnvelope && typeof rawEnvelope === "object" && !Array.isArray(rawEnvelope)
|
|
61
|
+
? { ...rawEnvelope as Record<string, unknown> }
|
|
62
|
+
: {
|
|
63
|
+
frameType: "final",
|
|
64
|
+
phase: "settled",
|
|
65
|
+
status: result.isError ? "error" : "ok",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY] = {
|
|
69
|
+
...envelope,
|
|
70
|
+
streamId,
|
|
71
|
+
sequence,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...result,
|
|
76
|
+
structuredContent,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function openInBrowser(state: McpExtensionState, url: string): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
await state.openBrowser(url);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
state.ui?.notify(`MCP UI browser open failed: ${message}`, "warning");
|
|
86
|
+
state.ui?.notify(`Open manually: ${url}`, "info");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function maybeStartUiSession(
|
|
91
|
+
state: McpExtensionState,
|
|
92
|
+
request: UiSessionRequest,
|
|
93
|
+
): Promise<UiSessionRuntime | null> {
|
|
94
|
+
const log = logger.child({
|
|
95
|
+
component: "UiSession",
|
|
96
|
+
server: request.serverName,
|
|
97
|
+
tool: request.toolName,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
if (
|
|
102
|
+
state.uiServer &&
|
|
103
|
+
state.uiServer.serverName === request.serverName &&
|
|
104
|
+
state.uiServer.toolName === request.toolName
|
|
105
|
+
) {
|
|
106
|
+
const existingHandle = state.uiServer;
|
|
107
|
+
const streamMode = request.streamMode;
|
|
108
|
+
const streamId = streamMode ? randomUUID() : undefined;
|
|
109
|
+
const streamToken = streamMode ? randomUUID() : undefined;
|
|
110
|
+
let active = true;
|
|
111
|
+
let nextStreamSequence = 0;
|
|
112
|
+
|
|
113
|
+
const cleanupStreamListener = () => {
|
|
114
|
+
if (streamToken) {
|
|
115
|
+
state.manager.removeUiStreamListener(streamToken);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
existingHandle.sendToolInput(request.toolArgs);
|
|
120
|
+
|
|
121
|
+
if (streamToken) {
|
|
122
|
+
state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
|
|
123
|
+
if (!active || state.uiServer !== existingHandle) return;
|
|
124
|
+
if (serverName !== request.serverName) return;
|
|
125
|
+
nextStreamSequence += 1;
|
|
126
|
+
existingHandle.sendResultPatch(
|
|
127
|
+
withStreamEnvelope(notification.result as CallToolResult, streamId, nextStreamSequence),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
serverName: request.serverName,
|
|
134
|
+
toolName: request.toolName,
|
|
135
|
+
reused: true,
|
|
136
|
+
streamId,
|
|
137
|
+
streamToken,
|
|
138
|
+
streamMode,
|
|
139
|
+
requestMeta: streamToken ? { [UI_STREAM_REQUEST_META_KEY]: streamToken } : undefined,
|
|
140
|
+
url: existingHandle.url,
|
|
141
|
+
isActive: () => active && state.uiServer === existingHandle,
|
|
142
|
+
sendToolResult: (result: CallToolResult) => {
|
|
143
|
+
if (!active || state.uiServer !== existingHandle) return;
|
|
144
|
+
nextStreamSequence += 1;
|
|
145
|
+
existingHandle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
146
|
+
},
|
|
147
|
+
sendResultPatch: (result: CallToolResult) => {
|
|
148
|
+
if (!active || state.uiServer !== existingHandle) return;
|
|
149
|
+
nextStreamSequence += 1;
|
|
150
|
+
existingHandle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
151
|
+
},
|
|
152
|
+
sendToolCancelled: (reason: string) => {
|
|
153
|
+
if (!active || state.uiServer !== existingHandle) return;
|
|
154
|
+
nextStreamSequence += 1;
|
|
155
|
+
existingHandle.sendToolResult(
|
|
156
|
+
withStreamEnvelope(
|
|
157
|
+
{
|
|
158
|
+
isError: true,
|
|
159
|
+
content: [{ type: "text", text: reason }],
|
|
160
|
+
},
|
|
161
|
+
streamId,
|
|
162
|
+
nextStreamSequence,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
close: () => {
|
|
167
|
+
active = false;
|
|
168
|
+
cleanupStreamListener();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const resource = await state.uiResourceHandler.readUiResource(request.serverName, request.uiResourceUri);
|
|
174
|
+
|
|
175
|
+
if (state.uiServer) {
|
|
176
|
+
state.uiServer.close("replaced");
|
|
177
|
+
state.uiServer = null;
|
|
178
|
+
}
|
|
179
|
+
if (activeGlimpseWindow) {
|
|
180
|
+
activeGlimpseWindow.close();
|
|
181
|
+
activeGlimpseWindow = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const streamMode = request.streamMode;
|
|
185
|
+
const streamId = streamMode ? randomUUID() : undefined;
|
|
186
|
+
const streamToken = streamMode ? randomUUID() : undefined;
|
|
187
|
+
const hostContext: UiHostContext | undefined = streamMode && streamId
|
|
188
|
+
? {
|
|
189
|
+
[UI_STREAM_HOST_CONTEXT_KEY]: {
|
|
190
|
+
mode: streamMode,
|
|
191
|
+
streamId,
|
|
192
|
+
intermediateResultPatches: streamMode === "stream-first",
|
|
193
|
+
partialInput: false,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
|
|
198
|
+
let active = true;
|
|
199
|
+
let nextStreamSequence = 0;
|
|
200
|
+
let handle: UiServerHandle | null = null;
|
|
201
|
+
|
|
202
|
+
const cleanupStreamListener = () => {
|
|
203
|
+
if (streamToken) {
|
|
204
|
+
state.manager.removeUiStreamListener(streamToken);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
handle = await startUiServer({
|
|
209
|
+
serverName: request.serverName,
|
|
210
|
+
toolName: request.toolName,
|
|
211
|
+
toolArgs: streamMode === "stream-first" ? {} : request.toolArgs,
|
|
212
|
+
resource,
|
|
213
|
+
manager: state.manager,
|
|
214
|
+
consentManager: state.consentManager,
|
|
215
|
+
hostContext,
|
|
216
|
+
|
|
217
|
+
onMessage: (params: UiMessageParams) => {
|
|
218
|
+
const prompt = extractUiPromptText(params);
|
|
219
|
+
if (prompt) {
|
|
220
|
+
if (state.sendMessage) {
|
|
221
|
+
state.sendMessage(
|
|
222
|
+
{
|
|
223
|
+
customType: "mcp-ui-prompt",
|
|
224
|
+
content: [{ type: "text", text: `User sent prompt from ${request.serverName} UI: "${prompt}"` }],
|
|
225
|
+
display: `💬 UI Prompt: ${prompt}`,
|
|
226
|
+
details: { server: request.serverName, tool: request.toolName, prompt },
|
|
227
|
+
},
|
|
228
|
+
{ triggerTurn: true },
|
|
229
|
+
);
|
|
230
|
+
log.debug("Triggered agent turn for UI prompt", { prompt: prompt.slice(0, 50) });
|
|
231
|
+
}
|
|
232
|
+
} else if (params.type === "intent" || params.intent) {
|
|
233
|
+
const intent = params.intent ?? "";
|
|
234
|
+
const intentParams = params.params;
|
|
235
|
+
if (intent && state.sendMessage) {
|
|
236
|
+
const paramsStr = intentParams ? ` ${JSON.stringify(intentParams)}` : "";
|
|
237
|
+
state.sendMessage(
|
|
238
|
+
{
|
|
239
|
+
customType: "mcp-ui-intent",
|
|
240
|
+
content: [{ type: "text", text: `User triggered intent from ${request.serverName} UI: ${intent}${paramsStr}` }],
|
|
241
|
+
display: `🎯 UI Intent: ${intent}`,
|
|
242
|
+
details: { server: request.serverName, tool: request.toolName, intent, params: intentParams },
|
|
243
|
+
},
|
|
244
|
+
{ triggerTurn: true },
|
|
245
|
+
);
|
|
246
|
+
log.debug("Triggered agent turn for UI intent", { intent });
|
|
247
|
+
}
|
|
248
|
+
} else if (params.type === "notify" || params.message) {
|
|
249
|
+
const text = params.message ?? "";
|
|
250
|
+
if (text && state.ui) {
|
|
251
|
+
state.ui.notify(`[${request.serverName}] ${text}`, "info");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
onContextUpdate: (params: UiModelContextParams) => {
|
|
257
|
+
log.debug("Model context update from UI", {
|
|
258
|
+
hasContent: !!params.content,
|
|
259
|
+
hasStructured: !!params.structuredContent,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
onComplete: (reason: string) => {
|
|
264
|
+
active = false;
|
|
265
|
+
cleanupStreamListener();
|
|
266
|
+
|
|
267
|
+
if (state.uiServer === handle) {
|
|
268
|
+
const messages = handle.getSessionMessages();
|
|
269
|
+
const stream = handle.getStreamSummary();
|
|
270
|
+
const hasContent =
|
|
271
|
+
messages.prompts.length > 0 ||
|
|
272
|
+
messages.intents.length > 0 ||
|
|
273
|
+
messages.notifications.length > 0 ||
|
|
274
|
+
!!stream;
|
|
275
|
+
|
|
276
|
+
if (hasContent) {
|
|
277
|
+
state.completedUiSessions.push({
|
|
278
|
+
serverName: handle.serverName,
|
|
279
|
+
toolName: handle.toolName,
|
|
280
|
+
completedAt: new Date(),
|
|
281
|
+
reason,
|
|
282
|
+
messages,
|
|
283
|
+
stream,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
while (state.completedUiSessions.length > MAX_COMPLETED_SESSIONS) {
|
|
287
|
+
state.completedUiSessions.shift();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
log.debug("Session completed", {
|
|
291
|
+
reason,
|
|
292
|
+
prompts: messages.prompts.length,
|
|
293
|
+
intents: messages.intents.length,
|
|
294
|
+
notifications: messages.notifications.length,
|
|
295
|
+
streamFrames: stream?.frames ?? 0,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
state.uiServer = null;
|
|
300
|
+
if (activeGlimpseWindow) {
|
|
301
|
+
activeGlimpseWindow.close();
|
|
302
|
+
activeGlimpseWindow = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (streamToken) {
|
|
309
|
+
state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
|
|
310
|
+
if (!active || state.uiServer !== handle) return;
|
|
311
|
+
if (serverName !== request.serverName) return;
|
|
312
|
+
nextStreamSequence += 1;
|
|
313
|
+
handle.sendResultPatch(withStreamEnvelope(notification.result as CallToolResult, streamId, nextStreamSequence));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
state.uiServer = handle;
|
|
318
|
+
|
|
319
|
+
const glimpseDetected = isGlimpseAvailable();
|
|
320
|
+
const viewerPref = process.env.MCP_UI_VIEWER?.toLowerCase();
|
|
321
|
+
const useGlimpse = viewerPref === "glimpse" ||
|
|
322
|
+
(viewerPref !== "browser" && glimpseDetected);
|
|
323
|
+
|
|
324
|
+
if (useGlimpse) {
|
|
325
|
+
try {
|
|
326
|
+
const glimpseHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:0;width:100vw;height:100vh;overflow:hidden}iframe{width:100%;height:100%;border:none}</style></head><body><iframe src="${handle.url}"></iframe></body></html>`;
|
|
327
|
+
activeGlimpseWindow = await openGlimpseWindow(glimpseHtml, {
|
|
328
|
+
title: `MCP · ${request.serverName} · ${request.toolName}`,
|
|
329
|
+
width: 1000,
|
|
330
|
+
height: 800,
|
|
331
|
+
onClosed: () => {
|
|
332
|
+
if (active) handle.close("glimpse-closed");
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
log.debug("Glimpse unavailable, using browser", {
|
|
337
|
+
error: error instanceof Error ? error.message : String(error),
|
|
338
|
+
});
|
|
339
|
+
await openInBrowser(state, handle.url);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
await openInBrowser(state, handle.url);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
serverName: request.serverName,
|
|
347
|
+
toolName: request.toolName,
|
|
348
|
+
reused: false,
|
|
349
|
+
streamId,
|
|
350
|
+
streamToken,
|
|
351
|
+
streamMode,
|
|
352
|
+
requestMeta: streamToken ? { [UI_STREAM_REQUEST_META_KEY]: streamToken } : undefined,
|
|
353
|
+
url: handle.url,
|
|
354
|
+
isActive: () => active && state.uiServer === handle,
|
|
355
|
+
sendToolResult: (result: CallToolResult) => {
|
|
356
|
+
if (!active || state.uiServer !== handle) return;
|
|
357
|
+
nextStreamSequence += 1;
|
|
358
|
+
handle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
359
|
+
},
|
|
360
|
+
sendResultPatch: (result: CallToolResult) => {
|
|
361
|
+
if (!active || state.uiServer !== handle) return;
|
|
362
|
+
nextStreamSequence += 1;
|
|
363
|
+
handle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
|
|
364
|
+
},
|
|
365
|
+
sendToolCancelled: (reason: string) => {
|
|
366
|
+
if (!active || state.uiServer !== handle) return;
|
|
367
|
+
handle.sendToolCancelled(reason);
|
|
368
|
+
},
|
|
369
|
+
close: (reason?: string) => {
|
|
370
|
+
active = false;
|
|
371
|
+
cleanupStreamListener();
|
|
372
|
+
handle.close(reason);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
+
log.error("Failed to start UI session", error instanceof Error ? error : undefined);
|
|
378
|
+
state.ui?.notify(
|
|
379
|
+
`MCP UI unavailable for ${request.toolName} (${request.serverName}): ${message}`,
|
|
380
|
+
"warning",
|
|
381
|
+
);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const UI_STREAM_HOST_CONTEXT_KEY = "pi-mcp-adapter/stream";
|
|
4
|
+
export const UI_STREAM_REQUEST_META_KEY = "pi-mcp-adapter/stream-token";
|
|
5
|
+
export const UI_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-result-patch";
|
|
6
|
+
export const SERVER_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/result-patch";
|
|
7
|
+
export const UI_STREAM_STRUCTURED_CONTENT_KEY = "pi-mcp-adapter/stream";
|
|
8
|
+
|
|
9
|
+
export const uiStreamModeSchema = z.enum(["eager", "stream-first"]);
|
|
10
|
+
export type UiStreamMode = z.infer<typeof uiStreamModeSchema>;
|
|
11
|
+
|
|
12
|
+
export const visualizationStreamPhaseSchema = z.enum(["shell", "narrative", "structure", "detail", "settled"]);
|
|
13
|
+
export type VisualizationStreamPhase = z.infer<typeof visualizationStreamPhaseSchema>;
|
|
14
|
+
|
|
15
|
+
export const visualizationStreamFrameTypeSchema = z.enum(["patch", "checkpoint", "final"]);
|
|
16
|
+
export type VisualizationStreamFrameType = z.infer<typeof visualizationStreamFrameTypeSchema>;
|
|
17
|
+
|
|
18
|
+
export const visualizationStreamStatusSchema = z.enum(["ok", "error"]);
|
|
19
|
+
export type VisualizationStreamStatus = z.infer<typeof visualizationStreamStatusSchema>;
|
|
20
|
+
|
|
21
|
+
const looseRecordSchema = z.record(z.string(), z.unknown());
|
|
22
|
+
const looseArraySchema = z.array(z.unknown());
|
|
23
|
+
|
|
24
|
+
export const uiStreamHostContextSchema = z.object({
|
|
25
|
+
mode: uiStreamModeSchema,
|
|
26
|
+
streamId: z.string().min(1),
|
|
27
|
+
intermediateResultPatches: z.boolean(),
|
|
28
|
+
partialInput: z.boolean(),
|
|
29
|
+
});
|
|
30
|
+
export type UiStreamHostContext = z.infer<typeof uiStreamHostContextSchema>;
|
|
31
|
+
|
|
32
|
+
export const visualizationStreamEnvelopeSchema = z.object({
|
|
33
|
+
streamId: z.string().min(1),
|
|
34
|
+
sequence: z.number().int().nonnegative(),
|
|
35
|
+
frameType: visualizationStreamFrameTypeSchema,
|
|
36
|
+
phase: visualizationStreamPhaseSchema,
|
|
37
|
+
status: visualizationStreamStatusSchema,
|
|
38
|
+
message: z.string().optional(),
|
|
39
|
+
spec: looseRecordSchema.optional(),
|
|
40
|
+
checkpoint: looseRecordSchema.optional(),
|
|
41
|
+
});
|
|
42
|
+
export type VisualizationStreamEnvelope = z.infer<typeof visualizationStreamEnvelopeSchema>;
|
|
43
|
+
|
|
44
|
+
export const uiStreamCallToolResultSchema = z.object({
|
|
45
|
+
content: looseArraySchema.optional(),
|
|
46
|
+
structuredContent: looseRecordSchema.optional(),
|
|
47
|
+
isError: z.boolean().optional(),
|
|
48
|
+
_meta: looseRecordSchema.optional(),
|
|
49
|
+
}).passthrough();
|
|
50
|
+
export type UiStreamCallToolResult = z.infer<typeof uiStreamCallToolResultSchema>;
|
|
51
|
+
|
|
52
|
+
export const uiStreamResultPatchNotificationSchema = z.object({
|
|
53
|
+
method: z.literal(UI_STREAM_RESULT_PATCH_METHOD),
|
|
54
|
+
params: uiStreamCallToolResultSchema,
|
|
55
|
+
});
|
|
56
|
+
export type UiStreamResultPatchNotification = z.infer<typeof uiStreamResultPatchNotificationSchema>;
|
|
57
|
+
|
|
58
|
+
export const serverStreamResultPatchNotificationSchema = z.object({
|
|
59
|
+
method: z.literal(SERVER_STREAM_RESULT_PATCH_METHOD),
|
|
60
|
+
params: z.object({
|
|
61
|
+
streamToken: z.string().min(1),
|
|
62
|
+
result: uiStreamCallToolResultSchema,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
export type ServerStreamResultPatchNotification = z.infer<typeof serverStreamResultPatchNotificationSchema>;
|
|
66
|
+
|
|
67
|
+
export interface UiStreamSummary {
|
|
68
|
+
streamId: string;
|
|
69
|
+
mode: UiStreamMode;
|
|
70
|
+
frames: number;
|
|
71
|
+
phases: VisualizationStreamPhase[];
|
|
72
|
+
finalStatus?: VisualizationStreamStatus;
|
|
73
|
+
lastMessage?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getUiStreamHostContext(hostContext: Record<string, unknown> | undefined): UiStreamHostContext | undefined {
|
|
77
|
+
const candidate = hostContext?.[UI_STREAM_HOST_CONTEXT_KEY];
|
|
78
|
+
const parsed = uiStreamHostContextSchema.safeParse(candidate);
|
|
79
|
+
return parsed.success ? parsed.data : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getVisualizationStreamEnvelope(structuredContent: unknown): VisualizationStreamEnvelope | undefined {
|
|
83
|
+
if (!structuredContent || typeof structuredContent !== "object" || Array.isArray(structuredContent)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const candidate = (structuredContent as Record<string, unknown>)[UI_STREAM_STRUCTURED_CONTENT_KEY];
|
|
87
|
+
const parsed = visualizationStreamEnvelopeSchema.safeParse(candidate);
|
|
88
|
+
return parsed.success ? parsed.data : undefined;
|
|
89
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
|
|
4
|
+
export async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
|
|
5
|
+
const os = platform();
|
|
6
|
+
let result;
|
|
7
|
+
|
|
8
|
+
if (os === "darwin") {
|
|
9
|
+
result = browser ? await pi.exec("open", ["-a", browser, url]) : await pi.exec("open", [url]);
|
|
10
|
+
} else if (os === "win32") {
|
|
11
|
+
result = browser
|
|
12
|
+
? await pi.exec("cmd", ["/c", "start", "", browser, url])
|
|
13
|
+
: await pi.exec("cmd", ["/c", "start", "", url]);
|
|
14
|
+
} else {
|
|
15
|
+
result = browser ? await pi.exec(browser, [url]) : await pi.exec("xdg-open", [url]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (result.code !== 0) {
|
|
19
|
+
throw new Error(result.stderr || `Failed to open browser (exit code ${result.code})`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function parallelLimit<T, R>(
|
|
24
|
+
items: T[],
|
|
25
|
+
limit: number,
|
|
26
|
+
fn: (item: T) => Promise<R>
|
|
27
|
+
): Promise<R[]> {
|
|
28
|
+
const results: R[] = [];
|
|
29
|
+
let index = 0;
|
|
30
|
+
|
|
31
|
+
async function worker() {
|
|
32
|
+
while (index < items.length) {
|
|
33
|
+
const i = index++;
|
|
34
|
+
results[i] = await fn(items[i]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const workers = Array(Math.min(limit, items.length)).fill(null).map(() => worker());
|
|
39
|
+
await Promise.all(workers);
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getConfigPathFromArgv(): string | undefined {
|
|
44
|
+
const idx = process.argv.indexOf("--mcp-config");
|
|
45
|
+
if (idx >= 0 && idx + 1 < process.argv.length) {
|
|
46
|
+
return process.argv[idx + 1];
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function truncateAtWord(text: string, target: number): string {
|
|
52
|
+
if (!text || text.length <= target) return text;
|
|
53
|
+
|
|
54
|
+
const truncated = text.slice(0, target);
|
|
55
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
56
|
+
|
|
57
|
+
if (lastSpace > target * 0.6) {
|
|
58
|
+
return truncated.slice(0, lastSpace) + "...";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return truncated + "...";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract the adapter-owned UI stream mode from tool metadata.
|
|
66
|
+
*/
|
|
67
|
+
export function extractToolUiStreamMode(toolMeta: Record<string, unknown> | undefined): "eager" | "stream-first" | undefined {
|
|
68
|
+
const uiMeta = toolMeta?.ui;
|
|
69
|
+
if (!uiMeta || typeof uiMeta !== "object") return undefined;
|
|
70
|
+
const streamMode = (uiMeta as Record<string, unknown>)["pi-mcp-adapter.streamMode"];
|
|
71
|
+
if (streamMode === "eager" || streamMode === "stream-first") {
|
|
72
|
+
return streamMode;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|