@oh-my-pi/pi-coding-agent 16.0.2 → 16.0.3

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 (88) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +0 -1
  3. package/dist/cli.js +217 -276
  4. package/dist/types/advisor/advise-tool.d.ts +30 -1
  5. package/dist/types/commands/install.d.ts +1 -1
  6. package/dist/types/config/model-resolver.d.ts +8 -0
  7. package/dist/types/config/settings-schema.d.ts +0 -10
  8. package/dist/types/eval/js/shared/runtime.d.ts +1 -0
  9. package/dist/types/eval/js/worker-core.d.ts +1 -0
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/goals/runtime.d.ts +0 -1
  12. package/dist/types/mcp/tool-bridge.d.ts +3 -0
  13. package/dist/types/modes/components/custom-editor.d.ts +14 -4
  14. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  15. package/dist/types/modes/interactive-mode.d.ts +1 -1
  16. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
  17. package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
  18. package/dist/types/modes/types.d.ts +1 -1
  19. package/dist/types/registry/agent-lifecycle.d.ts +16 -1
  20. package/dist/types/sdk.d.ts +8 -0
  21. package/dist/types/session/agent-session.d.ts +20 -8
  22. package/dist/types/session/session-dump-format.d.ts +8 -2
  23. package/dist/types/session/session-entries.d.ts +4 -0
  24. package/dist/types/session/session-history-format.d.ts +2 -0
  25. package/dist/types/session/session-manager.d.ts +22 -0
  26. package/dist/types/stt/downloader.d.ts +5 -5
  27. package/dist/types/task/executor.d.ts +6 -0
  28. package/dist/types/task/persisted-revive.d.ts +36 -0
  29. package/dist/types/tiny/models.d.ts +8 -0
  30. package/dist/types/tools/builtin-names.d.ts +1 -1
  31. package/dist/types/tools/index.d.ts +0 -1
  32. package/package.json +12 -12
  33. package/src/advisor/__tests__/advisor.test.ts +150 -50
  34. package/src/advisor/advise-tool.ts +48 -6
  35. package/src/advisor/runtime.ts +10 -3
  36. package/src/auto-thinking/classifier.ts +12 -3
  37. package/src/cli.ts +2 -2
  38. package/src/commands/install.ts +3 -3
  39. package/src/config/model-resolver.ts +28 -11
  40. package/src/config/settings-schema.ts +0 -11
  41. package/src/eval/agent-bridge.ts +2 -0
  42. package/src/eval/js/context-manager.ts +2 -1
  43. package/src/eval/js/shared/runtime.ts +189 -15
  44. package/src/eval/js/worker-core.ts +19 -0
  45. package/src/export/html/index.ts +1 -1
  46. package/src/export/html/tool-views.generated.js +34 -35
  47. package/src/extensibility/extensions/loader.ts +21 -9
  48. package/src/goals/runtime.ts +1 -23
  49. package/src/internal-urls/docs-index.generated.ts +4 -6
  50. package/src/main.ts +20 -0
  51. package/src/mcp/render.ts +11 -1
  52. package/src/mcp/tool-bridge.ts +3 -0
  53. package/src/modes/components/custom-editor.test.ts +63 -18
  54. package/src/modes/components/custom-editor.ts +63 -15
  55. package/src/modes/controllers/command-controller.ts +2 -2
  56. package/src/modes/controllers/input-controller.ts +15 -9
  57. package/src/modes/controllers/selector-controller.ts +13 -8
  58. package/src/modes/controllers/tan-command-controller.ts +1 -0
  59. package/src/modes/interactive-mode.ts +4 -2
  60. package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
  61. package/src/modes/theme/mermaid-cache.ts +74 -11
  62. package/src/modes/theme/theme.ts +14 -1
  63. package/src/modes/types.ts +1 -1
  64. package/src/prompts/system/system-prompt.md +2 -1
  65. package/src/registry/agent-lifecycle.ts +60 -8
  66. package/src/sdk.ts +20 -26
  67. package/src/session/agent-session.ts +246 -78
  68. package/src/session/artifacts.ts +19 -1
  69. package/src/session/session-dump-format.ts +167 -23
  70. package/src/session/session-entries.ts +4 -0
  71. package/src/session/session-history-format.ts +37 -3
  72. package/src/session/session-manager.ts +94 -4
  73. package/src/slash-commands/builtin-registry.ts +4 -7
  74. package/src/stt/asr-client.ts +6 -0
  75. package/src/stt/downloader.ts +13 -6
  76. package/src/stt/stt-controller.ts +52 -11
  77. package/src/task/executor.ts +18 -2
  78. package/src/task/index.ts +2 -2
  79. package/src/task/persisted-revive.ts +128 -0
  80. package/src/tiny/models.ts +10 -0
  81. package/src/tiny/worker.ts +4 -3
  82. package/src/tools/builtin-names.ts +0 -1
  83. package/src/tools/index.ts +0 -4
  84. package/src/tools/output-meta.ts +17 -3
  85. package/src/utils/title-generator.ts +4 -4
  86. package/dist/types/tools/render-mermaid.d.ts +0 -38
  87. package/src/prompts/tools/render-mermaid.md +0 -9
  88. package/src/tools/render-mermaid.ts +0 -69
