@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.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 (192) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +10 -2
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +43 -7
  22. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  23. package/dist/types/eval/backend.d.ts +6 -6
  24. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  25. package/dist/types/eval/idle-timeout.d.ts +16 -14
  26. package/dist/types/eval/js/executor.d.ts +3 -3
  27. package/dist/types/eval/py/executor.d.ts +2 -2
  28. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  29. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  30. package/dist/types/lsp/types.d.ts +10 -0
  31. package/dist/types/main.d.ts +3 -2
  32. package/dist/types/memory-backend/index.d.ts +2 -1
  33. package/dist/types/memory-backend/resolve.d.ts +1 -1
  34. package/dist/types/memory-backend/types.d.ts +1 -1
  35. package/dist/types/modes/components/assistant-message.d.ts +5 -0
  36. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  37. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  38. package/dist/types/modes/components/model-selector.d.ts +1 -0
  39. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  42. package/dist/types/modes/index.d.ts +5 -4
  43. package/dist/types/modes/interactive-mode.d.ts +2 -2
  44. package/dist/types/modes/setup-version.d.ts +11 -0
  45. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  46. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  49. package/dist/types/sdk.d.ts +1 -1
  50. package/dist/types/task/executor.d.ts +7 -0
  51. package/dist/types/telemetry-export.d.ts +1 -1
  52. package/dist/types/tools/eval-render.d.ts +1 -0
  53. package/dist/types/tools/fetch.d.ts +15 -7
  54. package/dist/types/tools/render-utils.d.ts +33 -0
  55. package/dist/types/tools/renderers.d.ts +16 -2
  56. package/dist/types/tools/search.d.ts +1 -1
  57. package/dist/types/tools/write.d.ts +2 -0
  58. package/dist/types/tui/code-cell.d.ts +6 -0
  59. package/dist/types/tui/output-block.d.ts +11 -0
  60. package/dist/types/web/scrapers/github.d.ts +22 -0
  61. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +9 -9
  64. package/scripts/dev-launch +42 -0
  65. package/scripts/dev-launch-preload.ts +19 -0
  66. package/src/autoresearch/dashboard.ts +11 -21
  67. package/src/cli/args.ts +2 -2
  68. package/src/cli/claude-trace-cli.ts +13 -1
  69. package/src/cli/gallery-cli.ts +223 -0
  70. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  71. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  72. package/src/cli/gallery-fixtures/edit.ts +194 -0
  73. package/src/cli/gallery-fixtures/fs.ts +153 -0
  74. package/src/cli/gallery-fixtures/index.ts +40 -0
  75. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  76. package/src/cli/gallery-fixtures/memory.ts +81 -0
  77. package/src/cli/gallery-fixtures/misc.ts +221 -0
  78. package/src/cli/gallery-fixtures/search.ts +213 -0
  79. package/src/cli/gallery-fixtures/shell.ts +167 -0
  80. package/src/cli/gallery-fixtures/types.ts +41 -0
  81. package/src/cli/gallery-fixtures/web.ts +158 -0
  82. package/src/cli/gallery-screenshot.ts +279 -0
  83. package/src/cli-commands.ts +1 -0
  84. package/src/commands/gallery.ts +52 -0
  85. package/src/commands/launch.ts +1 -1
  86. package/src/config/keybindings.ts +68 -2
  87. package/src/config/model-equivalence.ts +35 -12
  88. package/src/config/model-id-affixes.ts +39 -22
  89. package/src/config/model-registry.ts +16 -16
  90. package/src/config/settings-schema.ts +29 -6
  91. package/src/config/settings.ts +11 -0
  92. package/src/dap/client.ts +14 -16
  93. package/src/debug/raw-sse.ts +18 -4
  94. package/src/edit/file-snapshot-store.ts +1 -1
  95. package/src/edit/index.ts +1 -1
  96. package/src/edit/renderer.ts +43 -55
  97. package/src/edit/streaming.ts +1 -1
  98. package/src/eval/__tests__/agent-bridge.test.ts +102 -58
  99. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  100. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  101. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  102. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  103. package/src/eval/agent-bridge.ts +38 -12
  104. package/src/eval/backend.ts +6 -6
  105. package/src/eval/bridge-timeout.ts +44 -0
  106. package/src/eval/idle-timeout.ts +33 -15
  107. package/src/eval/js/executor.ts +10 -10
  108. package/src/eval/llm-bridge.ts +4 -5
  109. package/src/eval/py/executor.ts +6 -6
  110. package/src/eval/py/kernel.ts +11 -1
  111. package/src/eval/py/spawn-options.ts +126 -0
  112. package/src/export/ttsr.ts +9 -0
  113. package/src/extensibility/extensions/runner.ts +3 -0
  114. package/src/extensibility/plugins/doctor.ts +0 -1
  115. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  116. package/src/goals/tools/goal-tool.ts +2 -2
  117. package/src/internal-urls/docs-index.generated.ts +7 -6
  118. package/src/lsp/client.ts +179 -52
  119. package/src/lsp/index.ts +38 -4
  120. package/src/lsp/render.ts +3 -3
  121. package/src/lsp/types.ts +10 -0
  122. package/src/main.ts +47 -52
  123. package/src/memory-backend/index.ts +13 -1
  124. package/src/memory-backend/resolve.ts +3 -5
  125. package/src/memory-backend/types.ts +1 -1
  126. package/src/modes/components/agent-dashboard.ts +13 -4
  127. package/src/modes/components/assistant-message.ts +22 -1
  128. package/src/modes/components/copy-selector.ts +249 -0
  129. package/src/modes/components/custom-editor.ts +10 -1
  130. package/src/modes/components/extensions/extension-list.ts +17 -8
  131. package/src/modes/components/history-search.ts +19 -11
  132. package/src/modes/components/model-selector.ts +125 -29
  133. package/src/modes/components/oauth-selector.ts +28 -12
  134. package/src/modes/components/session-observer-overlay.ts +13 -15
  135. package/src/modes/components/session-selector.ts +24 -13
  136. package/src/modes/components/status-line.ts +3 -5
  137. package/src/modes/components/tool-execution.ts +83 -24
  138. package/src/modes/components/tree-selector.ts +19 -7
  139. package/src/modes/components/user-message-selector.ts +25 -14
  140. package/src/modes/controllers/command-controller.ts +13 -118
  141. package/src/modes/controllers/event-controller.ts +26 -10
  142. package/src/modes/controllers/input-controller.ts +11 -3
  143. package/src/modes/controllers/selector-controller.ts +40 -3
  144. package/src/modes/index.ts +5 -4
  145. package/src/modes/interactive-mode.ts +21 -7
  146. package/src/modes/setup-version.ts +11 -0
  147. package/src/modes/setup-wizard/index.ts +3 -2
  148. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  149. package/src/modes/theme/theme.ts +46 -10
  150. package/src/modes/types.ts +2 -2
  151. package/src/modes/utils/context-usage.ts +10 -6
  152. package/src/modes/utils/copy-targets.ts +254 -0
  153. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  154. package/src/prompts/tools/ast-edit.md +1 -1
  155. package/src/prompts/tools/ast-grep.md +1 -1
  156. package/src/prompts/tools/read.md +1 -1
  157. package/src/prompts/tools/search.md +1 -1
  158. package/src/sdk.ts +21 -23
  159. package/src/session/agent-session.ts +13 -9
  160. package/src/slash-commands/builtin-registry.ts +4 -12
  161. package/src/slash-commands/helpers/usage-report.ts +2 -0
  162. package/src/task/executor.ts +20 -2
  163. package/src/task/render.ts +37 -11
  164. package/src/telemetry-export.ts +25 -7
  165. package/src/tools/bash.ts +18 -8
  166. package/src/tools/browser/render.ts +5 -4
  167. package/src/tools/debug.ts +3 -3
  168. package/src/tools/eval-backends.ts +6 -17
  169. package/src/tools/eval-render.ts +28 -10
  170. package/src/tools/eval.ts +19 -23
  171. package/src/tools/fetch.ts +99 -89
  172. package/src/tools/read.ts +7 -7
  173. package/src/tools/render-utils.ts +63 -3
  174. package/src/tools/renderers.ts +16 -1
  175. package/src/tools/report-tool-issue.ts +1 -1
  176. package/src/tools/search.ts +173 -81
  177. package/src/tools/ssh.ts +21 -8
  178. package/src/tools/todo.ts +20 -7
  179. package/src/tools/write.ts +39 -9
  180. package/src/tui/code-cell.ts +19 -4
  181. package/src/tui/output-block.ts +14 -0
  182. package/src/web/scrapers/github.ts +255 -3
  183. package/src/web/scrapers/youtube.ts +3 -2
  184. package/src/web/search/providers/perplexity.ts +199 -51
  185. package/src/web/search/render.ts +42 -57
  186. package/src/web/search/types.ts +5 -1
  187. package/dist/types/eval/heartbeat.d.ts +0 -45
  188. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  189. package/src/eval/__tests__/shared-executors.test.ts +0 -609
  190. package/src/eval/heartbeat.ts +0 -74
  191. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
  192. /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Timeout suspension for in-flight host-side eval bridge calls.
