@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.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 (146) hide show
  1. package/CHANGELOG.md +159 -182
  2. package/dist/types/async/job-manager.d.ts +3 -3
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/claude-trace-cli.d.ts +54 -0
  5. package/dist/types/cli/session-picker.d.ts +10 -3
  6. package/dist/types/cli/update-cli.d.ts +17 -0
  7. package/dist/types/commands/launch.d.ts +3 -0
  8. package/dist/types/config/keybindings.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +11 -2
  10. package/dist/types/config/settings.d.ts +13 -0
  11. package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
  12. package/dist/types/eval/concurrency-bridge.d.ts +26 -0
  13. package/dist/types/eval/heartbeat.d.ts +45 -0
  14. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  15. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  16. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  17. package/dist/types/extensibility/extensions/types.d.ts +24 -5
  18. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
  19. package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
  20. package/dist/types/lsp/index.d.ts +2 -0
  21. package/dist/types/lsp/utils.d.ts +4 -0
  22. package/dist/types/main.d.ts +5 -0
  23. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  24. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  25. package/dist/types/modes/components/custom-editor.d.ts +3 -2
  26. package/dist/types/modes/components/hook-selector.d.ts +10 -1
  27. package/dist/types/modes/components/session-selector.d.ts +32 -5
  28. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  29. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
  30. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +10 -3
  32. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  33. package/dist/types/modes/theme/theme.d.ts +1 -1
  34. package/dist/types/modes/types.d.ts +5 -3
  35. package/dist/types/registry/agent-registry.d.ts +1 -1
  36. package/dist/types/sdk.d.ts +2 -2
  37. package/dist/types/session/agent-session.d.ts +12 -3
  38. package/dist/types/session/history-storage.d.ts +16 -1
  39. package/dist/types/session/session-manager.d.ts +4 -0
  40. package/dist/types/task/output-manager.d.ts +6 -15
  41. package/dist/types/tools/ask.d.ts +8 -6
  42. package/dist/types/tools/eval-backends.d.ts +12 -0
  43. package/dist/types/tools/eval-render.d.ts +52 -0
  44. package/dist/types/tools/eval.d.ts +2 -35
  45. package/dist/types/tools/find.d.ts +0 -9
  46. package/dist/types/tools/index.d.ts +5 -12
  47. package/dist/types/tools/path-utils.d.ts +16 -0
  48. package/dist/types/tools/sqlite-reader.d.ts +25 -8
  49. package/dist/types/tui/output-block.d.ts +4 -3
  50. package/dist/types/utils/clipboard.d.ts +4 -0
  51. package/dist/types/web/kagi.d.ts +76 -0
  52. package/dist/types/web/search/providers/exa.d.ts +7 -1
  53. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  54. package/examples/extensions/README.md +1 -0
  55. package/examples/extensions/thinking-note.ts +13 -0
  56. package/package.json +9 -9
  57. package/src/async/job-manager.ts +3 -3
  58. package/src/cli/args.ts +6 -2
  59. package/src/cli/claude-trace-cli.ts +783 -0
  60. package/src/cli/session-picker.ts +36 -10
  61. package/src/cli/update-cli.ts +35 -2
  62. package/src/commands/launch.ts +3 -0
  63. package/src/config/keybindings.ts +6 -0
  64. package/src/config/model-registry.ts +33 -4
  65. package/src/config/settings-schema.ts +12 -2
  66. package/src/config/settings.ts +23 -0
  67. package/src/discovery/claude-plugins.ts +7 -9
  68. package/src/discovery/claude.ts +41 -22
  69. package/src/edit/index.ts +23 -3
  70. package/src/eval/__tests__/agent-bridge.test.ts +148 -4
  71. package/src/eval/__tests__/heartbeat.test.ts +84 -0
  72. package/src/eval/__tests__/llm-bridge.test.ts +30 -0
  73. package/src/eval/agent-bridge.ts +44 -38
  74. package/src/eval/concurrency-bridge.ts +34 -0
  75. package/src/eval/heartbeat.ts +74 -0
  76. package/src/eval/js/executor.ts +13 -9
  77. package/src/eval/js/shared/prelude.txt +20 -17
  78. package/src/eval/js/tool-bridge.ts +5 -0
  79. package/src/eval/llm-bridge.ts +20 -14
  80. package/src/eval/py/executor.ts +14 -18
  81. package/src/eval/py/prelude.py +23 -15
  82. package/src/exec/bash-executor.ts +31 -5
  83. package/src/extensibility/extensions/loader.ts +16 -18
  84. package/src/extensibility/extensions/runner.ts +22 -17
  85. package/src/extensibility/extensions/types.ts +39 -5
  86. package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
  87. package/src/extensibility/skills.ts +0 -1
  88. package/src/internal-urls/docs-index.generated.ts +14 -13
  89. package/src/lsp/diagnostics-ledger.ts +51 -0
  90. package/src/lsp/index.ts +9 -22
  91. package/src/lsp/utils.ts +21 -0
  92. package/src/main.ts +92 -24
  93. package/src/modes/acp/acp-agent.ts +8 -4
  94. package/src/modes/acp/acp-event-mapper.ts +54 -4
  95. package/src/modes/components/assistant-message.ts +28 -1
  96. package/src/modes/components/custom-editor.ts +19 -7
  97. package/src/modes/components/hook-selector.ts +229 -44
  98. package/src/modes/components/oauth-selector.ts +12 -6
  99. package/src/modes/components/session-selector.ts +179 -24
  100. package/src/modes/components/tool-execution.ts +36 -7
  101. package/src/modes/controllers/command-controller.ts +2 -11
  102. package/src/modes/controllers/event-controller.ts +5 -2
  103. package/src/modes/controllers/extension-ui-controller.ts +6 -4
  104. package/src/modes/controllers/input-controller.ts +19 -16
  105. package/src/modes/controllers/selector-controller.ts +61 -21
  106. package/src/modes/interactive-mode.ts +127 -16
  107. package/src/modes/rpc/rpc-mode.ts +17 -6
  108. package/src/modes/theme/theme-schema.json +30 -0
  109. package/src/modes/theme/theme.ts +39 -2
  110. package/src/modes/types.ts +7 -3
  111. package/src/modes/utils/ui-helpers.ts +5 -2
  112. package/src/prompts/system/orchestrate-notice.md +5 -3
  113. package/src/prompts/system/workflow-notice.md +2 -2
  114. package/src/prompts/tools/ask.md +2 -1
  115. package/src/prompts/tools/eval.md +6 -6
  116. package/src/prompts/tools/find.md +1 -1
  117. package/src/prompts/tools/irc.md +6 -6
  118. package/src/prompts/tools/search.md +1 -1
  119. package/src/prompts/tools/task.md +1 -1
  120. package/src/registry/agent-registry.ts +1 -1
  121. package/src/sdk.ts +85 -31
  122. package/src/session/agent-session.ts +127 -57
  123. package/src/session/history-storage.ts +56 -12
  124. package/src/session/session-manager.ts +34 -0
  125. package/src/task/output-manager.ts +40 -48
  126. package/src/task/render.ts +3 -8
  127. package/src/tools/ask.ts +74 -32
  128. package/src/tools/browser/tab-worker.ts +8 -5
  129. package/src/tools/eval-backends.ts +38 -0
  130. package/src/tools/eval-render.ts +750 -0
  131. package/src/tools/eval.ts +27 -754
  132. package/src/tools/find.ts +5 -29
  133. package/src/tools/index.ts +8 -38
  134. package/src/tools/path-utils.ts +144 -1
  135. package/src/tools/read.ts +47 -0
  136. package/src/tools/renderers.ts +1 -1
  137. package/src/tools/search.ts +2 -27
  138. package/src/tools/sqlite-reader.ts +92 -9
  139. package/src/tools/write.ts +9 -1
  140. package/src/tui/output-block.ts +5 -4
  141. package/src/utils/clipboard.ts +38 -1
  142. package/src/utils/open.ts +37 -2
  143. package/src/web/kagi.ts +168 -49
  144. package/src/web/search/providers/anthropic.ts +1 -1
  145. package/src/web/search/providers/exa.ts +20 -86
  146. package/src/web/search/providers/kagi.ts +4 -0