@@ -55,6 +55,32 @@ export interface RuntimeOptions {
55
55
  const BASE64_STRICT_RE = /^[A-Za-z0-9+/]+={0,2}$/;
56
56
  const DECIMAL_CSV_RE = /^\d{1,3}(?:,\d{1,3})*$/;
57
57
 
58
+ const PRELUDE_GLOBAL_KEYS = [
59
+ "__omp_js_prelude_loaded__",
60
+ "console",
61
+ "print",
62
+ "display",
63
+ "tool",
64
+ "completion",
65
+ "output",
66
+ "agent",
67
+ "parallel",
68
+ "pipeline",
69
+ "log",
70
+ "phase",
71
+ "budget",
72
+ "__pool",
73
+ "read",
74
+ "write",
75
+ "append",
76
+ "sort",
77
+ "uniq",
78
+ "counter",
79
+ "diff",
80
+ "tree",
81
+ "env",
82
+ ];
83
+
58
84
  function isStrictBase64(s: string): boolean {
59
85
  if (s.length === 0 || s.length % 4 !== 0) return false;
60
86
  return BASE64_STRICT_RE.test(s);
@@ -125,6 +151,22 @@ function describeDataType(data: unknown): string {
125
151
  * concern.
126
152
  */
127
153
  export class JsRuntime {
154
+ #globalOwner = Symbol("JsRuntime globals");
155
+ #ownedGlobalKeys = new Set<string>();
156
+ #disposed = false;
157
+ #runHookResolver = () => this.#als.getStore()?.hooks;
158
+
159
+ #ownGlobal(key: string): void {
160
+ if (this.#ownedGlobalKeys.has(key)) return;
161
+ claimGlobalKey(key, this.#globalOwner);
162
+ this.#ownedGlobalKeys.add(key);
163
+ }
164
+
165
+ #activateGlobals(action: string): void {
166
+ if (this.#disposed) throw new Error(`Cannot ${action} on a disposed JS runtime`);
167
+ activateGlobalOwner(this.#globalOwner, this.#ownedGlobalKeys, action);
168
+ }
169
+
128
170
  readonly helpers: HelperBundle;
129
171
  #cwd: string;
130
172
  readonly sessionId: string;
@@ -153,6 +195,7 @@ export class JsRuntime {
153
195
  }
154
196
 
155
197
  setCwd(cwd: string): void {
198
+ this.#activateGlobals("set cwd");
156
199
  this.#cwd = cwd;
157
200
  const session = (globalThis as { __omp_session__?: { cwd?: string } }).__omp_session__;
158
201
  if (session) session.cwd = cwd;
@@ -164,6 +207,7 @@ export class JsRuntime {
164
207
  * cleanup it wants.
165
208
  */
166
209
  setRunScope(scope: Record<string, unknown>): void {
210
+ this.#activateGlobals("set run scope");
167
211
  Object.assign(globalThis, scope);
168
212
  }
169
213
 
@@ -173,6 +217,8 @@ export class JsRuntime {
173
217
  hooks: RuntimeHooks,
174
218
  options: { runId?: string; cwd?: string } = {},
175
219
  ): Promise<unknown> {
220
+ this.#activateGlobals("run code");
221
+ const leaveRun = enterGlobalRun(this.#globalOwner, "run code");
176
222
  const context: RunContext = {
177
223
  runId: options.runId ?? crypto.randomUUID(),
178
224
  hooks,
@@ -180,21 +226,25 @@ export class JsRuntime {
180
226
  finalExpressionSet: false,
181
227
  finalExpressionValue: undefined,
182
228
  };
183
- return await this.#als.run(context, async () => {
184
- const wrapped = await wrapCode(code);
185
- const value = indirectEval(wrapped.source, filename);
186
- if (wrapped.finalExpressionReturned) {
187
- const awaited = await awaitMaybePromise(value);
188
- if (context.finalExpressionSet) {
189
- const finalValue = context.finalExpressionValue;
190
- context.finalExpressionSet = false;
191
- context.finalExpressionValue = undefined;
192
- return await awaitMaybePromise(finalValue);
229
+ try {
230
+ return await this.#als.run(context, async () => {
231
+ const wrapped = await wrapCode(code);
232
+ const value = indirectEval(wrapped.source, filename);
233
+ if (wrapped.finalExpressionReturned) {
234
+ const awaited = await awaitMaybePromise(value);
235
+ if (context.finalExpressionSet) {
236
+ const finalValue = context.finalExpressionValue;
237
+ context.finalExpressionSet = false;
238
+ context.finalExpressionValue = undefined;
239
+ return await awaitMaybePromise(finalValue);
240
+ }
241
+ return awaited;
193
242
  }
194
- return awaited;
195
- }
196
- return await awaitMaybePromise(value);
197
- });
243
+ return await awaitMaybePromise(value);
244
+ });
245
+ } finally {
246
+ leaveRun();
247
+ }
198
248
  }
199
249
 
200
250
  displayValue(value: unknown, hooks: RuntimeHooks | undefined = this.#als.getStore()?.hooks): void {
@@ -338,13 +388,137 @@ export class JsRuntime {
338
388
  createRequire,
339
389
  fs,
340
390
  };
391
+
392
+ const allGlobalKeys = new Set<string>([
393
+ ...Object.keys(injected),
394
+ ...Object.keys(extraGlobals ?? {}),
395
+ ...PRELUDE_GLOBAL_KEYS,
396
+ ]);
397
+
398
+ for (const key of allGlobalKeys) {
399
+ this.#ownGlobal(key);
400
+ }
401
+
341
402
  Object.assign(globalThis, injected, extraGlobals ?? {});
342
403
  // Prelude assigns console bridge + short aliases (`read`, `write`, `tool`, `display`, ...)
343
404
  // onto globalThis. Must run after helpers are in place.
344
405
  indirectEval(JAVASCRIPT_PRELUDE_SOURCE);
345
- RUN_HOOK_RESOLVERS.add(() => this.#als.getStore()?.hooks);
406
+ for (const key of allGlobalKeys) recordGlobalValue(key, this.#globalOwner);
407
+ RUN_HOOK_RESOLVERS.add(this.#runHookResolver);
346
408
  patchStdioOnce();
347
409
  }
410
+
411
+ dispose(): void {
412
+ if (this.#disposed) return;
413
+ this.#disposed = true;
414
+ RUN_HOOK_RESOLVERS.delete(this.#runHookResolver);
415
+ for (const key of this.#ownedGlobalKeys) releaseGlobalKey(key, this.#globalOwner);
416
+ this.#ownedGlobalKeys.clear();
417
+ }
418
+ }
419
+
420
+ interface GlobalSnapshot {
421
+ exists: boolean;
422
+ value: unknown;
423
+ }
424
+
425
+ interface GlobalOwnerEntry {
426
+ owner: symbol;
427
+ value: unknown;
428
+ }
429
+
430
+ interface GlobalStack {
431
+ base: GlobalSnapshot;
432
+ entries: GlobalOwnerEntry[];
433
+ }
434
+
435
+ // Inline fallback and cmux tabs can create multiple JsRuntime instances in one Bun realm.
436
+ // Track reserved helper globals by owner so disposing one runtime restores the next active
437
+ // owner (or the original process global after the last owner), not a stale snapshot.
438
+ const GLOBAL_STACKS = new Map<string, GlobalStack>();
439
+
440
+ function snapshotGlobal(key: string): GlobalSnapshot {
441
+ return {
442
+ exists: key in globalThis,
443
+ value: (globalThis as Record<string, unknown>)[key],
444
+ };
445
+ }
446
+
447
+ function restoreGlobal(key: string, state: GlobalSnapshot): void {
448
+ if (state.exists) {
449
+ (globalThis as Record<string, unknown>)[key] = state.value;
450
+ } else {
451
+ delete (globalThis as Record<string, unknown>)[key];
452
+ }
453
+ }
454
+
455
+ function claimGlobalKey(key: string, owner: symbol): void {
456
+ let stack = GLOBAL_STACKS.get(key);
457
+ if (!stack) {
458
+ stack = { base: snapshotGlobal(key), entries: [] };
459
+ GLOBAL_STACKS.set(key, stack);
460
+ }
461
+ stack.entries.push({ owner, value: (globalThis as Record<string, unknown>)[key] });
462
+ }
463
+
464
+ function recordGlobalValue(key: string, owner: symbol): void {
465
+ const stack = GLOBAL_STACKS.get(key);
466
+ const entry = stack?.entries.findLast(item => item.owner === owner);
467
+ if (entry) entry.value = (globalThis as Record<string, unknown>)[key];
468
+ }
469
+
470
+ function releaseGlobalKey(key: string, owner: symbol): void {
471
+ const stack = GLOBAL_STACKS.get(key);
472
+ if (!stack) return;
473
+ const index = stack.entries.findIndex(entry => entry.owner === owner);
474
+ if (index === -1) return;
475
+ const wasTop = index === stack.entries.length - 1;
476
+ stack.entries.splice(index, 1);
477
+ if (!wasTop) return;
478
+ const next = stack.entries.at(-1);
479
+ if (next) {
480
+ (globalThis as Record<string, unknown>)[key] = next.value;
481
+ return;
482
+ }
483
+ restoreGlobal(key, stack.base);
484
+ GLOBAL_STACKS.delete(key);
485
+ }
486
+
487
+ // Plain globalThis cannot safely serve two different runtimes at the same instant:
488
+ // helpers dereference reserved globals on every call. Sequential cmux tab revisits
489
+ // re-activate their owner stack; overlapping cross-runtime runs fail explicitly.
490
+ let activeGlobalRunOwner: symbol | null = null;
491
+ let activeGlobalRunDepth = 0;
492
+
493
+ function assertCanUseGlobalOwner(owner: symbol, action: string): void {
494
+ if (activeGlobalRunOwner === null || activeGlobalRunOwner === owner) return;
495
+ throw new Error(`Cannot ${action} while another same-realm JS runtime is running`);
496
+ }
497
+
498
+ function activateGlobalOwner(owner: symbol, keys: Iterable<string>, action: string): void {
499
+ assertCanUseGlobalOwner(owner, action);
500
+ for (const key of keys) {
501
+ const stack = GLOBAL_STACKS.get(key);
502
+ const index = stack?.entries.findIndex(entry => entry.owner === owner) ?? -1;
503
+ if (!stack || index === -1) throw new Error(`Cannot ${action} on a disposed JS runtime`);
504
+ const entry = stack.entries[index];
505
+ stack.entries.splice(index, 1);
506
+ stack.entries.push(entry);
507
+ (globalThis as Record<string, unknown>)[key] = entry.value;
508
+ }
509
+ }
510
+
511
+ function enterGlobalRun(owner: symbol, action: string): () => void {
512
+ assertCanUseGlobalOwner(owner, action);
513
+ activeGlobalRunOwner = owner;
514
+ activeGlobalRunDepth++;
515
+ let left = false;
516
+ return () => {
517
+ if (left) return;
518
+ left = true;
519
+ activeGlobalRunDepth--;
520
+ if (activeGlobalRunDepth === 0) activeGlobalRunOwner = null;
521
+ };
348
522
  }
349
523
 
350
524
  /** Resolvers for each live runtime's active-run hooks (one per JsRuntime instance). */
@@ -124,9 +124,28 @@ export class WorkerCore {
124
124
  active.pendingTools.clear();
125
125
  }
126
126
  this.#runs.clear();
127
+ this.#runtime?.dispose?.();
127
128
  this.#runtime = null;
128
129
  this.#transport.send({ type: "closed" });
129
130
  this.#unsubscribe();
130
131
  this.#transport.close();
131
132
  }
133
+
134
+ dispose(): void {
135
+ for (const active of this.#runs.values()) {
136
+ for (const pending of active.pendingTools.values()) {
137
+ pending.reject(new ToolError("JS worker closed"));
138
+ }
139
+ active.pendingTools.clear();
140
+ }
141
+ this.#runs.clear();
142
+ this.#runtime?.dispose?.();
143
+ this.#runtime = null;
144
+ this.#unsubscribe();
145
+ try {
146
+ this.#transport.close();
147
+ } catch {
148
+ // Ignore
149
+ }
150
+ }
132
151
  }
@@ -242,7 +242,7 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
242
242
 
243
243
  let sm: SessionManager;
244
244
  try {
245
- sm = await SessionManager.open(inputPath);
245
+ sm = await SessionManager.open(inputPath, undefined, undefined, { suppressBreadcrumb: true });
246
246
  } catch (err) {
247
247
  if (isEnoent(err)) throw new Error(`File not found: ${inputPath}`);
248
248
  throw err;