3
+ *
4
+ * The eval watchdog caps a cell's `timeout` as a budget on the cell runtime's
5
+ * own work. Host-side `agent()` / `parallel()` / `llm()` bridge calls hand
6
+ * control to the outer TypeScript process, where the Python kernel or JS VM is
7
+ * only waiting for a result. While that delegated work is in flight, the cell
8
+ * timeout must be ignored completely; once the bridge returns and the runtime is
9
+ * back in control, the watchdog starts a fresh timeout window.
10
+ *
11
+ * Bridge helpers express that handoff with synthetic pause/resume status events
12
+ * on the existing `emitStatus → onStatus` path. Consumers MUST treat these as
13
+ * timeout-control events only: update the watchdog and drop them from rendered
14
+ * or persisted cell output.
15
+ */
16
+ import type { JsStatusEvent } from "./js/shared/types";
17
+
18
+ /** Synthetic status op emitted when a bridge call leaves the cell runtime. */
19
+ export const EVAL_TIMEOUT_PAUSE_OP = "timeout-pause";
20
+
21
+ /** Synthetic status op emitted when a bridge call returns control to the runtime. */
22
+ export const EVAL_TIMEOUT_RESUME_OP = "timeout-resume";
23
+
24
+ /** Whether a status event is pure eval-timeout control and should not render. */
25
+ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
26
+ return event.op === EVAL_TIMEOUT_PAUSE_OP || event.op === EVAL_TIMEOUT_RESUME_OP;
27
+ }
28
+
29
+ /**
30
+ * Run {@link operation} while suspending the eval watchdog through
31
+ * {@link emitStatus}. A no-op wrapper when no status sink is wired.
32
+ */
33
+ export async function withBridgeTimeoutPause<T>(
34
+ emitStatus: ((event: JsStatusEvent) => void) | undefined,
35
+ operation: () => Promise<T>,
36
+ ): Promise<T> {
37
+ if (!emitStatus) return operation();
38
+ emitStatus({ op: EVAL_TIMEOUT_PAUSE_OP });
39
+ try {
40
+ return await operation();
41
+ } finally {
42
+ emitStatus({ op: EVAL_TIMEOUT_RESUME_OP });
43
+ }
44
+ }
@@ -1,17 +1,15 @@
1
1
  /**
2
- * Inactivity watchdog for eval cells.
2
+ * Watchdog for eval cell work.
3
3
  *
4
- * A cell's `timeout` is treated as an *idle* budget rather than a hard
5
- * wall-clock deadline: the watchdog aborts {@link signal} (with a
6
- * `TimeoutError` reason, matching `AbortSignal.timeout`) only once `idleMs`
7
- * elapses with no {@link bump}. Every progress signal re-arms it, so a
8
- * long-running fanout that keeps reporting progress (e.g. `agent()` status
9
- * updates, `log()`/`phase()`) never trips the timeout, while a genuinely
10
- * stalled cell still gets interrupted.
4
+ * A cell's `timeout` bounds time while the Python kernel or JS VM is in control.
5
+ * Host-side bridge calls can {@link pause} the watchdog so delegated
6
+ * `agent()`/`parallel()`/`llm()` work is ignored completely, then {@link resume}
7
+ * starts a fresh timeout window once the runtime gets control back.
11
8
  *
12
- * The timer self-reschedules instead of being torn down and recreated on every
13
- * bump, so a high-frequency stream of bumps (sub-second agent progress) costs
14
- * one timestamp write per event rather than churning a timer each time.
9
+ * The active timer self-reschedules instead of being torn down on every
10
+ * activity event, so frequent activity costs one timestamp write per event.
11
+ * Pause is reference-counted because `parallel()` can have multiple bridge calls
12
+ * in flight at once.
15
13
  */
