@tt-a1i/openpi 0.3.1 → 0.5.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 (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -1,4 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { resolve } from "node:path";
2
4
  import type { SessionInfoLike } from "./sessions.ts";
3
5
 
4
6
  export interface SessionStats {
@@ -7,8 +9,15 @@ export interface SessionStats {
7
9
  del: number;
8
10
  }
9
11
 
12
+ export type SessionStatsState =
13
+ | { status: "pending" }
14
+ | { status: "ready"; stats: SessionStats }
15
+ | { status: "unavailable" };
16
+
17
+ type CachedSessionStats = Exclude<SessionStatsState, { status: "pending" }>;
18
+
10
19
  interface StatsLoaderOptions {
11
- cache?: Map<string, SessionStats>;
20
+ cache?: Map<string, CachedSessionStats>;
12
21
  maxCache?: number;
13
22
  maxConcurrency?: number;
14
23
  runGit?: (
@@ -16,6 +25,8 @@ interface StatsLoaderOptions {
16
25
  cwd: string,
17
26
  signal: AbortSignal,
18
27
  ) => Promise<string>;
28
+ canonicalizeCwd?: (cwd: string) => Promise<string>;
29
+ canonicalizeConcurrency?: number;
19
30
  }
20
31
 
21
32
  interface QueueEntry {
@@ -27,6 +38,7 @@ interface QueueEntry {
27
38
  const GIT_STATS_TIMEOUT_MS = 3_000;
28
39
  const DEFAULT_MAX_CACHE = 256;
29
40
  const DEFAULT_MAX_CONCURRENCY = 4;
41
+ const DEFAULT_CANONICALIZE_CONCURRENCY = 8;
30
42
 
31
43
  const abortError = () => {
32
44
  const error = new Error("Session stats load was cancelled.");
@@ -97,7 +109,7 @@ function createQueue(maxConcurrency: number) {
97
109
  }
98
110
 
99
111
  const runGit = (args: string[], cwd: string, signal: AbortSignal) =>
100
- new Promise<string>((resolve) => {
112
+ new Promise<string>((resolveOutput, reject) => {
101
113
  try {
102
114
  execFile(
103
115
  "git",
@@ -108,10 +120,13 @@ const runGit = (args: string[], cwd: string, signal: AbortSignal) =>
108
120
  timeout: GIT_STATS_TIMEOUT_MS,
109
121
  signal,
110
122
  },
111
- (error, stdout) => resolve(error ? "" : stdout),
123
+ (error, stdout) => {
124
+ if (error) reject(error);
125
+ else resolveOutput(stdout);
126
+ },
112
127
  );
113
- } catch {
114
- resolve("");
128
+ } catch (error) {
129
+ reject(error);
115
130
  }
116
131
  });
117
132
 
@@ -133,94 +148,265 @@ const calculateStats = (outputs: readonly string[]) => {
133
148
  };
134
149
 
135
150
  export function createSessionStatsLoader(options: StatsLoaderOptions = {}) {
136
- const cache = options.cache ?? new Map<string, SessionStats>();
151
+ const cache = options.cache ?? new Map<string, CachedSessionStats>();
137
152
  const maxCache = options.maxCache ?? DEFAULT_MAX_CACHE;
138
153
  const execute = options.runGit ?? runGit;
154
+ const canonicalize = options.canonicalizeCwd ?? realpath;
155
+ const canonicalizeConcurrency = Math.max(
156
+ 1,
157
+ options.canonicalizeConcurrency ?? DEFAULT_CANONICALIZE_CONCURRENCY,
158
+ );
139
159
  const queue = createQueue(
140
160
  Math.max(1, options.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY),
141
161
  );
142
- const loading = new Map<string, AbortSignal>();
162
+ interface Request {
163
+ key: string;
164
+ cwd: string;
165
+ after: string;
166
+ before: string;
167
+ includeWorktreeDiff: boolean;
168
+ }
169
+ interface ActiveRequest {
170
+ controller: AbortController;
171
+ promise: Promise<void>;
172
+ }
143
173
 
144
- const loadOne = async (
145
- session: SessionInfoLike,
146
- isLatestForCwd: boolean,
174
+ const active = new Map<string, ActiveRequest>();
175
+ const pathKeys = new Map<string, string>();
176
+ const canonicalCwds = new Map<string, Promise<string>>();
177
+ let canonicalUniverse:
178
+ | {
179
+ universe: readonly SessionInfoLike[];
180
+ controller: AbortController;
181
+ promise: Promise<Map<string, string>>;
182
+ }
183
+ | undefined;
184
+ let requestedPaths = new Set<string>();
185
+ let reconcileGeneration = 0;
186
+ let notify = () => {};
187
+
188
+ const canonicalCwd = (cwd: string) => {
189
+ const absolute = resolve(cwd);
190
+ let pending = canonicalCwds.get(absolute);
191
+ if (!pending) {
192
+ pending = canonicalize(absolute).catch(() => absolute);
193
+ canonicalCwds.set(absolute, pending);
194
+ }
195
+ return pending;
196
+ };
197
+
198
+ const buildCanonicalWorkspaceMap = async (
199
+ sessions: readonly SessionInfoLike[],
147
200
  signal: AbortSignal,
148
- onUpdate: () => void,
149
201
  ) => {
150
- const key = session.path;
151
- if (cache.has(key)) return;
152
- const currentLoad = loading.get(key);
153
- if (currentLoad && !currentLoad.aborted) return;
154
- loading.set(key, signal);
202
+ const workspaces = [
203
+ ...new Set(sessions.map((session) => resolve(session.cwd))),
204
+ ];
205
+ const result = new Map<string, string>();
206
+ let index = 0;
207
+ const worker = async () => {
208
+ while (!signal.aborted) {
209
+ const cwd = workspaces[index++];
210
+ if (!cwd) return;
211
+ result.set(cwd, await canonicalCwd(cwd));
212
+ }
213
+ };
214
+ await Promise.all(
215
+ Array.from(
216
+ { length: Math.min(canonicalizeConcurrency, workspaces.length) },
217
+ worker,
218
+ ),
219
+ );
220
+ if (signal.aborted) throw abortError();
221
+ return result;
222
+ };
223
+
224
+ const canonicalWorkspaceMap = (sessions: readonly SessionInfoLike[]) => {
225
+ if (
226
+ canonicalUniverse?.universe === sessions &&
227
+ !canonicalUniverse.controller.signal.aborted
228
+ ) {
229
+ return canonicalUniverse.promise;
230
+ }
231
+ canonicalUniverse?.controller.abort();
232
+ const controller = new AbortController();
233
+ const promise = buildCanonicalWorkspaceMap(sessions, controller.signal);
234
+ canonicalUniverse = { universe: sessions, controller, promise };
235
+ return promise;
236
+ };
237
+
238
+ const latestPathsByCwd = (
239
+ sessions: readonly SessionInfoLike[],
240
+ canonical: ReadonlyMap<string, string>,
241
+ ) => {
242
+ const latest = new Map<string, SessionInfoLike>();
243
+ for (const session of sessions) {
244
+ const cwd = canonical.get(resolve(session.cwd)) ?? resolve(session.cwd);
245
+ const current = latest.get(cwd);
246
+ if (!current || session.modified.getTime() > current.modified.getTime()) {
247
+ latest.set(cwd, session);
248
+ }
249
+ }
250
+ return new Map(
251
+ [...latest].map(([cwd, session]) => [cwd, session.path] as const),
252
+ );
253
+ };
155
254
 
255
+ const requestFor = (
256
+ session: SessionInfoLike,
257
+ latest: ReadonlyMap<string, string>,
258
+ canonical: ReadonlyMap<string, string>,
259
+ ): Request => {
260
+ const cwd = canonical.get(resolve(session.cwd)) ?? resolve(session.cwd);
156
261
  const after = session.created
157
262
  ? session.created.toISOString()
158
263
  : new Date(session.modified.getTime() - 24 * 3600 * 1000).toISOString();
159
264
  const before = session.modified.toISOString();
265
+ const includeWorktreeDiff = latest.get(cwd) === session.path;
266
+ return {
267
+ key: JSON.stringify([cwd, after, before, includeWorktreeDiff]),
268
+ cwd,
269
+ after,
270
+ before,
271
+ includeWorktreeDiff,
272
+ };
273
+ };
160
274
 
161
- try {
162
- const outputs = await Promise.all([
163
- queue
164
- .run(
275
+ const putCache = (key: string, value: CachedSessionStats) => {
276
+ if (cache.has(key)) cache.delete(key);
277
+ while (cache.size >= Math.max(1, maxCache)) {
278
+ const oldest = cache.keys().next().value;
279
+ if (oldest === undefined) break;
280
+ cache.delete(oldest);
281
+ }
282
+ cache.set(key, value);
283
+ };
284
+
285
+ const start = (request: Request) => {
286
+ const controller = new AbortController();
287
+ const promise = Promise.all([
288
+ queue.run(
289
+ () =>
290
+ execute(
291
+ [
292
+ "log",
293
+ `--after=${request.after}`,
294
+ `--before=${request.before}`,
295
+ "--numstat",
296
+ "--pretty=format:",
297
+ ],
298
+ request.cwd,
299
+ controller.signal,
300
+ ),
301
+ controller.signal,
302
+ ),
303
+ request.includeWorktreeDiff
304
+ ? queue.run(
165
305
  () =>
166
- execute(
167
- [
168
- "log",
169
- `--after=${after}`,
170
- `--before=${before}`,
171
- "--numstat",
172
- "--pretty=format:",
173
- ],
174
- session.cwd,
175
- signal,
176
- ),
177
- signal,
306
+ execute(["diff", "--numstat"], request.cwd, controller.signal),
307
+ controller.signal,
178
308
  )
179
- .catch(() => ""),
180
- isLatestForCwd
181
- ? queue
182
- .run(
183
- () => execute(["diff", "--numstat"], session.cwd, signal),
184
- signal,
185
- )
186
- .catch(() => "")
187
- : Promise.resolve(""),
188
- ]);
189
- if (signal.aborted || loading.get(key) !== signal) return;
190
-
191
- if (cache.size >= maxCache) {
192
- const oldest = cache.keys().next().value;
193
- if (oldest !== undefined) cache.delete(oldest);
194
- }
195
- cache.set(key, calculateStats(outputs));
196
- onUpdate();
197
- } finally {
198
- if (loading.get(key) === signal) loading.delete(key);
199
- }
309
+ : Promise.resolve(""),
310
+ ])
311
+ .then((outputs) => {
312
+ if (!controller.signal.aborted) {
313
+ putCache(request.key, {
314
+ status: "ready",
315
+ stats: calculateStats(outputs),
316
+ });
317
+ notify();
318
+ }
319
+ })
320
+ .catch(() => {
321
+ const cancelled = controller.signal.aborted;
322
+ if (!cancelled) {
323
+ putCache(request.key, { status: "unavailable" });
324
+ notify();
325
+ controller.abort();
326
+ }
327
+ })
328
+ .finally(() => {
329
+ if (active.get(request.key)?.controller === controller) {
330
+ active.delete(request.key);
331
+ }
332
+ });
333
+ const entry = { controller, promise };
334
+ active.set(request.key, entry);
335
+ return entry;
200
336
  };
201
337
 
202
- const load = (
203
- sessions: SessionInfoLike[],
204
- signal: AbortSignal,
338
+ const cancelKey = (key: string) => {
339
+ const entry = active.get(key);
340
+ if (!entry) return;
341
+ active.delete(key);
342
+ entry.controller.abort();
343
+ };
344
+
345
+ const reconcile = async (
346
+ targets: readonly SessionInfoLike[],
347
+ universe: readonly SessionInfoLike[],
205
348
  onUpdate: () => void,
206
349
  ) => {
207
- const latestByCwd = new Map<string, string>();
208
- for (const session of sessions) {
209
- if (!latestByCwd.has(session.cwd)) {
210
- latestByCwd.set(session.cwd, session.path);
211
- }
350
+ notify = onUpdate;
351
+ const generation = ++reconcileGeneration;
352
+ requestedPaths = new Set(targets.map((session) => session.path));
353
+ if (targets.length === 0) {
354
+ for (const key of [...active.keys()]) cancelKey(key);
355
+ pathKeys.clear();
356
+ return;
212
357
  }
213
- return Promise.all(
214
- sessions.map((session) =>
215
- loadOne(
216
- session,
217
- latestByCwd.get(session.cwd) === session.path,
218
- signal,
219
- onUpdate,
220
- ),
221
- ),
222
- ).then(() => undefined);
358
+
359
+ let canonical: ReadonlyMap<string, string>;
360
+ try {
361
+ canonical = await canonicalWorkspaceMap(universe);
362
+ } catch {
363
+ return;
364
+ }
365
+ if (generation !== reconcileGeneration) return;
366
+ const latest = latestPathsByCwd(universe, canonical);
367
+ const requests = targets.map((session) => ({
368
+ session,
369
+ request: requestFor(session, latest, canonical),
370
+ }));
371
+ const wanted = new Set(requests.map(({ request }) => request.key));
372
+
373
+ for (const key of active.keys()) {
374
+ if (!wanted.has(key)) cancelKey(key);
375
+ }
376
+
377
+ pathKeys.clear();
378
+ const pending: Promise<void>[] = [];
379
+ for (const { session, request } of requests) {
380
+ pathKeys.set(session.path, request.key);
381
+ if (cache.has(request.key)) continue;
382
+ const current = active.get(request.key) ?? start(request);
383
+ pending.push(current.promise);
384
+ }
385
+ await Promise.all(pending);
386
+ };
387
+
388
+ const get = (session: SessionInfoLike): SessionStatsState | undefined => {
389
+ if (!requestedPaths.has(session.path)) return undefined;
390
+ const key = pathKeys.get(session.path);
391
+ if (!key) return { status: "pending" };
392
+ const cached = cache.get(key);
393
+ if (cached) {
394
+ cache.delete(key);
395
+ cache.set(key, cached);
396
+ return cached;
397
+ }
398
+ if (active.has(key)) return { status: "pending" };
399
+ return undefined;
400
+ };
401
+
402
+ const cancel = () => {
403
+ reconcileGeneration++;
404
+ canonicalUniverse?.controller.abort();
405
+ canonicalUniverse = undefined;
406
+ for (const key of [...active.keys()]) cancelKey(key);
407
+ pathKeys.clear();
408
+ requestedPaths.clear();
223
409
  };
224
410
 
225
- return { cache, load };
411
+ return { cache, get, reconcile, cancel };
226
412
  }