@@ -3,6 +3,7 @@ import type { ToolSession } from "../../tools";
3
3
  import { ToolError } from "../../tools/tool-errors";
4
4
  import { EVAL_AGENT_BRIDGE_NAME, runEvalAgent } from "../agent-bridge";
5
5
  import { EVAL_BUDGET_BRIDGE_NAME, type EvalBudgetResult, runEvalBudget } from "../budget-bridge";
6
+ import { EVAL_CONCURRENCY_BRIDGE_NAME, type EvalConcurrencyResult, runEvalConcurrency } from "../concurrency-bridge";
6
7
  import { EVAL_LLM_BRIDGE_NAME, runEvalLlm } from "../llm-bridge";
7
8
  import type { JsStatusEvent } from "./shared/types";
8
9
 
@@ -17,6 +18,7 @@ interface ToolBridgeOptions {
17
18
  type ToolValue =
18
19
  | string
19
20
  | EvalBudgetResult
21
+ | EvalConcurrencyResult
20
22
  | {
21
23
  text: string;
22
24
  details?: unknown;
@@ -114,6 +116,9 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
114
116
  if (name === EVAL_BUDGET_BRIDGE_NAME) {
115
117
  return await runEvalBudget(args, options);
116
118
  }
119
+ if (name === EVAL_CONCURRENCY_BRIDGE_NAME) {
120
+ return runEvalConcurrency(args, options);
121
+ }
117
122
  const tool = getTool(options.session, name);
118
123
  const normalizedArgs = normalizeArgs(args);
119
124
  const toolCallId = `js-${name}-${crypto.randomUUID()}`;
@@ -18,6 +18,7 @@ import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit
18
18
  import { expandRoleAlias, formatModelString, resolveModelFromString } from "../config/model-resolver";
19
19
  import type { ToolSession } from "../tools";
20
20
  import { ToolError } from "../tools/tool-errors";
21
+ import { withBridgeHeartbeat } from "./heartbeat";
21
22
  import type { JsStatusEvent } from "./js/shared/types";
22
23
 
23
24
  /** Synthetic bridge name reserved for the `llm()` helper across both runtimes. */
@@ -131,20 +132,25 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
131
132
 
132
133
  const telemetry = resolveTelemetry(options.session.getTelemetry?.(), options.session.getSessionId?.() ?? undefined);
133
134
 
134
- const response = await instrumentedCompleteSimple(
135
- model,
136
- {
137
- systemPrompt: system ? [system] : undefined,
138
- messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
139
- tools,
140
- },
141
- {
142
- apiKey,
143
- signal: options.signal,
144
- reasoning: reasoningForTier(tier, model),
145
- toolChoice: schema ? { type: "tool", name: STRUCTURED_TOOL_NAME } : undefined,
146
- },
147
- { telemetry, oneshotKind: "eval_llm" },
135
+ // A oneshot completion emits no status until it returns, so pump a heartbeat
136
+ // while it runs to keep the eval idle watchdog armed across a slow (e.g.
137
+ // reasoning-tier) request that would otherwise look like a stalled cell.
138
+ const response = await withBridgeHeartbeat(options.emitStatus, () =>
139
+ instrumentedCompleteSimple(
140
+ model,
141
+ {
142
+ systemPrompt: system ? [system] : undefined,
143
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
144
+ tools,
145
+ },
146
+ {
147
+ apiKey,
148
+ signal: options.signal,
149
+ reasoning: reasoningForTier(tier, model),
150
+ toolChoice: schema ? { type: "tool", name: STRUCTURED_TOOL_NAME } : undefined,
151
+ },
152
+ { telemetry, oneshotKind: "eval_llm" },
153
+ ),
148
154
  );
149
155
 
150
156
  if (response.stopReason === "error") {
@@ -5,6 +5,7 @@ import { Settings } from "../../config/settings";
5
5
  import { OutputSink } from "../../session/streaming-output";
6
6
  import type { ToolSession } from "../../tools";
7
7
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
8
+ import { EVAL_HEARTBEAT_OP } from "../heartbeat";
8
9
  import type { JsStatusEvent } from "../js/shared/types";
9
10
  import {
10
11
  checkPythonKernelAvailability,
@@ -231,20 +232,18 @@ async function waitForPromiseWithCancellation<T>(
231
232
  // Result formatting
232
233
  // ---------------------------------------------------------------------------
233
234
 
234
- function formatTimeoutAnnotation(timeoutMs?: number, idle = false): string | undefined {
235
- const suffix = idle ? " of inactivity" : "";
235
+ function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
236
236
  if (timeoutMs === undefined) return "Command timed out";
237
237
  const secs = Math.max(1, Math.round(timeoutMs / 1000));
238
- return `Command timed out after ${secs} seconds${suffix}`;
238
+ return `Command timed out after ${secs} seconds`;
239
239
  }
240
240
 
241
- function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean, idle = false): string {
241
+ function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
242
242
  const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
243
- const suffix = idle ? " of inactivity" : "";
244
243
  if (kernelKilled) {
245
- return `eval cell timed out${suffix} and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.`;
244
+ return "eval cell timed out and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.";
246
245
  }
247
- const duration = secs === undefined ? "the configured timeout" : `${secs}s${suffix}`;
246
+ const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
248
247
  return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
249
248
  }
250
249
 
@@ -488,16 +487,17 @@ async function executeWithKernel(
488
487
  const displayOutputs: KernelDisplayOutput[] = [];
489
488
  const deadlineMs = getExecutionDeadlineMs(options);
490
489
  let executionTimeoutMs: number | undefined;
491
- // Idle mode: the caller (eval tool) drives cancellation via an idle-aware
492
- // signal and passes no wall-clock deadline, so annotate timeouts with the
493
- // configured inactivity budget rather than a remaining-deadline figure.
494
- const idleMode = deadlineMs === undefined && options?.idleTimeoutMs !== undefined;
495
490
 
496
491
  // Collect every display output and, for status events, stream them live so
497
492
  // long-running bridge helpers (e.g. `agent()`) surface progress mid-cell.
498
493
  const collectDisplay = (output: KernelDisplayOutput) => {
494
+ if (output.type === "status") {
495
+ // Heartbeats are pure idle-watchdog keepalives: forward them so the
496
+ // eval tool re-arms its timer, but never store or render them.
497
+ options?.onStatus?.(output.event);
498
+ if (output.event.op === EVAL_HEARTBEAT_OP) return;
499
+ }
499
500
  displayOutputs.push(output);
500
- if (output.type === "status") options?.onStatus?.(output.event);
501
501
  };
502
502
  const emitStatus = options?.emitStatus ?? ((event: JsStatusEvent) => collectDisplay({ type: "status", event }));
503
503
  const runId = `py-${crypto.randomUUID()}`;
@@ -524,11 +524,7 @@ async function executeWithKernel(
524
524
 
525
525
  if (result.cancelled) {
526
526
  const annotation = result.timedOut
527
- ? formatKernelTimeoutAnnotation(
528
- executionTimeoutMs ?? options?.idleTimeoutMs,
529
- result.kernelKilled ?? false,
530
- idleMode,
531
- )
527
+ ? formatKernelTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, result.kernelKilled ?? false)
532
528
  : undefined;
533
529
  return {
534
530
  exitCode: undefined,
@@ -566,7 +562,7 @@ async function executeWithKernel(
566
562
  displayOutputs,
567
563
  stdinRequested: false,
568
564
  ...(await sink.dump(
569
- timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, idleMode) : undefined,
565
+ timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs) : undefined,
570
566
  )),
571
567
  };
572
568
  }
@@ -503,27 +503,35 @@ if "__omp_prelude_loaded__" not in globals():
503
503
  text = res.get("text") if isinstance(res, dict) else res
504
504
  return json.loads(text) if schema is not None else text
505
505
 
506
- def _normalize_concurrency(value):
507
- """Clamp a concurrency hint to [1, 16], defaulting to 4 on bad input."""
506
+ def _concurrency_limit():
507
+ """Worker-pool ceiling from the host ``task.maxConcurrency`` setting.
508
+
509
+ An eval fan-out runs as wide as a ``task`` batch would. Returns ``0`` for
510
+ unbounded (run every item at once); falls back to ``0`` if the host
511
+ bridge is unreachable.
512
+ """
508
513
  try:
509
- n = int(value)
510
- except (TypeError, ValueError):
511
- n = 4
512
- return max(1, min(16, n))
514
+ snap = _bridge_call("__concurrency__", {}) or {}
515
+ n = int(snap.get("limit") or 0)
516
+ except Exception:
517
+ return 0
518
+ return n if n > 0 else 0
513
519
 
514
- def _pool_map(items, fn, concurrency):
520
+ def _pool_map(items, fn):
515
521
  """Run ``fn`` over ``items`` through a bounded thread pool.
516
522
 
517
523
  Preserves input order, barriers until every task settles, and raises the
518
524
  lowest-index exception if any task failed. Each task runs inside a copy
519
525
  of the submitting thread's context so the ``_CURRENT_RID`` ContextVar
520
- propagates and bridge calls (agent(), tool.*, etc.) keep working.
526
+ propagates and bridge calls (agent(), tool.*, etc.) keep working. The
527
+ pool width tracks ``task.maxConcurrency`` (0 = run every item at once).
521
528
  """
522
529
  import concurrent.futures, contextvars
523
530
  items = list(items)
524
531
  if not items:
525
532
  return []
526
- workers = min(_normalize_concurrency(concurrency), len(items))
533
+ limit = _concurrency_limit()
534
+ workers = min(limit, len(items)) if limit > 0 else len(items)
527
535
  results = [None] * len(items)
528
536
  errors = {}
529
537
  with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
@@ -541,30 +549,30 @@ if "__omp_prelude_loaded__" not in globals():
541
549
  raise errors[min(errors)]
542
550
  return results
543
551
 
544
- def parallel(thunks, *, concurrency=4):
552
+ def parallel(thunks):
545
553
  """Run zero-arg callables through a bounded pool, preserving input order.
546
554
 
547
555
  Barriers until all finish; re-raises the lowest-index exception if any
548
- thunk raised.
556
+ thunk raised. Pool width tracks the task tool's ``task.maxConcurrency``.
549
557
  """
550
558
  thunks = list(thunks)
551
559
  for t in thunks:
552
560
  if not callable(t):
553
561
  raise TypeError("parallel() expects an iterable of zero-arg callables")
554
- return _pool_map(thunks, lambda t: t(), concurrency)
562
+ return _pool_map(thunks, lambda t: t())
555
563
 
556
- def pipeline(items, *stages, concurrency=4):
564
+ def pipeline(items, *stages):
557
565
  """Map items left-to-right through one-arg stage callables.
558
566
 
559
567
  Every item clears stage N before any item enters stage N+1 (barrier per
560
568
  stage). Stage 1 receives the original item; later stages receive the
561
- previous stage's result.
569
+ previous stage's result. Pool width tracks ``task.maxConcurrency``.
562
570
  """
563
571
  current = list(items)
564
572
  for stage in stages:
565
573
  if not callable(stage):
566
574
  raise TypeError("pipeline() stages must be callables")
567
- current = _pool_map(current, stage, concurrency)
575
+ current = _pool_map(current, stage)
568
576
  return current
569
577
 
570
578
  def log(message):
@@ -52,6 +52,27 @@ export interface BashResult {
52
52
 
53
53
  const shellSessions = new Map<string, Shell>();
54
54
  const brokenShellSessions = new Set<string>();
55
+ const shellSessionQuarantines = new Map<string, Promise<unknown>>();
56
+
57
+ function quarantineShellSession(
58
+ sessionKey: string,
59
+ runPromise: Promise<ShellRunResult>,
60
+ abortCleanupPromise: Promise<void> | undefined,
61
+ ): void {
62
+ brokenShellSessions.add(sessionKey);
63
+ const cleanup = abortCleanupPromise
64
+ ? Promise.allSettled([runPromise, abortCleanupPromise])
65
+ : Promise.allSettled([runPromise]);
66
+ shellSessionQuarantines.set(sessionKey, cleanup);
67
+ void cleanup
68
+ .finally(() => {
69
+ if (shellSessionQuarantines.get(sessionKey) === cleanup) {
70
+ shellSessionQuarantines.delete(sessionKey);
71
+ brokenShellSessions.delete(sessionKey);
72
+ }
73
+ })
74
+ .catch(() => undefined);
75
+ }
55
76
 
56
77
  async function resolveShellCwd(cwd: string | undefined): Promise<string | undefined> {
57
78
  if (!cwd) return undefined;
@@ -134,13 +155,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
134
155
  }
135
156
  const userSignal = options?.signal;
136
157
  const runAbortController = new AbortController();
158
+ let abortCleanupPromise: Promise<void> | undefined;
137
159
  const abortCurrentExecution = () => {
138
160
  if (!runAbortController.signal.aborted) {
139
161
  runAbortController.abort();
140
162
  }
141
- if (shellSession) {
142
- // Native abort is async; fire-and-forget because the caller races the command separately.
143
- void shellSession.abort();
163
+ if (shellSession && !abortCleanupPromise) {
164
+ abortCleanupPromise = shellSession.abort().catch(() => undefined);
144
165
  }
145
166
  };
146
167
  const abortDeferred = Promise.withResolvers<"abort">();
@@ -209,8 +230,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
209
230
  acceptingChunks = false;
210
231
  if (shellSession) {
211
232
  resetSession = true;
212
- brokenShellSessions.add(sessionKey);
213
- void runPromise.finally(() => brokenShellSessions.delete(sessionKey)).catch(() => undefined);
233
+ quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
214
234
  } else {
215
235
  void runPromise.catch(() => undefined);
216
236
  }
@@ -235,6 +255,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
235
255
  ? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
236
256
  : "Command timed out";
237
257
  resetSession = true;
258
+ if (shellSession) {
259
+ quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
260
+ }
238
261
  return {
239
262
  exitCode: undefined,
240
263
  cancelled: true,
@@ -245,6 +268,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
245
268
  // Handle cancellation
246
269
  if (winner.result.cancelled) {
247
270
  resetSession = true;
271
+ if (shellSession) {
272
+ quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
273
+ }
248
274
  return {
249
275
  exitCode: undefined,
250
276
  cancelled: true,
@@ -5,7 +5,8 @@ import type * as fs1 from "node:fs";
5
5
  import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
- import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
8
+ import type { ImageContent, Model, TextContent, TSchema } from "@oh-my-pi/pi-ai";
9
+ import * as PiCodingAgent from "@oh-my-pi/pi-coding-agent";
9
10
  import type { KeyId } from "@oh-my-pi/pi-tui";
10
11
  import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
11
12
  import * as Zod from "zod/v4";
@@ -22,6 +23,7 @@ import * as TypeBox from "../typebox";
22
23
 
23
24
  import { resolvePath } from "../utils";
24
25
  import type {
26
+ AssistantThinkingRenderer,
25
27
  Extension,
26
28
  ExtensionAPI,
27
29
  ExtensionContext,
@@ -29,6 +31,7 @@ import type {
29
31
  ExtensionRuntime as IExtensionRuntime,
30
32
  LoadExtensionsResult,
31
33
  MessageRenderer,
34
+ ProviderConfig,
32
35
  RegisteredCommand,
33
36
  ToolDefinition,
34
37
  } from "./types";
@@ -55,8 +58,7 @@ export class ExtensionRuntimeNotInitializedError extends Error {
55
58
  */
56
59
  export class ExtensionRuntime implements IExtensionRuntime {
57
60
  flagValues = new Map<string, boolean | string>();
58
- pendingProviderRegistrations: Array<{ name: string; config: import("./types").ProviderConfig; sourceId: string }> =
59
- [];
61
+ pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }> = [];
60
62
 
61
63
  sendMessage(): void {
62
64
  throw new ExtensionRuntimeNotInitializedError();
@@ -123,12 +125,12 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
123
125
  readonly flagValues = new Map<string, boolean | string>();
124
126
  readonly pendingProviderRegistrations: Array<{
125
127
  name: string;
126
- config: import("./types").ProviderConfig;
128
+ config: ProviderConfig;
127
129
  sourceId: string;
128
130
  }> = [];
129
131
 
130
132
  constructor(
131
- public readonly pi: typeof import("@oh-my-pi/pi-coding-agent"),
133
+ public readonly pi: typeof PiCodingAgent,
132
134
  private readonly extension: Extension,
133
135
  private readonly runtime: IExtensionRuntime,
134
136
  private readonly cwd: string,
@@ -141,10 +143,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
141
143
  this.extension.handlers.set(event, list);
142
144
  }
143
145
 
144
- registerTool<
145
- TParams extends import("@oh-my-pi/pi-ai").TSchema = import("@oh-my-pi/pi-ai").TSchema,
146
- TDetails = unknown,
147
- >(tool: ToolDefinition<TParams, TDetails>): void {
146
+ registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
148
147
  this.extension.tools.set(tool.name, {
149
148
  definition: tool,
150
149
  extensionPath: this.extension.path,
@@ -190,6 +189,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
190
189
  this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
191
190
  }
192
191
 
192
+ registerAssistantThinkingRenderer(renderer: AssistantThinkingRenderer): void {
193
+ this.extension.assistantThinkingRenderers.push(renderer);
194
+ }
195
+
193
196
  getFlag(name: string): boolean | string | undefined {
194
197
  if (!this.extension.flags.has(name)) return undefined;
195
198
  return this.runtime.flagValues.get(name);
@@ -253,7 +256,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
253
256
  return this.runtime.setSessionName(name);
254
257
  }
255
258
 
256
- registerProvider(name: string, config: import("./types").ProviderConfig): void {
259
+ registerProvider(name: string, config: ProviderConfig): void {
257
260
  this.runtime.pendingProviderRegistrations.push({ name, config, sourceId: this.extension.path });
258
261
  }
259
262
  }
@@ -267,6 +270,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
267
270
  resolvedPath,
268
271
  handlers: new Map(),
269
272
  tools: new Map(),
273
+ assistantThinkingRenderers: [],
270
274
  messageRenderers: new Map(),
271
275
  commands: new Map(),
272
276
  flags: new Map(),
@@ -293,13 +297,7 @@ async function loadExtension(
293
297
  }
294
298
 
295
299
  const extension = createExtension(extensionPath, resolvedPath);
296
- const api = new ConcreteExtensionAPI(
297
- await import("@oh-my-pi/pi-coding-agent"),
298
- extension,
299
- runtime,
300
- cwd,
301
- eventBus,
302
- );
300
+ const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
303
301
  await factory(api);
304
302
 
305
303
  return { extension, error: null };
@@ -320,7 +318,7 @@ export async function loadExtensionFromFactory(
320
318
  name = "<inline>",
321
319
  ): Promise<Extension> {
322
320
  const extension = createExtension(name, name);
323
- const api = new ConcreteExtensionAPI(await import("@oh-my-pi/pi-coding-agent"), extension, runtime, cwd, eventBus);
321
+ const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
324
322
  await factory(api);
325
323
  return extension;
326
324
  }
@@ -10,6 +10,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
10
10
  import type { SessionManager } from "../../session/session-manager";
11
11
  import type {
12
12
  AfterProviderResponseEvent,
13
+ AssistantThinkingRenderer,
13
14
  BeforeAgentStartEvent,
14
15
  BeforeAgentStartEventResult,
15
16
  BeforeProviderRequestEvent,
@@ -343,22 +344,22 @@ export class ExtensionRunner {
343
344
  this.runtime.flagValues.set(name, value);
344
345
  }
345
346
 
346
- static readonly #RESERVED_SHORTCUTS = new Set([
347
- "ctrl+c",
348
- "ctrl+d",
349
- "ctrl+z",
350
- "ctrl+k",
351
- "ctrl+p",
352
- "ctrl+l",
353
- "ctrl+o",
354
- "ctrl+t",
355
- "ctrl+g",
356
- "shift+tab",
357
- "shift+ctrl+p",
358
- "alt+enter",
359
- "escape",
360
- "enter",
361
- ]);
347
+ static readonly #RESERVED_SHORTCUTS: Record<string, true> = {
348
+ "ctrl+c": true,
349
+ "ctrl+d": true,
350
+ "ctrl+z": true,
351
+ "ctrl+k": true,
352
+ "ctrl+p": true,
353
+ "ctrl+l": true,
354
+ "ctrl+o": true,
355
+ "ctrl+t": true,
356
+ "ctrl+g": true,
357
+ "shift+tab": true,
358
+ "shift+ctrl+p": true,
359
+ "alt+enter": true,
360
+ escape: true,
361
+ enter: true,
362
+ };
362
363
 
363
364
  getShortcuts(): Map<KeyId, ExtensionShortcut> {
364
365
  const allShortcuts = new Map<KeyId, ExtensionShortcut>();
@@ -366,7 +367,7 @@ export class ExtensionRunner {
366
367
  for (const [key, shortcut] of ext.shortcuts) {
367
368
  const normalizedKey = key.toLowerCase() as KeyId;
368
369
 
369
- if (ExtensionRunner.#RESERVED_SHORTCUTS.has(normalizedKey)) {
370
+ if (ExtensionRunner.#RESERVED_SHORTCUTS[normalizedKey]) {
370
371
  logger.warn("Extension shortcut conflicts with built-in shortcut", {
371
372
  key,
372
373
  extensionPath: shortcut.extensionPath,
@@ -419,6 +420,10 @@ export class ExtensionRunner {
419
420
  return undefined;
420
421
  }
421
422
 
423
+ getAssistantThinkingRenderers(): AssistantThinkingRenderer[] {
424
+ return this.extensions.flatMap(ext => ext.assistantThinkingRenderers);
425
+ }
426
+
422
427
  getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
423
428
  this.#commandDiagnostics = [];
424
429
 
@@ -25,6 +25,8 @@ import type {
25
25
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
26
26
  import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
27
27
  import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
28
+ import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
29
+ import type * as Zod from "zod/v4";
28
30
  import type { KeybindingsManager } from "../../config/keybindings";
29
31
  import type { ModelRegistry } from "../../config/model-registry";
30
32
  import type { EditToolDetails } from "../../edit";
@@ -81,6 +83,7 @@ import type {
81
83
  TurnStartEvent,
82
84
  } from "../shared-events";
83
85
  import type { SlashCommandInfo } from "../slash-commands";
86
+ import type * as TypeBox from "../typebox";
84
87
 
85
88
  export type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
86
89
  export type { ExecOptions, ExecResult } from "../../exec/exec";
@@ -90,6 +93,17 @@ export type { AgentToolResult, AgentToolUpdateCallback };
90
93
  // UI Context
91
94
  // ============================================================================
92
95
 
96
+ export interface ExtensionUISelectOption {
97
+ label: string;
98
+ description?: string;
99
+ }
100
+
101
+ export type ExtensionUISelectItem = string | ExtensionUISelectOption;
102
+
103
+ export function getExtensionUISelectOptionLabel(option: ExtensionUISelectItem): string {
104
+ return typeof option === "string" ? option : option.label;
105
+ }
106
+
93
107
  /**
94
108
  * UI dialog options for extensions.
95
109
  */
@@ -135,8 +149,12 @@ export type ExtensionWidgetContent = string[] | ExtensionUiComponentFactory | un
135
149
  // and may be invoked from event handlers that have already taken the agent
136
150
  // loop's lock — hooks intentionally cannot.
137
151
  export interface ExtensionUIContext {
138
- /** Show a selector and return the user's choice. */
139
- select(title: string, options: string[], dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined>;
152
+ /** Show a selector and return the selected label, even when an option also includes a description. */
153
+ select(
154
+ title: string,
155
+ options: ExtensionUISelectItem[],
156
+ dialogOptions?: ExtensionUIDialogOptions,
157
+ ): Promise<string | undefined>;
140
158
 
141
159
  /** Show a confirmation dialog. */
142
160
  confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean>;
@@ -799,6 +817,18 @@ export type MessageRenderer<T = unknown> = (
799
817
  theme: Theme,
800
818
  ) => Component | undefined;
801
819
 
820
+ export interface AssistantThinkingRenderContext {
821
+ contentIndex: number;
822
+ thinkingIndex: number;
823
+ text: string;
824
+ requestRender(): void;
825
+ }
826
+
827
+ export type AssistantThinkingRenderer = (
828
+ context: AssistantThinkingRenderContext,
829
+ theme: Theme,
830
+ ) => Component | undefined;
831
+
802
832
  // ============================================================================
803
833
  // Command Registration
804
834
  // ============================================================================
@@ -830,13 +860,13 @@ export interface ExtensionAPI {
830
860
  // =========================================================================
831
861
 
832
862
  /** File logger for error/warning/debug messages */
833
- logger: typeof import("@oh-my-pi/pi-utils").logger;
863
+ logger: typeof PiLogger;
834
864
 
835
865
  /** Injected zod-backed typebox shim for legacy `Type.Object(...)` parameter authoring. */
836
- typebox: typeof import("../typebox");
866
+ typebox: typeof TypeBox;
837
867
 
838
868
  /** Injected zod module for Zod-authored extension tools (canonical going forward). */
839
- zod: typeof import("zod/v4");
869
+ zod: typeof Zod;
840
870
 
841
871
  /** Injected pi-coding-agent exports for accessing SDK utilities */
842
872
  pi: typeof piCodingAgent;
@@ -950,6 +980,9 @@ export interface ExtensionAPI {
950
980
  /** Register a custom renderer for CustomMessageEntry. */
951
981
  registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
952
982
 
983
+ /** Register a renderer for assistant thinking blocks. Rendered after the original thinking text. */
984
+ registerAssistantThinkingRenderer(renderer: AssistantThinkingRenderer): void;
985
+
953
986
  // =========================================================================
954
987
  // Actions
955
988
  // =========================================================================
@@ -1232,6 +1265,7 @@ export interface Extension {
1232
1265
  label?: string;
1233
1266
  handlers: Map<string, HandlerFn[]>;
1234
1267
  tools: Map<string, RegisteredTool<any, any>>;
1268
+ assistantThinkingRenderers: AssistantThinkingRenderer[];
1235
1269
  messageRenderers: Map<string, MessageRenderer>;
1236
1270
  commands: Map<string, RegisteredCommand>;
1237
1271
  flags: Map<string, ExtensionFlag>;