@tt-a1i/openpi 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/SETUP.md +2 -2
- package/THIRD_PARTY_NOTICES.md +242 -0
- package/extensions/ai-providers/README.md +12 -6
- package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
- package/extensions/ai-providers/cursor/input-images.ts +2 -3
- package/extensions/ai-providers/cursor/proto.ts +218 -11
- package/extensions/ai-providers/cursor/protobuf.ts +12 -2
- package/extensions/ai-providers/cursor/provider.ts +280 -37
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/index.ts +3 -3
- package/extensions/file-mutation-display/index.ts +13 -9
- package/extensions/shared/agent-transcript.ts +3 -2
- package/extensions/shared/child-session.ts +14 -0
- package/extensions/subagents/index.ts +20 -3
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +70 -59
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/manager.ts +2 -71
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/web/index.ts +69 -5
- package/extensions/workflows/artifacts.ts +362 -23
- package/extensions/workflows/dashboard.ts +141 -21
- package/extensions/workflows/index.ts +62 -20
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/runner.ts +5 -162
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +28 -4
- package/skills/subagents/REFERENCE.md +6 -7
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +3 -1
- package/web/dist/app.js +87 -0
- package/web/dist/favicon.svg +9 -0
- package/web/dist/index.html +15 -0
- package/web/dist/styles.css +3 -0
- package/web/host/web-host.ts +6 -9
- package/web/ui/index.html +3 -131
- package/web/ui/public/favicon.svg +9 -0
- package/web/ui/src/app/App.tsx +134 -0
- package/web/ui/src/app/providers.tsx +38 -0
- package/web/ui/src/components/Markdown.tsx +58 -0
- package/web/ui/src/components/OpenPiLogo.tsx +41 -0
- package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
- package/web/ui/src/features/composer/Composer.tsx +237 -0
- package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
- package/web/ui/src/features/transcript/Transcript.tsx +860 -0
- package/web/ui/src/i18n.ts +159 -0
- package/web/ui/src/lib/format.ts +57 -0
- package/web/ui/src/main.tsx +16 -0
- package/web/ui/src/protocol/client.ts +199 -0
- package/web/ui/src/protocol/event-stream.ts +88 -0
- package/web/ui/src/store/web-store.ts +926 -0
- package/web/ui/src/styles.css +420 -0
- package/web/ui/tsconfig.json +12 -0
- package/web/ui/vite-env.d.ts +1 -0
- package/web/vite.config.mjs +21 -1
- package/web/host/static-assets.ts +0 -4
- package/web/ui/app.js +0 -1700
- package/web/ui/styles.css +0 -680
|
@@ -10,9 +10,11 @@ import type {
|
|
|
10
10
|
Model,
|
|
11
11
|
SimpleStreamOptions,
|
|
12
12
|
TextContent,
|
|
13
|
+
ToolCall,
|
|
13
14
|
} from "@earendil-works/pi-ai/compat";
|
|
14
15
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/compat";
|
|
15
16
|
import { emptyUsage } from "../usage.ts";
|
|
17
|
+
import { ConnectFrameReader } from "./connect-frame-reader.ts";
|
|
16
18
|
import {
|
|
17
19
|
CURSOR_API_URL,
|
|
18
20
|
CURSOR_CLIENT_VERSION,
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
CursorRuleSchema,
|
|
36
38
|
CursorRuleTypeGlobalSchema,
|
|
37
39
|
CursorRuleTypeSchema,
|
|
40
|
+
CursorToolCallSchema,
|
|
38
41
|
ExecClientControlMessageSchema,
|
|
39
42
|
ExecClientMessageSchema,
|
|
40
43
|
ExecClientStreamCloseSchema,
|
|
@@ -44,6 +47,15 @@ import {
|
|
|
44
47
|
KvClientMessageSchema,
|
|
45
48
|
type KvServerMessage,
|
|
46
49
|
KvServerMessageSchema,
|
|
50
|
+
McpArgsSchema,
|
|
51
|
+
McpImageContentSchema,
|
|
52
|
+
McpRejectedSchema,
|
|
53
|
+
McpResultSchema,
|
|
54
|
+
McpSuccessSchema,
|
|
55
|
+
McpTextContentSchema,
|
|
56
|
+
McpToolCallSchema,
|
|
57
|
+
McpToolResultContentItemSchema,
|
|
58
|
+
McpToolResultSchema,
|
|
47
59
|
type ModelDetails,
|
|
48
60
|
ModelDetailsSchema,
|
|
49
61
|
RequestContextResultSchema,
|
|
@@ -59,8 +71,14 @@ import {
|
|
|
59
71
|
UserMessageActionSchema,
|
|
60
72
|
UserMessageSchema,
|
|
61
73
|
} from "./proto.ts";
|
|
62
|
-
import { create, fromBinary, toBinary } from "./protobuf.ts";
|
|
74
|
+
import { create, encodeJsonValue, fromBinary, toBinary } from "./protobuf.ts";
|
|
63
75
|
import { connectCursorHttp2 } from "./proxy.ts";
|
|
76
|
+
import {
|
|
77
|
+
buildCursorTools,
|
|
78
|
+
CURSOR_PI_PROVIDER,
|
|
79
|
+
CURSOR_PI_TOOLS_SYSTEM_PROMPT,
|
|
80
|
+
decodeCursorTool,
|
|
81
|
+
} from "./tool-bridge.ts";
|
|
64
82
|
|
|
65
83
|
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
66
84
|
const CONNECT_COMPRESSED_FLAG = 0b00000001;
|
|
@@ -199,22 +217,51 @@ function rootPromptContent(
|
|
|
199
217
|
|
|
200
218
|
function assistantRootContent(
|
|
201
219
|
message: Extract<Message, { role: "assistant" }>,
|
|
220
|
+
results: Map<string, Extract<Message, { role: "toolResult" }>>,
|
|
202
221
|
) {
|
|
203
222
|
const content: Array<Record<string, unknown>> = [];
|
|
204
223
|
for (const item of message.content) {
|
|
205
224
|
if (item.type === "text" && item.text) {
|
|
206
225
|
content.push({ type: "text", text: item.text });
|
|
226
|
+
} else if (
|
|
227
|
+
item.type === "toolCall" &&
|
|
228
|
+
results.get(item.id)?.toolName === item.name
|
|
229
|
+
) {
|
|
230
|
+
content.push({
|
|
231
|
+
type: "tool-call",
|
|
232
|
+
toolCallId: item.id,
|
|
233
|
+
toolName: item.name,
|
|
234
|
+
args: item.arguments,
|
|
235
|
+
});
|
|
207
236
|
}
|
|
208
237
|
}
|
|
209
238
|
return content;
|
|
210
239
|
}
|
|
211
240
|
|
|
241
|
+
function pairedToolResults(messages: Message[], end: number) {
|
|
242
|
+
const calls = new Map<string, string>();
|
|
243
|
+
const results = new Map<string, Extract<Message, { role: "toolResult" }>>();
|
|
244
|
+
for (const message of messages.slice(0, end < 0 ? undefined : end)) {
|
|
245
|
+
if (message.role === "assistant") {
|
|
246
|
+
for (const part of message.content)
|
|
247
|
+
if (part.type === "toolCall") calls.set(part.id, part.name);
|
|
248
|
+
} else if (
|
|
249
|
+
message.role === "toolResult" &&
|
|
250
|
+
calls.get(message.toolCallId) === message.toolName
|
|
251
|
+
) {
|
|
252
|
+
results.set(message.toolCallId, message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
|
|
212
258
|
function buildHistoryRootPrompt(
|
|
213
259
|
messages: Message[],
|
|
214
260
|
store: CursorBlobStore,
|
|
215
261
|
activeUserIndex: number,
|
|
216
262
|
): Uint8Array[] {
|
|
217
263
|
const entries: Uint8Array[] = [];
|
|
264
|
+
const results = pairedToolResults(messages, activeUserIndex);
|
|
218
265
|
for (let index = 0; index < messages.length; index++) {
|
|
219
266
|
if (index === activeUserIndex) break;
|
|
220
267
|
const message = messages[index];
|
|
@@ -224,13 +271,29 @@ function buildHistoryRootPrompt(
|
|
|
224
271
|
if (content.length === 0) continue;
|
|
225
272
|
value = { role: "user", content };
|
|
226
273
|
} else if (message.role === "assistant") {
|
|
227
|
-
const content = assistantRootContent(message);
|
|
274
|
+
const content = assistantRootContent(message, results);
|
|
228
275
|
if (content.length === 0) continue;
|
|
229
276
|
value = { role: "assistant", content };
|
|
230
277
|
} else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
if (results.get(message.toolCallId) !== message) continue;
|
|
279
|
+
value = {
|
|
280
|
+
role: "tool",
|
|
281
|
+
id: message.toolCallId,
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "tool-result",
|
|
285
|
+
toolCallId: message.toolCallId,
|
|
286
|
+
toolName: message.toolName,
|
|
287
|
+
result: message.content.some((part) => part.type === "image")
|
|
288
|
+
? rootPromptContent(message.content)
|
|
289
|
+
: message.content
|
|
290
|
+
.filter((part) => part.type === "text")
|
|
291
|
+
.map((part) => part.text)
|
|
292
|
+
.join("\n"),
|
|
293
|
+
...(message.isError ? { isError: true } : {}),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
234
297
|
}
|
|
235
298
|
entries.push(
|
|
236
299
|
storeBlob(store, new TextEncoder().encode(JSON.stringify(value))),
|
|
@@ -242,13 +305,17 @@ function buildHistoryRootPrompt(
|
|
|
242
305
|
function buildSystemPrompt(
|
|
243
306
|
systemPrompt: Context["systemPrompt"],
|
|
244
307
|
store: CursorBlobStore,
|
|
308
|
+
hasTools = false,
|
|
245
309
|
): Uint8Array[] {
|
|
246
310
|
const prompts = systemPrompt
|
|
247
311
|
? Array.isArray(systemPrompt)
|
|
248
312
|
? systemPrompt
|
|
249
313
|
: [systemPrompt]
|
|
250
314
|
: ["You are a helpful assistant."];
|
|
251
|
-
return [
|
|
315
|
+
return [
|
|
316
|
+
...prompts,
|
|
317
|
+
hasTools ? CURSOR_PI_TOOLS_SYSTEM_PROMPT : CURSOR_CHAT_ONLY_SYSTEM_PROMPT,
|
|
318
|
+
].map((prompt) =>
|
|
252
319
|
storeBlob(
|
|
253
320
|
store,
|
|
254
321
|
new TextEncoder().encode(
|
|
@@ -260,11 +327,12 @@ function buildSystemPrompt(
|
|
|
260
327
|
|
|
261
328
|
/**
|
|
262
329
|
* Cursor asks for these rules over the exec channel before generating text.
|
|
263
|
-
*
|
|
264
|
-
*
|
|
330
|
+
* These rules keep Cursor-native tools disabled. The request-context response
|
|
331
|
+
* advertises only the active Pi tools through the MCP protocol bridge.
|
|
265
332
|
*/
|
|
266
333
|
export function buildCursorRequestContextRules(
|
|
267
334
|
systemPrompt: Context["systemPrompt"],
|
|
335
|
+
hasTools = false,
|
|
268
336
|
): CursorRule[] {
|
|
269
337
|
const rules: CursorRule[] = systemPrompt?.trim()
|
|
270
338
|
? [
|
|
@@ -283,8 +351,10 @@ export function buildCursorRequestContextRules(
|
|
|
283
351
|
: [];
|
|
284
352
|
rules.push(
|
|
285
353
|
create(CursorRuleSchema, {
|
|
286
|
-
fullPath: "/pi/cursor-chat-only.mdc",
|
|
287
|
-
content:
|
|
354
|
+
fullPath: hasTools ? "/pi/cursor-tools.mdc" : "/pi/cursor-chat-only.mdc",
|
|
355
|
+
content: hasTools
|
|
356
|
+
? CURSOR_PI_TOOLS_SYSTEM_PROMPT
|
|
357
|
+
: CURSOR_CHAT_ONLY_SYSTEM_PROMPT,
|
|
288
358
|
source: 2,
|
|
289
359
|
type: create(CursorRuleTypeSchema, {
|
|
290
360
|
type: { case: "global", value: create(CursorRuleTypeGlobalSchema, {}) },
|
|
@@ -300,6 +370,7 @@ function buildHistoryTurns(
|
|
|
300
370
|
activeUserIndex: number,
|
|
301
371
|
): Uint8Array[] {
|
|
302
372
|
const turns: Uint8Array[] = [];
|
|
373
|
+
const results = pairedToolResults(messages, activeUserIndex);
|
|
303
374
|
const end = activeUserIndex >= 0 ? activeUserIndex : messages.length;
|
|
304
375
|
let index = 0;
|
|
305
376
|
while (index < end) {
|
|
@@ -335,6 +406,68 @@ function buildHistoryTurns(
|
|
|
335
406
|
),
|
|
336
407
|
),
|
|
337
408
|
);
|
|
409
|
+
} else if (item.type === "toolCall") {
|
|
410
|
+
const result = results.get(item.id);
|
|
411
|
+
if (!result || result.toolName !== item.name) continue;
|
|
412
|
+
const args = Object.fromEntries(
|
|
413
|
+
Object.entries(item.arguments).map(([key, value]) => [
|
|
414
|
+
key,
|
|
415
|
+
encodeJsonValue(JSON.parse(JSON.stringify(value))),
|
|
416
|
+
]),
|
|
417
|
+
);
|
|
418
|
+
const tool = create(CursorToolCallSchema, {
|
|
419
|
+
toolCallId: item.id,
|
|
420
|
+
tool: {
|
|
421
|
+
case: "mcpToolCall",
|
|
422
|
+
value: create(McpToolCallSchema, {
|
|
423
|
+
args: create(McpArgsSchema, {
|
|
424
|
+
name: item.name,
|
|
425
|
+
toolName: item.name,
|
|
426
|
+
providerIdentifier: CURSOR_PI_PROVIDER,
|
|
427
|
+
toolCallId: item.id,
|
|
428
|
+
args,
|
|
429
|
+
}),
|
|
430
|
+
result: create(McpToolResultSchema, {
|
|
431
|
+
result: {
|
|
432
|
+
case: "success",
|
|
433
|
+
value: create(McpSuccessSchema, {
|
|
434
|
+
isError: result.isError,
|
|
435
|
+
content: result.content.map((part) =>
|
|
436
|
+
create(McpToolResultContentItemSchema, {
|
|
437
|
+
content:
|
|
438
|
+
part.type === "text"
|
|
439
|
+
? {
|
|
440
|
+
case: "text",
|
|
441
|
+
value: create(McpTextContentSchema, {
|
|
442
|
+
text: part.text,
|
|
443
|
+
}),
|
|
444
|
+
}
|
|
445
|
+
: {
|
|
446
|
+
case: "image",
|
|
447
|
+
value: create(McpImageContentSchema, {
|
|
448
|
+
data: Buffer.from(part.data, "base64"),
|
|
449
|
+
mimeType: part.mimeType,
|
|
450
|
+
}),
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
),
|
|
454
|
+
}),
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
steps.push(
|
|
461
|
+
storeBlob(
|
|
462
|
+
store,
|
|
463
|
+
toBinary(
|
|
464
|
+
ConversationStepSchema,
|
|
465
|
+
create(ConversationStepSchema, {
|
|
466
|
+
message: { case: "toolCall", value: tool },
|
|
467
|
+
}),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
);
|
|
338
471
|
}
|
|
339
472
|
}
|
|
340
473
|
}
|
|
@@ -414,11 +547,14 @@ export async function buildCursorRequest(
|
|
|
414
547
|
options?: SimpleStreamOptions,
|
|
415
548
|
): Promise<CursorRequestBuild> {
|
|
416
549
|
const store: CursorBlobStore = new Map();
|
|
417
|
-
const activeIndex =
|
|
550
|
+
const activeIndex =
|
|
551
|
+
context.messages.at(-1)?.role === "user"
|
|
552
|
+
? lastUserIndex(context.messages)
|
|
553
|
+
: -1;
|
|
418
554
|
const active = activeIndex >= 0 ? context.messages[activeIndex] : undefined;
|
|
419
555
|
const activeContent = active?.role === "user" ? active.content : undefined;
|
|
420
556
|
const rootPromptMessagesJson = [
|
|
421
|
-
...buildSystemPrompt(context.systemPrompt, store),
|
|
557
|
+
...buildSystemPrompt(context.systemPrompt, store, !!context.tools?.length),
|
|
422
558
|
...buildHistoryRootPrompt(context.messages, store, activeIndex),
|
|
423
559
|
];
|
|
424
560
|
const state = create(ConversationStateStructureSchema, {
|
|
@@ -554,7 +690,7 @@ function isAbortError(
|
|
|
554
690
|
);
|
|
555
691
|
}
|
|
556
692
|
|
|
557
|
-
/** Cursor AgentService/Run
|
|
693
|
+
/** Cursor AgentService/Run with Pi-owned tool execution across provider turns. */
|
|
558
694
|
export function streamCursor(
|
|
559
695
|
model: Model<Api>,
|
|
560
696
|
context: Context,
|
|
@@ -586,6 +722,8 @@ export function streamCursor(
|
|
|
586
722
|
let turnEnded = false;
|
|
587
723
|
let terminalError: Error | undefined;
|
|
588
724
|
let finished = false;
|
|
725
|
+
const pendingCalls = new Map<string, ToolCall>();
|
|
726
|
+
let handingOffTools = false;
|
|
589
727
|
|
|
590
728
|
const closeBlocks = () => {
|
|
591
729
|
if (currentText) {
|
|
@@ -696,8 +834,9 @@ export function streamCursor(
|
|
|
696
834
|
h2Request?.close(http2.constants.NGHTTP2_CANCEL);
|
|
697
835
|
}, requestTimeoutMs);
|
|
698
836
|
};
|
|
699
|
-
|
|
837
|
+
const frames = new ConnectFrameReader(MAX_CONNECT_FRAME_BYTES);
|
|
700
838
|
const processFrame = (flags: number, bytes: Uint8Array) => {
|
|
839
|
+
if (handingOffTools) return;
|
|
701
840
|
if ((flags & CONNECT_COMPRESSED_FLAG) !== 0) {
|
|
702
841
|
throw new Error("Compressed Cursor Connect frames are unsupported");
|
|
703
842
|
}
|
|
@@ -715,8 +854,11 @@ export function streamCursor(
|
|
|
715
854
|
case: "success",
|
|
716
855
|
value: create(RequestContextSuccessSchema, {
|
|
717
856
|
requestContext: create(RequestContextSchema, {
|
|
718
|
-
rules: buildCursorRequestContextRules(
|
|
719
|
-
|
|
857
|
+
rules: buildCursorRequestContextRules(
|
|
858
|
+
context.systemPrompt,
|
|
859
|
+
!!context.tools?.length,
|
|
860
|
+
),
|
|
861
|
+
tools: buildCursorTools(context.tools),
|
|
720
862
|
}),
|
|
721
863
|
}),
|
|
722
864
|
},
|
|
@@ -734,6 +876,60 @@ export function streamCursor(
|
|
|
734
876
|
);
|
|
735
877
|
return;
|
|
736
878
|
}
|
|
879
|
+
if (exec.message.case === "mcpArgs") {
|
|
880
|
+
const args = exec.message.value;
|
|
881
|
+
if (args.smartModeApprovalOnly) {
|
|
882
|
+
// A probe must not become a tool call or preauthorize Pi execution.
|
|
883
|
+
const reply = create(AgentClientMessageSchema, {
|
|
884
|
+
message: {
|
|
885
|
+
case: "execClientMessage",
|
|
886
|
+
value: create(ExecClientMessageSchema, {
|
|
887
|
+
id: exec.id,
|
|
888
|
+
execId: exec.execId,
|
|
889
|
+
message: {
|
|
890
|
+
case: "mcpResult",
|
|
891
|
+
value: create(McpResultSchema, {
|
|
892
|
+
result: {
|
|
893
|
+
case: "rejected",
|
|
894
|
+
value: create(McpRejectedSchema, {
|
|
895
|
+
reason:
|
|
896
|
+
"Pi must evaluate permissions when executing the tool; approval-only probes cannot authorize execution.",
|
|
897
|
+
}),
|
|
898
|
+
},
|
|
899
|
+
}),
|
|
900
|
+
},
|
|
901
|
+
}),
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
h2Request?.write(
|
|
905
|
+
frameConnectMessage(toBinary(AgentClientMessageSchema, reply)),
|
|
906
|
+
);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const call = decodeCursorTool(args, context.tools);
|
|
910
|
+
if (
|
|
911
|
+
context.messages.some((message) =>
|
|
912
|
+
message.role === "toolResult"
|
|
913
|
+
? message.toolCallId === call.id
|
|
914
|
+
: message.role === "assistant" &&
|
|
915
|
+
message.content.some(
|
|
916
|
+
(part) => part.type === "toolCall" && part.id === call.id,
|
|
917
|
+
),
|
|
918
|
+
)
|
|
919
|
+
) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
"Cursor attempted to replay a tool call identity already present in Pi history",
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const previous = pendingCalls.get(call.id);
|
|
925
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(call)) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
"Cursor repeated a tool call identity with different arguments",
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
pendingCalls.set(call.id, call);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
737
933
|
const throwReply = create(AgentClientMessageSchema, {
|
|
738
934
|
message: {
|
|
739
935
|
case: "execClientControlMessage",
|
|
@@ -742,8 +938,9 @@ export function streamCursor(
|
|
|
742
938
|
case: "throw",
|
|
743
939
|
value: create(ExecClientThrowSchema, {
|
|
744
940
|
id: exec.id,
|
|
745
|
-
error:
|
|
746
|
-
"Cursor
|
|
941
|
+
error: context.tools?.length
|
|
942
|
+
? "Cursor-native execution is unavailable; use advertised Pi MCP tools"
|
|
943
|
+
: "Cursor tools are not available in this chat-only provider",
|
|
747
944
|
errorCode: "UNIMPLEMENTED",
|
|
748
945
|
}),
|
|
749
946
|
},
|
|
@@ -762,7 +959,9 @@ export function streamCursor(
|
|
|
762
959
|
},
|
|
763
960
|
});
|
|
764
961
|
const error = new Error(
|
|
765
|
-
|
|
962
|
+
context.tools?.length
|
|
963
|
+
? "Cursor requested unsupported native execution outside Pi"
|
|
964
|
+
: "Cursor requested a tool that is unavailable in chat-only mode",
|
|
766
965
|
);
|
|
767
966
|
terminalError = error;
|
|
768
967
|
if (!h2Request) {
|
|
@@ -784,10 +983,32 @@ export function streamCursor(
|
|
|
784
983
|
}
|
|
785
984
|
if (message.message.case === "interactionQuery") {
|
|
786
985
|
throw new Error(
|
|
787
|
-
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable in chat-only mode`,
|
|
986
|
+
`Cursor interaction query ${message.message.value.query.case ?? "unknown"} is unavailable ${context.tools?.length ? "outside Pi's interaction lifecycle" : "in chat-only mode"}`,
|
|
788
987
|
);
|
|
789
988
|
}
|
|
790
989
|
if (message.message.case !== "interactionUpdate") return;
|
|
990
|
+
const update = message.message.value;
|
|
991
|
+
if (
|
|
992
|
+
context.tools?.length &&
|
|
993
|
+
(update.message.case === "partialToolCall" ||
|
|
994
|
+
update.message.case === "toolCallStarted" ||
|
|
995
|
+
update.message.case === "toolCallCompleted")
|
|
996
|
+
) {
|
|
997
|
+
const preview = update.message.value.toolCall;
|
|
998
|
+
if (preview && preview.tool.case !== "mcpToolCall") {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
"Cursor-native tools are unavailable; use the advertised Pi MCP tools",
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
// Only exec mcpArgs is an invocation. UI previews may be partial,
|
|
1004
|
+
// duplicated, or emitted for approval probes, and never execute.
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (context.tools?.length && update.message.case === "toolCallDelta") {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
"Cursor-native tool deltas are unavailable; use the advertised Pi MCP tools",
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
791
1012
|
processInteraction(
|
|
792
1013
|
message.message.value,
|
|
793
1014
|
output,
|
|
@@ -813,22 +1034,39 @@ export function streamCursor(
|
|
|
813
1034
|
);
|
|
814
1035
|
};
|
|
815
1036
|
const processData = (chunk: Buffer) => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1037
|
+
frames.push(chunk, processFrame);
|
|
1038
|
+
// Preserve tool batching while a complete header awaits its body.
|
|
1039
|
+
if (frames.awaitingPayload) return;
|
|
1040
|
+
if (pendingCalls.size > 0 && !handingOffTools) {
|
|
1041
|
+
if (terminalError) throw terminalError;
|
|
1042
|
+
if (options?.signal?.aborted)
|
|
1043
|
+
throw new Error("Cursor request aborted");
|
|
1044
|
+
closeBlocks();
|
|
1045
|
+
for (const call of pendingCalls.values()) {
|
|
1046
|
+
const contentIndex = output.content.length;
|
|
1047
|
+
output.content.push(call);
|
|
1048
|
+
stream.push({
|
|
1049
|
+
type: "toolcall_start",
|
|
1050
|
+
contentIndex,
|
|
1051
|
+
partial: output,
|
|
1052
|
+
});
|
|
1053
|
+
stream.push({
|
|
1054
|
+
type: "toolcall_delta",
|
|
1055
|
+
contentIndex,
|
|
1056
|
+
delta: JSON.stringify(call.arguments),
|
|
1057
|
+
partial: output,
|
|
1058
|
+
});
|
|
1059
|
+
stream.push({
|
|
1060
|
+
type: "toolcall_end",
|
|
1061
|
+
contentIndex,
|
|
1062
|
+
toolCall: call,
|
|
1063
|
+
partial: output,
|
|
1064
|
+
});
|
|
826
1065
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
processFrame(flags, data);
|
|
1066
|
+
output.stopReason = "toolUse";
|
|
1067
|
+
handingOffTools = true;
|
|
1068
|
+
turnEnded = true;
|
|
1069
|
+
settle();
|
|
832
1070
|
}
|
|
833
1071
|
};
|
|
834
1072
|
|
|
@@ -907,7 +1145,7 @@ export function streamCursor(
|
|
|
907
1145
|
.then(() => {
|
|
908
1146
|
if (!responseSeen)
|
|
909
1147
|
throw new Error("Cursor response headers were not received");
|
|
910
|
-
if (
|
|
1148
|
+
if (frames.incomplete)
|
|
911
1149
|
throw new Error("Incomplete Cursor Connect frame");
|
|
912
1150
|
settle();
|
|
913
1151
|
})
|
|
@@ -967,7 +1205,12 @@ export function streamCursor(
|
|
|
967
1205
|
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
968
1206
|
stream.push({
|
|
969
1207
|
type: "done",
|
|
970
|
-
reason:
|
|
1208
|
+
reason:
|
|
1209
|
+
output.stopReason === "toolUse"
|
|
1210
|
+
? "toolUse"
|
|
1211
|
+
: output.stopReason === "length"
|
|
1212
|
+
? "length"
|
|
1213
|
+
: "stop",
|
|
971
1214
|
message: output,
|
|
972
1215
|
});
|
|
973
1216
|
stream.end();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Translate Cursor MCP requests into Pi calls; execution stays in Pi's loop. */
|
|
2
|
+
import type { Context, ToolCall } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { type McpArgs, McpToolDefinitionSchema } from "./proto.ts";
|
|
4
|
+
import {
|
|
5
|
+
create,
|
|
6
|
+
decodeJsonValue,
|
|
7
|
+
encodeJsonValue,
|
|
8
|
+
type JsonValue,
|
|
9
|
+
} from "./protobuf.ts";
|
|
10
|
+
|
|
11
|
+
export const CURSOR_PI_PROVIDER = "openpi";
|
|
12
|
+
export const CURSOR_PI_TOOLS_SYSTEM_PROMPT =
|
|
13
|
+
"Use only the provided openpi MCP tools. These are the active Pi tools and Pi owns their execution and permissions. Do not use Cursor-native filesystem, shell, editing, web, task, or interaction tools. When tool results appear in conversation history, continue from those results. Do not repeat a completed tool call.";
|
|
14
|
+
|
|
15
|
+
export function buildCursorTools(tools: Context["tools"]) {
|
|
16
|
+
return (tools ?? []).map((tool) => {
|
|
17
|
+
const schema: JsonValue = JSON.parse(JSON.stringify(tool.parameters));
|
|
18
|
+
return create(McpToolDefinitionSchema, {
|
|
19
|
+
name: tool.name,
|
|
20
|
+
providerIdentifier: CURSOR_PI_PROVIDER,
|
|
21
|
+
toolName: tool.name,
|
|
22
|
+
description: tool.description,
|
|
23
|
+
inputSchema: encodeJsonValue(schema),
|
|
24
|
+
inputSchemaJson: JSON.stringify(schema),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeCursorTool(
|
|
30
|
+
args: McpArgs,
|
|
31
|
+
tools: Context["tools"],
|
|
32
|
+
): ToolCall {
|
|
33
|
+
const name = args.toolName || args.name;
|
|
34
|
+
if (
|
|
35
|
+
args.providerIdentifier !== CURSOR_PI_PROVIDER ||
|
|
36
|
+
(args.serverIdentifier && args.serverIdentifier !== CURSOR_PI_PROVIDER) ||
|
|
37
|
+
!name ||
|
|
38
|
+
(args.name && args.name !== name) ||
|
|
39
|
+
!tools?.some((tool) => tool.name === name)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("Cursor requested an unadvertised Pi tool identity");
|
|
42
|
+
}
|
|
43
|
+
if (!args.toolCallId.trim())
|
|
44
|
+
throw new Error("Cursor MCP tool call has no identity");
|
|
45
|
+
const values: Record<string, unknown> = {};
|
|
46
|
+
for (const [key, value] of Object.entries(args.args)) {
|
|
47
|
+
// google.protobuf.Value, not JSON text. Define own properties so keys such
|
|
48
|
+
// as __proto__ cannot alter the decoded argument object's prototype.
|
|
49
|
+
if (!value.length || ![8, 17, 26, 32, 42, 50].includes(value[0]!)) {
|
|
50
|
+
throw new Error("Cursor MCP argument is not a protobuf JSON value");
|
|
51
|
+
}
|
|
52
|
+
const decoded = decodeJsonValue(value);
|
|
53
|
+
const validateJson = (item: JsonValue): void => {
|
|
54
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
55
|
+
throw new Error("Cursor MCP argument contains a non-finite number");
|
|
56
|
+
if (item && typeof item === "object")
|
|
57
|
+
for (const child of Object.values(item)) validateJson(child);
|
|
58
|
+
};
|
|
59
|
+
validateJson(decoded);
|
|
60
|
+
Object.defineProperty(values, key, {
|
|
61
|
+
value: decoded,
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { type: "toolCall", id: args.toolCallId, name, arguments: values };
|
|
68
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
|
|
5
5
|
* inert until the user logs in and selects one of their models. Cursor uses
|
|
6
|
-
* AgentService/Run
|
|
7
|
-
* are not exposed or executed by this extension.
|
|
6
|
+
* AgentService/Run with an experimental bridge to normal Pi tool calls.
|
|
7
|
+
* Cursor-native coding tools are not exposed or executed by this extension.
|
|
8
8
|
*
|
|
9
9
|
* Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
|
|
10
10
|
* SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
17
16
|
import { encodeApiKey } from "./antigravity/credentials.ts";
|
|
18
17
|
import { fetchAntigravityModels } from "./antigravity/discovery.ts";
|
|
19
18
|
import {
|
|
@@ -31,6 +30,7 @@ import { transformCursorImageInput } from "./cursor/input-images.ts";
|
|
|
31
30
|
import { CURSOR_MODELS } from "./cursor/models.ts";
|
|
32
31
|
import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
|
|
33
32
|
import { streamCursor } from "./cursor/provider.ts";
|
|
33
|
+
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
34
34
|
|
|
35
35
|
function providerStreams(
|
|
36
36
|
streamSimple: ProviderStreams["streamSimple"],
|
|
@@ -27,15 +27,19 @@ function compact<TParams extends TSchema, TDetails, TState>(
|
|
|
27
27
|
export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
28
28
|
pi.on("session_start", (_event, ctx) => {
|
|
29
29
|
const display = loadSetupConfig().ui;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
// This extension changes only the interactive TUI projection. Headless
|
|
31
|
+
// sessions must keep Pi's native definitions, especially bash: replacing
|
|
32
|
+
// it here would drop the SettingsManager-provided shellPath and can make
|
|
33
|
+
// Windows resolve the WSL System32 stub instead of the configured shell.
|
|
34
|
+
if (ctx.mode !== "tui") return;
|
|
35
|
+
|
|
36
|
+
// Ctrl+O remains a temporary override. A new/reloaded session starts from
|
|
37
|
+
// the persisted defaults instead of inheriting an old expanded toggle.
|
|
38
|
+
ctx.ui.setToolsExpanded(
|
|
39
|
+
display.subagentResultDisplay === "full" &&
|
|
40
|
+
display.bashToolDisplay === "full" &&
|
|
41
|
+
display.fileMutationDisplay === "full",
|
|
42
|
+
);
|
|
39
43
|
|
|
40
44
|
pi.registerTool(
|
|
41
45
|
compact(
|
|
@@ -409,7 +409,7 @@ function findResult(
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
/**
|
|
412
|
-
* Caches finalized transcript items by identity and
|
|
412
|
+
* Caches finalized transcript items by identity, width, and cwd. Live state remains
|
|
413
413
|
* uncached because it changes on every stream tick; callers clear this cache
|
|
414
414
|
* from their component's invalidate() when Pi changes theme.
|
|
415
415
|
*/
|
|
@@ -434,7 +434,8 @@ export class AgentTranscriptRenderer {
|
|
|
434
434
|
for (let index = 0; index < document.items.length; index++) {
|
|
435
435
|
const item = document.items[index];
|
|
436
436
|
const context = itemContext(document.items, index, liveIds, pairing);
|
|
437
|
-
|
|
437
|
+
// Fallback tool rows relativize paths against the document's cwd.
|
|
438
|
+
const key = JSON.stringify([width, context.token, document.cwd]);
|
|
438
439
|
const cacheable = !document.toolRenderer || !itemHasTool(item);
|
|
439
440
|
const cached = cacheable ? this.itemCache.get(item)?.get(key) : undefined;
|
|
440
441
|
const lines =
|
|
@@ -550,6 +550,20 @@ export function effectiveChildToolAllowlist(tools?: readonly string[]) {
|
|
|
550
550
|
);
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
/** Project the parent's active surface into a child; a role can only narrow it.
|
|
554
|
+
* Active tools are a visibility choice, not a filesystem/network sandbox.
|
|
555
|
+
* Inactive tools are not implicitly activated by delegation.
|
|
556
|
+
*/
|
|
557
|
+
export function inheritedChildToolAllowlist(
|
|
558
|
+
parentTools: readonly string[],
|
|
559
|
+
roleTools?: readonly string[],
|
|
560
|
+
) {
|
|
561
|
+
const allowed = roleTools === undefined ? undefined : new Set(roleTools);
|
|
562
|
+
return effectiveChildToolAllowlist([...new Set(parentTools)])!.filter(
|
|
563
|
+
(name) => allowed === undefined || allowed.has(name),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
553
567
|
export function childToolPolicy(tools?: readonly string[]) {
|
|
554
568
|
const effectiveTools = effectiveChildToolAllowlist(tools);
|
|
555
569
|
return {
|