16
14
  export class IdleTimeout {
17
15
  readonly #controller = new AbortController();
@@ -20,6 +18,7 @@ export class IdleTimeout {
20
18
  #deadlineMs: number;
21
19
  #timer: NodeJS.Timeout | undefined;
22
20
  #settled = false;
21
+ #pauseDepth = 0;
23
22
 
24
23
  constructor(idleMs: number) {
25
24
  this.#idleMs = Math.max(1, Math.floor(idleMs));
@@ -27,20 +26,39 @@ export class IdleTimeout {
27
26
  this.#arm(this.#idleMs);
28
27
  }
29
28
 
30
- /** Aborts with a `TimeoutError` reason once the inactivity budget is exhausted. */
29
+ /** Aborts with a `TimeoutError` reason once the active timeout window is exhausted. */
31
30
  get signal(): AbortSignal {
32
31
  return this.#controller.signal;
33
32
  }
34
33
 
35
- /** Configured inactivity budget in milliseconds. */
34
+ /** Configured active timeout window in milliseconds. */
36
35
  get idleMs(): number {
37
36
  return this.#idleMs;
38
37
  }
39
38
 
40
- /** Record activity, pushing the inactivity deadline forward by `idleMs`. */
39
+ /** Record runtime activity, pushing the active deadline forward by `idleMs`. */
41
40
  bump(): void {
41
+ if (this.#settled || this.#pauseDepth > 0) return;
42
+ this.#deadlineMs = Date.now() + this.#idleMs;
43
+ }
44
+ /** Suspend timeout accounting while control is delegated to host-side work. */
45
+ pause(): void {
42
46
  if (this.#settled) return;
47
+ this.#pauseDepth++;
48
+ if (this.#pauseDepth !== 1) return;
49
+ if (this.#timer) {
50
+ clearTimeout(this.#timer);
51
+ this.#timer = undefined;
52
+ }
53
+ }
54
+
55
+ /** Resume timeout accounting with a fresh timeout window. */
56
+ resume(): void {
57
+ if (this.#settled || this.#pauseDepth === 0) return;
58
+ this.#pauseDepth--;
59
+ if (this.#pauseDepth > 0) return;
43
60
  this.#deadlineMs = Date.now() + this.#idleMs;
61
+ this.#arm(this.#idleMs);
44
62
  }
45
63
 
46
64
  /** Stop the watchdog. Safe to call multiple times. */
@@ -65,7 +83,7 @@ export class IdleTimeout {
65
83
  }
66
84
 
67
85
  #onExpire(): void {
68
- if (this.#settled) return;
86
+ if (this.#settled || this.#pauseDepth > 0) return;
69
87
  const remainingMs = this.#deadlineMs - Date.now();
70
88
  if (remainingMs > 0) {
71
89
  // A bump moved the deadline forward after this timer was armed; wait
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
2
2
  import type { ToolSession } from "../../tools";
3
3
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
4
- import { EVAL_HEARTBEAT_OP } from "../heartbeat";
4
+ import { isEvalTimeoutControlEvent } from "../bridge-timeout";
5
5
  import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
6
6
  import type { JsStatusEvent } from "./shared/types";
7
7
 
@@ -10,9 +10,9 @@ export interface JsExecutorOptions {
10
10
  timeoutMs?: number;
11
11
  deadlineMs?: number;
12
12
  /**
13
- * Inactivity budget (ms). Used for worker cold-start headroom and
14
- * timeout-annotation text when the caller drives cancellation via an
15
- * idle-aware `signal` instead of `deadlineMs`/`timeoutMs`. Never arms a timer.
13
+ * Runtime-work budget (ms). Used for worker cold-start headroom and
14
+ * timeout-annotation text when the caller drives cancellation via the eval
15
+ * watchdog `signal` instead of `deadlineMs`/`timeoutMs`. Never arms a timer.
16
16
  */
17
17
  idleTimeoutMs?: number;
18
18
  onChunk?: (chunk: string) => Promise<void> | void;
@@ -85,9 +85,9 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
85
85
  options.signal && timeoutSignal
86
86
  ? AbortSignal.any([options.signal, timeoutSignal])
87
87
  : (options.signal ?? timeoutSignal);
88
- // The eval tool drives cancellation via an idle-aware `signal` and passes only
89
- // an inactivity budget; use it solely as worker cold-start headroom and never
90
- // derive a competing fixed timer from it.
88
+ // The eval tool drives cancellation via its own watchdog `signal` and passes
89
+ // only the runtime-work budget; use it solely as worker cold-start headroom
90
+ // and never derive a competing fixed timer from it.
91
91
  const acquireBudgetMs = legacyTimeoutMs ?? options.idleTimeoutMs;
92
92
 
93
93
  try {
@@ -105,10 +105,10 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
105
105
  onText: chunk => outputSink.push(chunk),
106
106
  onDisplay: output => {
107
107
  if (output.type === "status") {
108
- // Heartbeats are pure idle-watchdog keepalives: forward them so
109
- // the eval tool re-arms its timer, but never store or render them.
108
+ // Timeout-control events drive the eval watchdog only; never
109
+ // store or render them as cell output.
110
110
  options.onStatus?.(output.event);
111
- if (output.event.op === EVAL_HEARTBEAT_OP) return;
111
+ if (isEvalTimeoutControlEvent(output.event)) return;
112
112
  }
113
113
  displayOutputs.push(output);
114
114
  },
@@ -18,7 +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
+ import { withBridgeTimeoutPause } from "./bridge-timeout";
22
22
  import type { JsStatusEvent } from "./js/shared/types";
23
23
 
24
24
  /** Synthetic bridge name reserved for the `llm()` helper across both runtimes. */
@@ -132,10 +132,9 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
132
132
 
133
133
  const telemetry = resolveTelemetry(options.session.getTelemetry?.(), options.session.getSessionId?.() ?? undefined);
134
134
 
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, () =>
135
+ // Suspend eval timeout accounting while the model request owns control. The
136
+ // timeout clock restarts once the bridge returns to the cell runtime.
137
+ const response = await withBridgeTimeoutPause(options.emitStatus, () =>
139
138
  instrumentedCompleteSimple(
140
139
  model,
141
140
  {
@@ -5,7 +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
+ import { isEvalTimeoutControlEvent } from "../bridge-timeout";
9
9
  import type { JsStatusEvent } from "../js/shared/types";
10
10
  import {
11
11
  checkPythonKernelAvailability,
@@ -27,8 +27,8 @@ export interface PythonExecutorOptions {
27
27
  /** Absolute wall-clock deadline in milliseconds since epoch */
28
28
  deadlineMs?: number;
29
29
  /**
30
- * Inactivity budget (ms). Used only for timeout-annotation text when the
31
- * caller drives cancellation via an idle-aware `signal` instead of a
30
+ * Runtime-work budget (ms). Used only for timeout-annotation text when the
31
+ * caller drives cancellation via the eval watchdog `signal` instead of a
32
32
  * wall-clock `deadlineMs`/`timeoutMs`. Does not arm a timer.
33
33
  */
34
34
  idleTimeoutMs?: number;
@@ -492,10 +492,10 @@ async function executeWithKernel(
492
492
  // long-running bridge helpers (e.g. `agent()`) surface progress mid-cell.
493
493
  const collectDisplay = (output: KernelDisplayOutput) => {
494
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.
495
+ // Timeout-control events drive the eval watchdog only; never store or
496
+ // render them as cell output.
497
497
  options?.onStatus?.(output.event);
498
- if (output.event.op === EVAL_HEARTBEAT_OP) return;
498
+ if (isEvalTimeoutControlEvent(output.event)) return;
499
499
  }
500
500
  displayOutputs.push(output);
501
501
  };
@@ -18,6 +18,7 @@ import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
18
18
  import { PYTHON_PRELUDE } from "./prelude";
19
19
  import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
20
20
  import { enumeratePythonRuntimes, filterEnv, type PythonRuntime, resolvePythonRuntime } from "./runtime";
21
+ import { hostHasInheritableConsole, shouldHideKernelWindow } from "./spawn-options";
21
22
 
22
23
  export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
23
24
  export { renderKernelDisplay } from "./display";
@@ -253,7 +254,16 @@ export class PythonKernel {
253
254
  stdin: "pipe",
254
255
  stdout: "pipe",
255
256
  stderr: "pipe",
256
- windowsHide: true,
257
+ // Detached from any inherited console only when the host itself
258
+ // has no console — kernel32!GetConsoleWindow() is authoritative
259
+ // (works even when every stdio stream is redirected), with a
260
+ // TTY-OR fallback when the FFI probe is unavailable. See #1960
261
+ // for the numpy/pandas LoadLibraryExW hang + SIGINT-recovery
262
+ // failure that motivates the predicate.
263
+ windowsHide: shouldHideKernelWindow({
264
+ platform: process.platform,
265
+ hostHasInheritableConsole: hostHasInheritableConsole(),
266
+ }),
257
267
  });
258
268
  kernel.#proc = proc;
259
269
  kernel.#stdin = proc.stdin;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Subprocess spawn-option helpers for the Python kernel.
3
+ *
4
+ * Pure helpers (`shouldHideKernelWindow`, `consoleAttachedViaTTY`) live here
5
+ * so they can be unit-tested without dragging in the kernel's runtime
6
+ * dependencies. The effectful `hostHasInheritableConsole` wraps a Win32 FFI
7
+ * probe with a TTY fallback and is the function `kernel.ts` actually calls.
8
+ */
9
+ import { dlopen, FFIType } from "bun:ffi";
10
+
11
+ /**
12
+ * Decide whether the long-lived Python kernel subprocess should be spawned
13
+ * with `windowsHide: true`.
14
+ *
15
+ * On Windows, Bun maps `windowsHide: true` to the `CREATE_NO_WINDOW` flag,
16
+ * which detaches the child from any inherited console. The Python kernel
17
+ * runs user code that imports NumPy/pandas; those native extensions
18
+ * (`numpy/_core/_multiarray_umath.pyd` + bundled OpenBLAS/SLEEF thread-pool
19
+ * init) can deadlock inside `LoadLibraryExW` when no console is attached,
20
+ * and a console-less child cannot receive SIGINT via
21
+ * `GenerateConsoleCtrlEvent` (the recovery path the host relies on). See
22
+ * issue #1960.
23
+ *
24
+ * So on Windows we hide only when the host itself has no console to share.
25
+ * In any launch where a console is attached — even one with every stdio
26
+ * stream redirected — the kernel inherits the parent's console, matching
27
+ * `python.exe` invoked from `cmd.exe`, which keeps native imports and
28
+ * SIGINT recovery working.
29
+ *
30
+ * Short-lived helper subprocesses elsewhere in the codebase (LSP probes,
31
+ * git, plugin installs) keep `windowsHide: true` because they don't load
32
+ * complex native modules and the brief console flash would be user-visible
33
+ * noise.
34
+ */
35
+ export function shouldHideKernelWindow(opts: {
36
+ platform: NodeJS.Platform;
37
+ hostHasInheritableConsole: boolean;
38
+ }): boolean {
39
+ if (opts.platform !== "win32") return false;
40
+ return !opts.hostHasInheritableConsole;
41
+ }
42
+
43
+ /**
44
+ * TTY-based fallback used when the Win32 console probe is unavailable.
45
+ *
46
+ * Returns `true` if any of stdin/stdout/stderr is currently a TTY. This
47
+ * correctly detects the common interactive launches and the partial-
48
+ * redirection cases (`omp -p > out.txt`, `< in.txt`, `2> err.log`) where at
49
+ * least one stream stays bound to the terminal. The all-stdio-redirected
50
+ * case (`< in > out 2> err` from a console) is the reason we prefer the
51
+ * Win32 probe over this fallback whenever possible.
52
+ */
53
+ export function consoleAttachedViaTTY(opts: {
54
+ stdinIsTTY: boolean;
55
+ stdoutIsTTY: boolean;
56
+ stderrIsTTY: boolean;
57
+ }): boolean {
58
+ return opts.stdinIsTTY || opts.stdoutIsTTY || opts.stderrIsTTY;
59
+ }
60
+
61
+ /**
62
+ * Probe `kernel32.dll!GetConsoleWindow()` to detect whether the current
63
+ * Windows process owns a console window.
64
+ *
65
+ * Returns `true` for a non-NULL HWND, `false` when NULL (no console — true
66
+ * service / `DETACHED_PROCESS` / GUI parent), and `null` when the probe
67
+ * itself fails (off-Windows, FFI disabled, or unexpected kernel32 layout).
68
+ * A `null` return means "don't trust me, use the TTY fallback".
69
+ *
70
+ * Cached on first call because in practice the console attachment of a
71
+ * long-lived OMP host never changes for the lifetime of the process, and
72
+ * we don't want to re-dlopen kernel32 on every kernel spawn.
73
+ */
74
+ type ConsoleProbeResult = boolean | null;
75
+ let cachedWindowsConsoleProbe: { value: ConsoleProbeResult } | undefined;
76
+
77
+ function probeWindowsConsoleWindow(): ConsoleProbeResult {
78
+ if (cachedWindowsConsoleProbe) return cachedWindowsConsoleProbe.value;
79
+ let value: ConsoleProbeResult = null;
80
+ try {
81
+ const lib = dlopen("kernel32.dll", {
82
+ GetConsoleWindow: { args: [], returns: FFIType.ptr },
83
+ });
84
+ try {
85
+ const hwnd = lib.symbols.GetConsoleWindow();
86
+ // FFIType.ptr returns `Pointer | null`; a 0 pointer should also be
87
+ // treated as NULL defensively in case Bun ever returns 0n / 0.
88
+ value = hwnd !== null && hwnd !== 0;
89
+ } finally {
90
+ lib.close();
91
+ }
92
+ } catch {
93
+ value = null;
94
+ }
95
+ cachedWindowsConsoleProbe = { value };
96
+ return value;
97
+ }
98
+
99
+ /** Reset the cached Win32 probe result. Test-only; not part of the public surface. */
100
+ export function __resetWindowsConsoleProbeCache(): void {
101
+ cachedWindowsConsoleProbe = undefined;
102
+ }
103
+
104
+ /**
105
+ * Whether the host process owns a console its children can inherit.
106
+ *
107
+ * - On Windows, the authoritative signal is `GetConsoleWindow()`. It returns
108
+ * a non-NULL HWND whenever the process has a console attached, regardless
109
+ * of how the standard streams are redirected — so an `omp -p ... < in.txt
110
+ * > out.txt 2> err.log` launched from a real Windows Terminal session is
111
+ * correctly classified as console-attached and the kernel keeps its
112
+ * inheritable console.
113
+ * - On any other platform, or if the FFI probe fails, fall back to the
114
+ * TTY-OR heuristic. That still catches the common interactive cases.
115
+ */
116
+ export function hostHasInheritableConsole(): boolean {
117
+ if (process.platform === "win32") {
118
+ const native = probeWindowsConsoleWindow();
119
+ if (native !== null) return native;
120
+ }
121
+ return consoleAttachedViaTTY({
122
+ stdinIsTTY: !!process.stdin.isTTY,
123
+ stdoutIsTTY: !!process.stdout.isTTY,
124
+ stderrIsTTY: !!process.stderr.isTTY,
125
+ });
126
+ }
@@ -294,6 +294,9 @@ export class TtsrManager {
294
294
 
295
295
  /** Add a TTSR rule to be monitored. */
296
296
  addRule(rule: Rule): boolean {
297
+ if (!this.#settings.enabled) {
298
+ return false;
299
+ }
297
300
  if (this.#rules.has(rule.name)) {
298
301
  return false;
299
302
  }
@@ -357,6 +360,9 @@ export class TtsrManager {
357
360
  }
358
361
 
359
362
  #matchBuffer(buffer: string, context: TtsrMatchContext): Rule[] {
363
+ if (!this.#settings.enabled) {
364
+ return [];
365
+ }
360
366
  const matches: Rule[] = [];
361
367
  for (const [name, entry] of this.#rules) {
362
368
  if (!this.#canTrigger(name)) {
@@ -433,6 +439,9 @@ export class TtsrManager {
433
439
 
434
440
  /** Check if any TTSR rules are registered. */
435
441
  hasRules(): boolean {
442
+ if (!this.#settings.enabled) {
443
+ return false;
444
+ }
436
445
  return this.#rules.size > 0;
437
446
  }
438
447
 
@@ -354,6 +354,9 @@ export class ExtensionRunner {
354
354
  "ctrl+o": true,
355
355
  "ctrl+t": true,
356
356
  "ctrl+g": true,
357
+ "alt+m": true,
358
+ // Default chord for `app.message.followUp` (Windows Terminal can't deliver Ctrl+Enter; #1903).
359
+ "ctrl+q": true,
357
360
  "shift+tab": true,
358
361
  "shift+ctrl+p": true,
359
362
  "alt+enter": true,
@@ -25,7 +25,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
25
25
  const apiKeys = [
26
26
  { name: "ANTHROPIC_API_KEY", description: "Anthropic API" },
27
27
  { name: "OPENAI_API_KEY", description: "OpenAI API" },
28
- { name: "PERPLEXITY_API_KEY", description: "Perplexity search" },
29
28
  { name: "EXA_API_KEY", description: "Exa search" },
30
29
  ];
31
30
 
@@ -0,0 +1,49 @@
1
+ import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
2
+
3
+ type MarketplaceAutoUpdateMode = "off" | "notify" | "auto";
4
+
5
+ interface MarketplaceAutoUpdateOptions {
6
+ autoUpdate: MarketplaceAutoUpdateMode;
7
+ resolveActiveProjectRegistryPath: (cwd: string) => Promise<string | null>;
8
+ clearPluginRootsCache: () => void;
9
+ }
10
+
11
+ export function scheduleMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): void {
12
+ if (options.autoUpdate === "off") {
13
+ return;
14
+ }
15
+
16
+ void runMarketplaceAutoUpdate(options);
17
+ }
18
+
19
+ async function runMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): Promise<void> {
20
+ try {
21
+ // Startup perf: marketplace manager pulls scraper/fetch/cache code; keep it out of the initial TUI graph.
22
+ const {
23
+ MarketplaceManager,
24
+ getInstalledPluginsRegistryPath,
25
+ getMarketplacesCacheDir,
26
+ getMarketplacesRegistryPath,
27
+ getPluginsCacheDir,
28
+ } = await import("./marketplace");
29
+ const mgr = new MarketplaceManager({
30
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
31
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
32
+ projectInstalledRegistryPath: (await options.resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
33
+ marketplacesCacheDir: getMarketplacesCacheDir(),
34
+ pluginsCacheDir: getPluginsCacheDir(),
35
+ clearPluginRootsCache: options.clearPluginRootsCache,
36
+ });
37
+ await mgr.refreshStaleMarketplaces();
38
+ const updates = await mgr.checkForUpdates();
39
+ if (updates.length === 0) return;
40
+ if (options.autoUpdate === "auto") {
41
+ await mgr.upgradeAllPlugins();
42
+ logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
43
+ } else {
44
+ logger.debug(`${updates.length} marketplace plugin update(s) available — /marketplace upgrade`);
45
+ }
46
+ } catch {
47
+ // Silently ignore — network failure, corrupt data, offline.
48
+ }
49
+ }
@@ -8,7 +8,7 @@ import type { Theme, ThemeColor } from "../../modes/theme/theme";
8
8
  import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
9
9
  import { formatDuration } from "../../slash-commands/helpers/format";
10
10
  import type { ToolSession } from "../../tools";
11
- import { formatErrorMessage, TRUNCATE_LENGTHS } from "../../tools/render-utils";
11
+ import { formatErrorDetail, TRUNCATE_LENGTHS } from "../../tools/render-utils";
12
12
  import { ToolError } from "../../tools/tool-errors";
13
13
  import { renderStatusLine, truncateToWidth } from "../../tui";
14
14
  import { completionBudgetReport, remainingTokens } from "../runtime";
@@ -190,7 +190,7 @@ export const goalToolRenderer = {
190
190
 
191
191
  if (result.isError) {
192
192
  const header = renderStatusLine({ icon: "error", title: "Goal", description }, uiTheme);
193
- const body = formatErrorMessage(fallbackText || "Goal tool failed", uiTheme);
193
+ const body = formatErrorDetail(fallbackText || "Goal tool failed", uiTheme);
194
194
  return new Text([header, body].join("\n"), 0, 0);
195
195
  }
196
196