@tt-a1i/openpi 0.4.0 → 0.6.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.
Files changed (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -0,0 +1,213 @@
1
+ import * as http2 from "node:http2";
2
+ import * as net from "node:net";
3
+ import * as tls from "node:tls";
4
+
5
+ export interface CursorHttp2ConnectOptions {
6
+ signal?: AbortSignal;
7
+ timeoutMs?: number;
8
+ }
9
+
10
+ function isLocalOrMetadataHost(hostname: string): boolean {
11
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
12
+ if (
13
+ host === "localhost" ||
14
+ host.endsWith(".localhost") ||
15
+ host === "metadata.google.internal"
16
+ ) {
17
+ return true;
18
+ }
19
+ if (host === "::" || host === "::1" || /^f[cd]/.test(host)) return true;
20
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\./.exec(host);
21
+ if (!ipv4) return false;
22
+ const first = Number(ipv4[1]);
23
+ const second = Number(ipv4[2]);
24
+ return (
25
+ first === 0 ||
26
+ first === 10 ||
27
+ first === 127 ||
28
+ (first === 169 && second === 254) ||
29
+ (first === 172 && second >= 16 && second <= 31) ||
30
+ (first === 192 && second === 168)
31
+ );
32
+ }
33
+
34
+ function shouldBypassProxy(target: URL): boolean {
35
+ if (isLocalOrMetadataHost(target.hostname)) return true;
36
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy;
37
+ if (!noProxy) return false;
38
+ const targetHost = target.hostname.toLowerCase().replace(/^\[|\]$/g, "");
39
+ const targetPort =
40
+ target.port || (target.protocol === "https:" ? "443" : "80");
41
+ for (const rawRule of noProxy.split(/[,\s]+/)) {
42
+ let rule = rawRule.trim().toLowerCase();
43
+ if (!rule) continue;
44
+ if (rule === "*") return true;
45
+ let rulePort: string | undefined;
46
+ const portMatch = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(rule);
47
+ if (portMatch) {
48
+ rule = portMatch[1]!;
49
+ rulePort = portMatch[2];
50
+ }
51
+ if (rulePort && rulePort !== targetPort) continue;
52
+ rule = rule.replace(/^\[|\]$/g, "").replace(/^\./, "");
53
+ if (targetHost === rule || targetHost.endsWith(`.${rule}`)) return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /** Resolve Cursor's provider override first, then the standard proxy variables. */
59
+ export function resolveCursorProxy(target: URL): string | undefined {
60
+ if (shouldBypassProxy(target)) return undefined;
61
+ const protocolProxy =
62
+ target.protocol === "https:"
63
+ ? process.env.HTTPS_PROXY || process.env.https_proxy
64
+ : process.env.HTTP_PROXY || process.env.http_proxy;
65
+ return [
66
+ process.env.PI_PROXY_CURSOR,
67
+ process.env.PI_PROXY,
68
+ protocolProxy,
69
+ process.env.ALL_PROXY || process.env.all_proxy,
70
+ ]
71
+ .map((value) => value?.trim())
72
+ .find((value): value is string => Boolean(value));
73
+ }
74
+
75
+ function connectProxyTunnel(
76
+ proxyUrl: URL,
77
+ targetUrl: URL,
78
+ options: CursorHttp2ConnectOptions,
79
+ ): Promise<net.Socket> {
80
+ if (!["http:", "https:"].includes(proxyUrl.protocol)) {
81
+ return Promise.reject(
82
+ new Error(`Unsupported Cursor proxy protocol: ${proxyUrl.protocol}`),
83
+ );
84
+ }
85
+ if (options.signal?.aborted) {
86
+ return Promise.reject(new Error("Cursor proxy tunnel aborted"));
87
+ }
88
+ const proxyTls = proxyUrl.protocol === "https:";
89
+ const proxyPort = Number(proxyUrl.port || (proxyTls ? 443 : 80));
90
+ const targetPort = Number(
91
+ targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
92
+ );
93
+ const targetAuthority = `${targetUrl.hostname}:${targetPort}`;
94
+ let proxyAuthorization: string | undefined;
95
+ if (proxyUrl.username || proxyUrl.password) {
96
+ try {
97
+ const credentials = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
98
+ proxyAuthorization = Buffer.from(credentials).toString("base64");
99
+ } catch (cause) {
100
+ return Promise.reject(
101
+ new Error("Cursor proxy credentials contain invalid percent-encoding", {
102
+ cause,
103
+ }),
104
+ );
105
+ }
106
+ }
107
+ const { promise, resolve, reject } = Promise.withResolvers<net.Socket>();
108
+ let rawSocket: net.Socket | undefined;
109
+ let targetSocket: net.Socket | undefined;
110
+ let timer: ReturnType<typeof setTimeout> | undefined;
111
+ let response = Buffer.alloc(0);
112
+ let settled = false;
113
+
114
+ const cleanup = () => {
115
+ if (timer) clearTimeout(timer);
116
+ options.signal?.removeEventListener("abort", onAbort);
117
+ rawSocket?.removeListener("error", onError);
118
+ rawSocket?.removeListener(proxyTls ? "secureConnect" : "connect", onReady);
119
+ rawSocket?.removeListener("data", onData);
120
+ targetSocket?.removeListener("error", onError);
121
+ targetSocket?.removeListener("secureConnect", onTargetReady);
122
+ };
123
+ const fail = (error: Error) => {
124
+ if (settled) return;
125
+ settled = true;
126
+ cleanup();
127
+ targetSocket?.destroy();
128
+ rawSocket?.destroy();
129
+ reject(error);
130
+ };
131
+ const succeed = (socket: net.Socket) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ cleanup();
135
+ resolve(socket);
136
+ };
137
+ const onAbort = () => fail(new Error("Cursor proxy tunnel aborted"));
138
+ const onError = (error: Error) => fail(error);
139
+ const onTargetReady = () => {
140
+ if (targetSocket) succeed(targetSocket);
141
+ };
142
+ const onData = (chunk: Buffer) => {
143
+ if (!rawSocket) return;
144
+ response = Buffer.concat([response, chunk]);
145
+ if (response.length > 64 * 1024) {
146
+ fail(new Error("Cursor proxy response headers exceed 64 KiB"));
147
+ return;
148
+ }
149
+ const headerEnd = response.indexOf("\r\n\r\n");
150
+ if (headerEnd === -1) return;
151
+ const statusLine = response
152
+ .subarray(0, headerEnd)
153
+ .toString("latin1")
154
+ .split("\r\n")[0];
155
+ if (!/^HTTP\/1\.[01] 200\b/.test(statusLine ?? "")) {
156
+ fail(
157
+ new Error(
158
+ `Cursor proxy tunnel failed: ${statusLine || "invalid response"}`,
159
+ ),
160
+ );
161
+ return;
162
+ }
163
+ rawSocket.removeListener("data", onData);
164
+ if (targetUrl.protocol !== "https:") {
165
+ succeed(rawSocket);
166
+ return;
167
+ }
168
+ targetSocket = tls.connect({
169
+ socket: rawSocket,
170
+ servername: targetUrl.hostname,
171
+ ALPNProtocols: ["h2"],
172
+ });
173
+ targetSocket.once("error", onError);
174
+ targetSocket.once("secureConnect", onTargetReady);
175
+ };
176
+ const onReady = () => {
177
+ if (!rawSocket) return;
178
+ let request = `CONNECT ${targetAuthority} HTTP/1.1\r\nHost: ${targetAuthority}\r\n`;
179
+ if (proxyAuthorization) {
180
+ request += `Proxy-Authorization: Basic ${proxyAuthorization}\r\n`;
181
+ }
182
+ rawSocket.on("data", onData);
183
+ rawSocket.write(`${request}\r\n`);
184
+ };
185
+
186
+ options.signal?.addEventListener("abort", onAbort, { once: true });
187
+ if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
188
+ const timeoutMs = Math.floor(options.timeoutMs);
189
+ timer = setTimeout(
190
+ () =>
191
+ fail(new Error(`Cursor proxy tunnel timed out after ${timeoutMs}ms`)),
192
+ timeoutMs,
193
+ );
194
+ }
195
+ rawSocket = proxyTls
196
+ ? tls.connect({ host: proxyUrl.hostname, port: proxyPort })
197
+ : net.connect({ host: proxyUrl.hostname, port: proxyPort });
198
+ rawSocket.once("error", onError);
199
+ rawSocket.once(proxyTls ? "secureConnect" : "connect", onReady);
200
+ return promise;
201
+ }
202
+
203
+ /** Open Cursor's HTTP/2 session directly or through an HTTP CONNECT proxy. */
204
+ export async function connectCursorHttp2(
205
+ baseUrl: string,
206
+ options: CursorHttp2ConnectOptions = {},
207
+ ): Promise<http2.ClientHttp2Session> {
208
+ const target = new URL(baseUrl);
209
+ const proxy = resolveCursorProxy(target);
210
+ if (!proxy) return http2.connect(target);
211
+ const socket = await connectProxyTunnel(new URL(proxy), target, options);
212
+ return http2.connect(target, { createConnection: () => socket });
213
+ }
@@ -0,0 +1,12 @@
1
+ /** Node >=22 provides Promise.withResolvers; the repo's ES2022 lib needs a shim. */
2
+ declare global {
3
+ interface PromiseConstructor {
4
+ withResolvers<T>(): {
5
+ promise: Promise<T>;
6
+ resolve: (value: T | PromiseLike<T>) => void;
7
+ reject: (reason?: unknown) => void;
8
+ };
9
+ }
10
+ }
11
+
12
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ai-providers — OAuth-backed model providers for pi.
3
+ *
4
+ * Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
5
+ * inert until the user logs in and selects one of their models. Cursor uses
6
+ * AgentService/Run in deliberately chat-only mode: Cursor-native coding tools
7
+ * are not exposed or executed by this extension.
8
+ *
9
+ * Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
10
+ * SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
11
+ * google-gemini-cli provider (shared google-gemini-cli/google-antigravity).
12
+ */
13
+
14
+ import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { createOAuthAuth } from "./oauth-adapter.ts";
17
+ import { encodeApiKey } from "./antigravity/credentials.ts";
18
+ import { fetchAntigravityModels } from "./antigravity/discovery.ts";
19
+ import {
20
+ ANTIGRAVITY_API_URL,
21
+ ANTIGRAVITY_MODELS,
22
+ } from "./antigravity/models.ts";
23
+ import {
24
+ loginAntigravity,
25
+ refreshAntigravityToken,
26
+ } from "./antigravity/oauth.ts";
27
+ import { streamAntigravity } from "./antigravity/provider.ts";
28
+ import { getCursorApiKey } from "./cursor/credentials.ts";
29
+ import { fetchCursorModels } from "./cursor/discovery.ts";
30
+ import { transformCursorImageInput } from "./cursor/input-images.ts";
31
+ import { CURSOR_MODELS } from "./cursor/models.ts";
32
+ import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
33
+ import { streamCursor } from "./cursor/provider.ts";
34
+
35
+ function providerStreams(
36
+ streamSimple: ProviderStreams["streamSimple"],
37
+ ): ProviderStreams {
38
+ return {
39
+ stream: (model, context, options) => streamSimple(model, context, options),
40
+ streamSimple,
41
+ };
42
+ }
43
+
44
+ export default function authProviders(pi: ExtensionAPI) {
45
+ pi.on("input", transformCursorImageInput);
46
+
47
+ pi.registerProvider(
48
+ createProvider({
49
+ id: "google-antigravity",
50
+ name: "Google Antigravity",
51
+ baseUrl: ANTIGRAVITY_API_URL,
52
+ api: providerStreams(streamAntigravity),
53
+ auth: {
54
+ oauth: createOAuthAuth({
55
+ name: "Google (Antigravity)",
56
+ isSubscription: true,
57
+ login: loginAntigravity,
58
+ refreshToken: refreshAntigravityToken,
59
+ getApiKey: encodeApiKey,
60
+ }),
61
+ },
62
+ models: ANTIGRAVITY_MODELS,
63
+ fetchModels: fetchAntigravityModels,
64
+ }),
65
+ );
66
+
67
+ pi.registerProvider(
68
+ createProvider({
69
+ id: "cursor",
70
+ name: "Cursor",
71
+ baseUrl: "https://api2.cursor.sh",
72
+ api: providerStreams(streamCursor),
73
+ auth: {
74
+ oauth: createOAuthAuth({
75
+ name: "Cursor",
76
+ isSubscription: true,
77
+ login: loginCursor,
78
+ refreshToken: refreshCursorToken,
79
+ getApiKey: getCursorApiKey,
80
+ }),
81
+ },
82
+ models: CURSOR_MODELS,
83
+ fetchModels: fetchCursorModels,
84
+ }),
85
+ );
86
+ }
@@ -0,0 +1,81 @@
1
+ import type {
2
+ ModelAuth,
3
+ OAuthAuth,
4
+ OAuthCredential,
5
+ OAuthCredentials,
6
+ OAuthLoginCallbacks,
7
+ ProviderAuthInteraction,
8
+ } from "@earendil-works/pi-ai";
9
+
10
+ interface LegacyOAuthImplementation {
11
+ name: string;
12
+ isSubscription?: boolean;
13
+ login(callbacks: CancellableOAuthLoginCallbacks): Promise<OAuthCredentials>;
14
+ refreshToken(
15
+ credential: OAuthCredentials,
16
+ signal: AbortSignal,
17
+ ): Promise<OAuthCredentials>;
18
+ getApiKey(credential: OAuthCredentials): string | Promise<string>;
19
+ }
20
+
21
+ export type CancellableOAuthLoginCallbacks = Omit<
22
+ OAuthLoginCallbacks,
23
+ "onManualCodeInput"
24
+ > & {
25
+ onManualCodeInput?(signal?: AbortSignal): Promise<string>;
26
+ };
27
+
28
+ function legacyCallbacks(
29
+ interaction: ProviderAuthInteraction,
30
+ ): CancellableOAuthLoginCallbacks {
31
+ return {
32
+ signal: interaction.signal,
33
+ onAuth: (info) => interaction.notify({ type: "auth_url", ...info }),
34
+ onDeviceCode: (info) =>
35
+ interaction.notify({ type: "device_code", ...info }),
36
+ onProgress: (message) => interaction.notify({ type: "progress", message }),
37
+ onPrompt: (prompt) =>
38
+ interaction.prompt({
39
+ type: "text",
40
+ message: prompt.message,
41
+ placeholder: prompt.placeholder,
42
+ }),
43
+ onManualCodeInput: (signal) =>
44
+ interaction.prompt({
45
+ type: "manual_code",
46
+ message: "Paste the authorization callback URL or code",
47
+ signal,
48
+ }),
49
+ onSelect: (prompt) =>
50
+ interaction.prompt({
51
+ type: "select",
52
+ message: prompt.message,
53
+ options: prompt.options,
54
+ }),
55
+ };
56
+ }
57
+
58
+ function canonicalCredential(credentials: OAuthCredentials): OAuthCredential {
59
+ return { ...credentials, type: "oauth" };
60
+ }
61
+
62
+ /** Adapt pi's retained extension OAuth callbacks to the native Provider API. */
63
+ export function createOAuthAuth(
64
+ implementation: LegacyOAuthImplementation,
65
+ ): OAuthAuth {
66
+ return {
67
+ name: implementation.name,
68
+ isSubscription: implementation.isSubscription,
69
+ login: async (interaction) =>
70
+ canonicalCredential(
71
+ await implementation.login(legacyCallbacks(interaction)),
72
+ ),
73
+ refresh: async (credential, signal) =>
74
+ canonicalCredential(
75
+ await implementation.refreshToken(credential, signal),
76
+ ),
77
+ toAuth: async (credential): Promise<ModelAuth> => ({
78
+ apiKey: await implementation.getApiKey(credential),
79
+ }),
80
+ };
81
+ }
@@ -0,0 +1,10 @@
1
+ export function emptyUsage() {
2
+ return {
3
+ input: 0,
4
+ output: 0,
5
+ cacheRead: 0,
6
+ cacheWrite: 0,
7
+ totalTokens: 0,
8
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9
+ };
10
+ }
@@ -32,6 +32,11 @@ import {
32
32
  OPENPI_TOOL_SURFACE,
33
33
  patchOwnedTools,
34
34
  } from "../shared/tool-surface.ts";
35
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
36
+ import {
37
+ projectBackgroundTerminalCapability,
38
+ registerWebCapability,
39
+ } from "../shared/web-observer-registry.ts";
35
40
  import type { TerminalSnapshot } from "./src/domain.ts";
36
41
  import {
37
42
  MAX_RUNNING,
@@ -95,11 +100,18 @@ interface WatchToolDetails {
95
100
  export default function (pi: ExtensionAPI) {
96
101
  let runtime: TerminalRuntime | undefined;
97
102
  let managerPromise: Promise<TerminalManagerShape> | undefined;
103
+ let managerForHooks: TerminalManagerShape | undefined;
104
+ let unregisterWebCapability: (() => void) | undefined;
98
105
  let sessionContext: ExtensionContext | undefined;
99
106
  let ui: ExtensionUIContext | undefined;
100
107
  let unsubStatus: (() => void) | undefined;
101
108
  let startReservations = 0;
102
- const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>();
109
+ const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>({
110
+ owner: () =>
111
+ sessionContext
112
+ ? completionOwnerFor(sessionContext.sessionManager)
113
+ : undefined,
114
+ });
103
115
  const hideLifecycleTools = () =>
104
116
  patchOwnedTools(pi, "background", {
105
117
  disable: OPENPI_TOOL_SURFACE.background.deferred,
@@ -115,9 +127,21 @@ export default function (pi: ExtensionAPI) {
115
127
 
116
128
  /** Resolve the manager service once per runtime and wire the extension hooks. */
117
129
  const getManager = () => {
130
+ const scope = sessionContext?.sessionManager;
118
131
  managerPromise ??= getRuntime()
119
132
  .runPromise(TerminalManager)
120
133
  .then((manager) => {
134
+ managerForHooks = manager;
135
+ unregisterWebCapability?.();
136
+ unregisterWebCapability =
137
+ scope && sessionContext?.sessionManager === scope
138
+ ? registerWebCapability(scope, {
139
+ kind: "background-terminals",
140
+ snapshot: () =>
141
+ projectBackgroundTerminalCapability(manager.view.list()),
142
+ subscribe: (listener) => manager.view.subscribe(listener),
143
+ })
144
+ : undefined;
121
145
  manager.view.setOnSettled(onSettled);
122
146
  unsubStatus?.();
123
147
  unsubStatus = manager.view.subscribe(() => updateWidget(manager));
@@ -214,6 +238,7 @@ export default function (pi: ExtensionAPI) {
214
238
  // costing a turn.
215
239
  resultDeliveryOptions(wake),
216
240
  );
241
+ for (const snap of snaps) managerForHooks?.view.releaseResult(snap.id);
217
242
  return true;
218
243
  } catch (error) {
219
244
  // Session may be shutting down, but retain the snapshot so any later
@@ -226,6 +251,7 @@ export default function (pi: ExtensionAPI) {
226
251
  const flushResults = (wake: boolean) => {
227
252
  const snaps = resultDelivery.drain(MAX_RUNNING);
228
253
  if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
254
+ else resultDelivery.acknowledge(snaps);
229
255
  };
230
256
 
231
257
  const idleResultBatcher = createIdleResultBatcher({
@@ -253,6 +279,7 @@ export default function (pi: ExtensionAPI) {
253
279
  stdout: { ...snap.stdout },
254
280
  stderr: { ...snap.stderr },
255
281
  });
282
+ managerForHooks?.view.retainResult(snap.id);
256
283
  // Pending settlements remain retractable while busy, so bg_status/bg_kill
257
284
  // can consume them before they are committed to context. bg_start applies
258
285
  // backpressure across running + pending + reserved work, keeping this map
@@ -310,7 +337,10 @@ export default function (pi: ExtensionAPI) {
310
337
  ui = undefined;
311
338
  const closing = runtime;
312
339
  runtime = undefined;
340
+ unregisterWebCapability?.();
341
+ unregisterWebCapability = undefined;
313
342
  managerPromise = undefined;
343
+ managerForHooks = undefined;
314
344
  await closing?.dispose();
315
345
  });
316
346
 
@@ -423,12 +453,16 @@ export default function (pi: ExtensionAPI) {
423
453
  );
424
454
  }
425
455
 
456
+ const text = buildStatusResult(snap);
426
457
  // This status is returning the settlement itself; a pending automatic
427
458
  // follow-up for the same settle would be a duplicate.
428
- if (snap.status !== "running") resultDelivery.consume([snap.id]);
459
+ if (snap.status !== "running") {
460
+ resultDelivery.consume([snap.id]);
461
+ manager.view.releaseResult(snap.id);
462
+ }
429
463
 
430
464
  return {
431
- content: [{ type: "text", text: buildStatusResult(snap) }],
465
+ content: [{ type: "text", text }],
432
466
  details: {
433
467
  id: snap.id,
434
468
  status: snap.status,
@@ -510,6 +544,7 @@ export default function (pi: ExtensionAPI) {
510
544
  // via the killInterest consumed flag). Remove any deferred automatic
511
545
  // delivery now that this tool returns the final state itself.
512
546
  resultDelivery.consume(ids);
547
+ for (const id of ids) manager.view.releaseResult(id);
513
548
 
514
549
  return {
515
550
  content: [{ type: "text", text: buildKillReport(report) }],
@@ -23,6 +23,8 @@ export type TerminalStatus =
23
23
  export interface OutputView {
24
24
  /** Decoded, possibly head-trimmed text (bounded by the in-memory cap). */
25
25
  readonly text: string;
26
+ /** Stateful, bounded projection safe for model-facing output. */
27
+ readonly modelSafeText: string;
26
28
  /** True total bytes ever received on this stream. */
27
29
  readonly totalBytes: number;
28
30
  /** Bytes dropped from the head of the in-memory view (0 = complete). */