@oh-my-pi/pi-coding-agent 15.9.5 → 15.9.67

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 (98) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/config/keybindings.d.ts +4 -1
  3. package/dist/types/config/settings-schema.d.ts +11 -1
  4. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  5. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
  6. package/dist/types/eval/backend.d.ts +6 -6
  7. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  8. package/dist/types/eval/idle-timeout.d.ts +16 -14
  9. package/dist/types/eval/js/executor.d.ts +3 -3
  10. package/dist/types/eval/py/executor.d.ts +2 -2
  11. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  12. package/dist/types/modes/components/assistant-message.d.ts +5 -0
  13. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  14. package/dist/types/modes/components/model-selector.d.ts +1 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +1 -1
  18. package/dist/types/modes/types.d.ts +1 -1
  19. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  20. package/dist/types/tools/eval-render.d.ts +8 -0
  21. package/dist/types/tools/render-utils.d.ts +25 -0
  22. package/dist/types/tui/code-cell.d.ts +6 -0
  23. package/dist/types/tui/output-block.d.ts +11 -0
  24. package/package.json +9 -9
  25. package/src/autoresearch/dashboard.ts +11 -21
  26. package/src/cli/claude-trace-cli.ts +13 -1
  27. package/src/config/keybindings.ts +58 -1
  28. package/src/config/settings-schema.ts +11 -1
  29. package/src/debug/raw-sse.ts +18 -4
  30. package/src/edit/file-snapshot-store.ts +1 -1
  31. package/src/edit/index.ts +1 -1
  32. package/src/edit/renderer.ts +7 -7
  33. package/src/edit/streaming.ts +1 -1
  34. package/src/eval/__tests__/agent-bridge.test.ts +28 -27
  35. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  36. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  37. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  38. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  39. package/src/eval/__tests__/shared-executors.test.ts +2 -2
  40. package/src/eval/agent-bridge.ts +4 -5
  41. package/src/eval/backend.ts +6 -6
  42. package/src/eval/bridge-timeout.ts +44 -0
  43. package/src/eval/idle-timeout.ts +33 -15
  44. package/src/eval/js/executor.ts +10 -10
  45. package/src/eval/llm-bridge.ts +4 -5
  46. package/src/eval/py/executor.ts +6 -6
  47. package/src/eval/py/kernel.ts +11 -1
  48. package/src/eval/py/spawn-options.ts +126 -0
  49. package/src/export/ttsr.ts +9 -0
  50. package/src/extensibility/extensions/runner.ts +2 -0
  51. package/src/internal-urls/docs-index.generated.ts +6 -5
  52. package/src/lsp/client.ts +80 -2
  53. package/src/lsp/index.ts +38 -4
  54. package/src/lsp/render.ts +3 -3
  55. package/src/main.ts +1 -1
  56. package/src/modes/components/agent-dashboard.ts +13 -4
  57. package/src/modes/components/assistant-message.ts +22 -1
  58. package/src/modes/components/copy-selector.ts +249 -0
  59. package/src/modes/components/extensions/extension-list.ts +17 -8
  60. package/src/modes/components/history-search.ts +19 -11
  61. package/src/modes/components/model-selector.ts +125 -29
  62. package/src/modes/components/oauth-selector.ts +28 -12
  63. package/src/modes/components/session-observer-overlay.ts +13 -15
  64. package/src/modes/components/session-selector.ts +24 -13
  65. package/src/modes/components/tool-execution.ts +27 -13
  66. package/src/modes/components/tree-selector.ts +19 -7
  67. package/src/modes/components/user-message-selector.ts +25 -14
  68. package/src/modes/controllers/command-controller.ts +0 -116
  69. package/src/modes/controllers/event-controller.ts +26 -10
  70. package/src/modes/controllers/selector-controller.ts +38 -1
  71. package/src/modes/interactive-mode.ts +4 -4
  72. package/src/modes/theme/theme.ts +46 -10
  73. package/src/modes/types.ts +1 -1
  74. package/src/modes/utils/copy-targets.ts +254 -0
  75. package/src/prompts/tools/ast-edit.md +1 -1
  76. package/src/prompts/tools/ast-grep.md +1 -1
  77. package/src/prompts/tools/read.md +1 -1
  78. package/src/prompts/tools/search.md +1 -1
  79. package/src/session/agent-session.ts +6 -2
  80. package/src/slash-commands/builtin-registry.ts +3 -11
  81. package/src/task/render.ts +38 -11
  82. package/src/tools/bash.ts +18 -8
  83. package/src/tools/browser/render.ts +5 -4
  84. package/src/tools/debug.ts +3 -3
  85. package/src/tools/eval-render.ts +24 -9
  86. package/src/tools/eval.ts +14 -19
  87. package/src/tools/fetch.ts +5 -5
  88. package/src/tools/read.ts +7 -7
  89. package/src/tools/render-utils.ts +46 -0
  90. package/src/tools/ssh.ts +21 -8
  91. package/src/tools/write.ts +17 -8
  92. package/src/tui/code-cell.ts +19 -4
  93. package/src/tui/output-block.ts +14 -0
  94. package/src/web/search/render.ts +3 -3
  95. package/dist/types/eval/heartbeat.d.ts +0 -45
  96. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  97. package/src/eval/heartbeat.ts +0 -74
  98. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,7 +2,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.9.67] - 2026-06-06
6
+ ### Added
7
+
8
+ - Added `timeout-pause` and `timeout-resume` eval bridge status events emitted around `agent()`/`llm()` operations
9
+ - Added a `/copy` picker: `/copy` now opens a fullscreen, outlined tree of recent assistant messages with their code blocks nested beneath (like `/tree`). Navigate with ↑↓, and Enter copies the highlighted node — a whole message, an individual code block, "All N blocks", or a bash/eval command interleaved with the assistant turn that issued it. A live preview pane shows the selected target, wrapping prose and syntax-highlighting code/commands.
10
+
11
+ ### Changed
12
+
13
+ - Changed eval timeout accounting so delegated bridge calls now suspend the cell watchdog and start a fresh timeout window when runtime control returns
14
+ - Changed `IdleTimeout` to support reference-counted pauses so overlapping delegated bridge calls keep timeout paused until all calls complete
15
+ - Changed the default `app.message.followUp` binding from `Ctrl+Enter` alone to `[Ctrl+Q, Ctrl+Enter]` so the follow-up shortcut works in Windows Terminal, which does not deliver a distinct `Ctrl+Enter` event to console apps. `Ctrl+Q` mirrors the GitHub Copilot CLI default for the same action; existing remaps in `~/.omp/agent/keybindings.yml` are untouched, and if another user-remapped action already claims `Ctrl+Q`, that user binding wins while follow-up keeps `Ctrl+Enter`. `Ctrl+Q` is also reserved by `ExtensionRunner` so an extension cannot register that chord and be silently overwritten by the built-in follow-up handler ([#1903](https://github.com/can1357/oh-my-pi/issues/1903)).
16
+ - Changed all scrollable TUI pickers and viewports to render through the shared `ScrollView` right-edge scrollbar for a uniform look, replacing their ad-hoc `(N/M)` / `[a-b/total]` text indicators (search hints and the tree filter-mode label are preserved). Covers the session/resume picker, model selector, OAuth provider selector, history search, session tree selector, agent dashboard list, extension list, user-message selector, the raw SSE debug viewer, the autoresearch dashboard overlay, and the session observer overlay.
17
+ - Changed the `/model` and `/switch` selectors to dim and skip models whose context windows are smaller than the current chat context.
18
+ - Changed `/copy` command targets to appear inline with recent assistant messages instead of as a separate "Last bash command" row at the end of the picker.
19
+
20
+ ### Fixed
21
+
22
+ - Fixed the idle `Working...` loader freezing on ED3-risk terminals with unobservable native scrollback by keeping foreground live-region rendering enabled from `agent_start` until `agent_end`, before the first assistant or tool event arrives.
23
+ - Fixed framed tool output blocks rendering one column inset inside tool boxes; modern bordered blocks now span the same width as legacy background-filled tool boxes.
24
+ - Fixed potential `TimeoutError` aborts for short `timeout` eval cells during long bridged `agent()`/`llm()` work where no progress events are emitted until completion
25
+ - Fixed retry recovery to allow automatic retries without switching models when `retry.modelFallback` is disabled.
26
+ - Fixed `ttsr.enabled: false` being ignored at runtime. TTSR rules were still being registered with `TtsrManager.addRule` and matched against stream deltas even when the global toggle was off, so disabling TTSR did not suppress rule injection or stream abort. The manager now gates `addRule`, `hasRules`, and `#matchBuffer` on the enabled flag, so disabling fully short-circuits the TTSR path. Condition rules fall through to the rulebook bucket instead of being silently swallowed. ([#1767](https://github.com/can1357/oh-my-pi/issues/1767))
27
+ - Fixed the Python eval kernel hanging on Windows during `import pandas` / `import numpy`, with SIGINT unable to recover the cell. `PythonKernel.start()` spawned the runner with `windowsHide: true`, which in Bun maps to the Win32 `CREATE_NO_WINDOW` flag and detaches the long-lived child from any inherited console — so native extensions like `numpy/_core/_multiarray_umath.pyd` (and its bundled OpenBLAS/SLEEF thread-pool init) could deadlock inside `LoadLibraryExW`, and `GenerateConsoleCtrlEvent`-based SIGINT delivery silently became a no-op. The kernel now hides its window only when the host itself has no console to share (service / piped launch); an interactive TUI launch lets the kernel inherit the parent's console, matching the behavior of `python.exe` invoked from `cmd.exe` ([#1960](https://github.com/can1357/oh-my-pi/issues/1960)).
28
+ - Fixed `task` renderer crashing the TUI with `TypeError: completeData?.map is not a function` when a subagent's `extractedToolData.yield` slot held a non-array value. `renderAgentResult` (and the live-progress sibling) cast the slot to `Array<{ data }>` and called `?.map`, but optional chaining short-circuits only on `null`/`undefined`, so a plain object made `.map` `undefined` and threw — taking down every `review` task render. Both sites now go through `normalizeYieldData`, which wraps a single object as a 1-element array and drops primitives ([#1987](https://github.com/can1357/oh-my-pi/issues/1987))
29
+ - Fixed `sdk-async-job-manager-singleton` tests flaking under the full parallel suite. The four `createAgentSession`-based cases ran on the default 5000ms per-test timeout, which two real session startups can exceed when `test:ts` saturates the machine across packages; on timeout the still-running test body and `afterEach` reset raced, surfacing a spurious "Unhandled error between tests" on the `AsyncJobManager.instance()` assertion. They now carry an explicit 60000ms timeout, matching the convention used by the other session-creating tests in this suite.
30
+ - Fixed streaming `eval`, `bash`, `ssh`, and `task` call previews overflowing the live transcript viewport and cutting off their top while pending. A volatile tool block taller than the viewport could strand its scrolled-off head out of native scrollback on ED3-risk terminals (committed nowhere, repainted nowhere) until the result landed. The pending `eval` source preview now follows the streaming edge in a bounded 12-line tail window (newest lines pinned to the bottom, "… N earlier lines" on top) so you can watch the code being written without the box overflowing; `bash`/`ssh` commands and `task` context use a bounded head+tail window. `Ctrl+O` still lifts the cap for a full view.
31
+ - Fixed the streaming `write` call preview ignoring `Ctrl+O` so the expand toggle was a no-op while a file was being written. Unlike the `eval`/`bash`/`ssh`/`task` streaming previews, `formatStreamingContent` never received the `expanded` flag, leaving the preview pinned to a bounded 12-line tail window even after pressing `Ctrl+O` — so on a large write you could not widen past the streaming edge until the tool result landed. The preview now lifts the cap to the full file (head through tail) when expanded, matching the documented streaming-preview behavior of the other tools.
32
+ - Fixed turn-ending provider errors rendering twice — once as the transcript's inline `Error: …` line and again in the pinned banner above the editor (added in 15.9.5). The inline line is now suppressed while the same error is mirrored in the banner and restored to the transcript when the banner clears at the next turn, so the error stays in history without the duplicate render at the error moment.
33
+
34
+ ### Removed
35
+
36
+ - Removed the `/copy last|code|all|cmd` subcommands; every copy target is now reachable by picking it in the `/copy` tree.
37
+
5
38
  ## [15.9.5] - 2026-06-05
39
+
6
40
  ### Added
7
41
 
8
42
  - Added a persistent error banner pinned above the editor when an assistant turn ends on a provider error (e.g. Anthropic's "Output blocked by content filtering policy"). The transcript `Error: …` line scrolls away as the conversation grows, so terminal turns that ended on a stream error could pass unnoticed; the banner stays in the fixed region above the input and is cleared when the next turn starts.
@@ -31,6 +65,7 @@
31
65
  - Blocked OSC 8 hyperlink wrapping for URI targets containing terminal control bytes to avoid rendering malformed control-sequence links
32
66
 
33
67
  ## [15.9.4] - 2026-06-05
68
+
34
69
  ### Fixed
35
70
 
36
71
  - Fixed chat transcript updates after submitting input so frozen scrollback is only thawed when native scrollback replay succeeds, preventing misplaced or duplicated rows when the viewport is not at the tail
@@ -224,7 +224,7 @@ export declare const KEYBINDINGS: {
224
224
  readonly description: "Open external editor";
225
225
  };
226
226
  readonly "app.message.followUp": {
227
- readonly defaultKeys: "ctrl+enter";
227
+ readonly defaultKeys: ["ctrl+q", "ctrl+enter"];
228
228
  readonly description: "Send follow-up message";
229
229
  };
230
230
  readonly "app.message.dequeue": {
@@ -329,6 +329,9 @@ export declare class KeybindingsManager extends TuiKeybindingsManager {
329
329
  * Reload keybindings from the config file.
330
330
  */
331
331
  reload(): void;
332
+ setUserBindings(userBindings: KeybindingsConfig): void;
333
+ getKeys(keybinding: Keybinding): KeyId[];
334
+ getResolvedBindings(): KeybindingsConfig;
332
335
  /**
333
336
  * Get the effective resolved bindings (defaults + user overrides).
334
337
  */
@@ -1027,6 +1027,15 @@ export declare const SETTINGS_SCHEMA: {
1027
1027
  readonly description: "Maximum wait between retries, in ms. When the provider asks us to wait longer than this and no credential or model fallback succeeds, the request fails fast instead of sleeping (e.g. 3-hour Anthropic rate-limit windows).";
1028
1028
  };
1029
1029
  };
1030
+ readonly "retry.modelFallback": {
1031
+ readonly type: "boolean";
1032
+ readonly default: true;
1033
+ readonly ui: {
1034
+ readonly tab: "model";
1035
+ readonly label: "Retry Model Fallback";
1036
+ readonly description: "Allow retry recovery to switch to configured fallback models";
1037
+ };
1038
+ };
1030
1039
  readonly "retry.fallbackChains": {
1031
1040
  readonly type: "record";
1032
1041
  readonly default: Record<string, string[]>;
@@ -2186,7 +2195,7 @@ export declare const SETTINGS_SCHEMA: {
2186
2195
  readonly ui: {
2187
2196
  readonly tab: "editing";
2188
2197
  readonly label: "Hash Lines";
2189
- readonly description: "Include snapshot-tag headers and line numbers in read output for hashline edit mode (PATH#tag plus LINE:content)";
2198
+ readonly description: "Include snapshot-tag headers and line numbers in read output for hashline edit mode ([PATH#TAG] plus LINE:content)";
2190
2199
  };
2191
2200
  };
2192
2201
  readonly "read.defaultLimit": {
@@ -3902,6 +3911,7 @@ export interface RetrySettings {
3902
3911
  maxRetries: number;
3903
3912
  baseDelayMs: number;
3904
3913
  maxDelayMs: number;
3914
+ modelFallback: boolean;
3905
3915
  }
3906
3916
  export interface MemoriesSettings {
3907
3917
  enabled: boolean;
@@ -12,7 +12,7 @@ import { InMemorySnapshotStore } from "@oh-my-pi/hashline";
12
12
  /**
13
13
  * Upper bound on the file size we snapshot. A section tag is a content hash of
14
14
  * the *whole* file, so minting one means holding the full normalized text in
15
- * the store. Files above this cap emit no path#tag` header — line-anchored
15
+ * the store. Files above this cap emit no `[path#tag]` header — line-anchored
16
16
  * editing of multi-megabyte files is out of scope under the full-content model.
17
17
  */
18
18
  export declare const SNAPSHOT_MAX_BYTES: number;
@@ -0,0 +1 @@
1
+ export {};
@@ -9,12 +9,12 @@ export interface ExecutorBackendExecOptions {
9
9
  signal?: AbortSignal;
10
10
  session: ToolSession;
11
11
  /**
12
- * Inactivity budget in milliseconds (the cell's `timeout`). Cancellation is
13
- * driven entirely by `signal`, which the eval tool arms as an idle watchdog
14
- * that fires a `TimeoutError` reason after this much time with no progress
15
- * (status) events. Backends use this value only for timeout-annotation text
16
- * and as cold-start headroom; they MUST NOT derive a competing wall-clock
17
- * timer from it.
12
+ * Runtime-work budget in milliseconds (the cell's `timeout`). Cancellation is
13
+ * driven entirely by `signal`, which the eval tool arms as a watchdog that
14
+ * pauses on bridge timeout-control status events and fires a `TimeoutError`
15
+ * reason only while the Python/JS runtime owns control. Backends use this
16
+ * value only for timeout-annotation text and as cold-start headroom; they MUST
17
+ * NOT derive a competing wall-clock timer from it.
18
18
  */
19
19
  idleTimeoutMs: number;
20
20
  reset: boolean;
@@ -0,0 +1,27 @@
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
+ /** Synthetic status op emitted when a bridge call leaves the cell runtime. */
18
+ export declare const EVAL_TIMEOUT_PAUSE_OP = "timeout-pause";
19
+ /** Synthetic status op emitted when a bridge call returns control to the runtime. */
20
+ export declare const EVAL_TIMEOUT_RESUME_OP = "timeout-resume";
21
+ /** Whether a status event is pure eval-timeout control and should not render. */
22
+ export declare function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean;
23
+ /**
24
+ * Run {@link operation} while suspending the eval watchdog through
25
+ * {@link emitStatus}. A no-op wrapper when no status sink is wired.
26
+ */
27
+ export declare function withBridgeTimeoutPause<T>(emitStatus: ((event: JsStatusEvent) => void) | undefined, operation: () => Promise<T>): Promise<T>;
@@ -1,27 +1,29 @@
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 declare class IdleTimeout {
17
15
  #private;
18
16
  constructor(idleMs: number);
19
- /** Aborts with a `TimeoutError` reason once the inactivity budget is exhausted. */
17
+ /** Aborts with a `TimeoutError` reason once the active timeout window is exhausted. */
20
18
  get signal(): AbortSignal;
21
- /** Configured inactivity budget in milliseconds. */
19
+ /** Configured active timeout window in milliseconds. */
22
20
  get idleMs(): number;
23
- /** Record activity, pushing the inactivity deadline forward by `idleMs`. */
21
+ /** Record runtime activity, pushing the active deadline forward by `idleMs`. */
24
22
  bump(): void;
23
+ /** Suspend timeout accounting while control is delegated to host-side work. */
24
+ pause(): void;
25
+ /** Resume timeout accounting with a fresh timeout window. */
26
+ resume(): void;
25
27
  /** Stop the watchdog. Safe to call multiple times. */
26
28
  dispose(): void;
27
29
  [Symbol.dispose](): void;
@@ -6,9 +6,9 @@ export interface JsExecutorOptions {
6
6
  timeoutMs?: number;
7
7
  deadlineMs?: number;
8
8
  /**
9
- * Inactivity budget (ms). Used for worker cold-start headroom and
10
- * timeout-annotation text when the caller drives cancellation via an
11
- * idle-aware `signal` instead of `deadlineMs`/`timeoutMs`. Never arms a timer.
9
+ * Runtime-work budget (ms). Used for worker cold-start headroom and
10
+ * timeout-annotation text when the caller drives cancellation via the eval
11
+ * watchdog `signal` instead of `deadlineMs`/`timeoutMs`. Never arms a timer.
12
12
  */
13
13
  idleTimeoutMs?: number;
14
14
  onChunk?: (chunk: string) => Promise<void> | void;
@@ -10,8 +10,8 @@ export interface PythonExecutorOptions {
10
10
  /** Absolute wall-clock deadline in milliseconds since epoch */
11
11
  deadlineMs?: number;
12
12
  /**
13
- * Inactivity budget (ms). Used only for timeout-annotation text when the
14
- * caller drives cancellation via an idle-aware `signal` instead of a
13
+ * Runtime-work budget (ms). Used only for timeout-annotation text when the
14
+ * caller drives cancellation via the eval watchdog `signal` instead of a
15
15
  * wall-clock `deadlineMs`/`timeoutMs`. Does not arm a timer.
16
16
  */
17
17
  idleTimeoutMs?: number;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Decide whether the long-lived Python kernel subprocess should be spawned
3
+ * with `windowsHide: true`.
4
+ *
5
+ * On Windows, Bun maps `windowsHide: true` to the `CREATE_NO_WINDOW` flag,
6
+ * which detaches the child from any inherited console. The Python kernel
7
+ * runs user code that imports NumPy/pandas; those native extensions
8
+ * (`numpy/_core/_multiarray_umath.pyd` + bundled OpenBLAS/SLEEF thread-pool
9
+ * init) can deadlock inside `LoadLibraryExW` when no console is attached,
10
+ * and a console-less child cannot receive SIGINT via
11
+ * `GenerateConsoleCtrlEvent` (the recovery path the host relies on). See
12
+ * issue #1960.
13
+ *
14
+ * So on Windows we hide only when the host itself has no console to share.
15
+ * In any launch where a console is attached — even one with every stdio
16
+ * stream redirected — the kernel inherits the parent's console, matching
17
+ * `python.exe` invoked from `cmd.exe`, which keeps native imports and
18
+ * SIGINT recovery working.
19
+ *
20
+ * Short-lived helper subprocesses elsewhere in the codebase (LSP probes,
21
+ * git, plugin installs) keep `windowsHide: true` because they don't load
22
+ * complex native modules and the brief console flash would be user-visible
23
+ * noise.
24
+ */
25
+ export declare function shouldHideKernelWindow(opts: {
26
+ platform: NodeJS.Platform;
27
+ hostHasInheritableConsole: boolean;
28
+ }): boolean;
29
+ /**
30
+ * TTY-based fallback used when the Win32 console probe is unavailable.
31
+ *
32
+ * Returns `true` if any of stdin/stdout/stderr is currently a TTY. This
33
+ * correctly detects the common interactive launches and the partial-
34
+ * redirection cases (`omp -p > out.txt`, `< in.txt`, `2> err.log`) where at
35
+ * least one stream stays bound to the terminal. The all-stdio-redirected
36
+ * case (`< in > out 2> err` from a console) is the reason we prefer the
37
+ * Win32 probe over this fallback whenever possible.
38
+ */
39
+ export declare function consoleAttachedViaTTY(opts: {
40
+ stdinIsTTY: boolean;
41
+ stdoutIsTTY: boolean;
42
+ stderrIsTTY: boolean;
43
+ }): boolean;
44
+ /** Reset the cached Win32 probe result. Test-only; not part of the public surface. */
45
+ export declare function __resetWindowsConsoleProbeCache(): void;
46
+ /**
47
+ * Whether the host process owns a console its children can inherit.
48
+ *
49
+ * - On Windows, the authoritative signal is `GetConsoleWindow()`. It returns
50
+ * a non-NULL HWND whenever the process has a console attached, regardless
51
+ * of how the standard streams are redirected — so an `omp -p ... < in.txt
52
+ * > out.txt 2> err.log` launched from a real Windows Terminal session is
53
+ * correctly classified as console-attached and the kernel keeps its
54
+ * inheritable console.
55
+ * - On any other platform, or if the FFI probe fails, fall back to the
56
+ * TTY-OR heuristic. That still catches the common interactive cases.
57
+ */
58
+ export declare function hostHasInheritableConsole(): boolean;
@@ -13,6 +13,11 @@ export declare class AssistantMessageComponent extends Container {
13
13
  constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, thinkingRenderers?: readonly AssistantThinkingRenderer[], imageBudget?: ImageBudget | undefined);
14
14
  invalidate(): void;
15
15
  setHideThinkingBlock(hide: boolean): void;
16
+ /**
17
+ * Toggle suppression of the inline `Error: …` line while the same error is
18
+ * pinned in the banner above the editor. Re-renders so the change is visible.
19
+ */
20
+ setErrorPinned(pinned: boolean): void;
16
21
  isTranscriptBlockFinalized(): boolean;
17
22
  /**
18
23
  * Assistant text/thinking streams in append-only: earlier rendered rows never
@@ -0,0 +1,22 @@
1
+ import { type Component } from "@oh-my-pi/pi-tui";
2
+ import type { CopyTarget } from "../utils/copy-targets";
3
+ export interface CopySelectorCallbacks {
4
+ /** A copy target was chosen — copy its `content`. */
5
+ onPick: (target: CopyTarget) => void;
6
+ /** The picker was dismissed. */
7
+ onCancel: () => void;
8
+ }
9
+ /**
10
+ * Fullscreen `/copy` picker rendered as a `/tree`-style tree inside one
11
+ * outlined box: a title, the tree of copy targets (recent assistant messages
12
+ * with their code blocks nested beneath), a live preview of the highlighted
13
+ * node, and a keybinding footer. Every node copies its `content` on Enter.
14
+ */
15
+ export declare class CopySelectorComponent implements Component {
16
+ #private;
17
+ private readonly callbacks;
18
+ constructor(roots: CopyTarget[], callbacks: CopySelectorCallbacks);
19
+ invalidate(): void;
20
+ handleInput(keyData: string): void;
21
+ render(width: number): string[];
22
+ }
@@ -20,6 +20,7 @@ export declare class ModelSelectorComponent extends Container {
20
20
  constructor(tui: TUI, _currentModel: Model | undefined, settings: Settings, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: RoleSelectCallback, onCancel: () => void, options?: {
21
21
  temporaryOnly?: boolean;
22
22
  initialSearchInput?: string;
23
+ currentContextTokens?: number;
23
24
  });
24
25
  handleInput(keyData: string): void;
25
26
  getSearchInput(): Input;
@@ -13,7 +13,6 @@ export declare class CommandController {
13
13
  handleDumpCommand(): void;
14
14
  handleDebugTranscriptCommand(): Promise<void>;
15
15
  handleShareCommand(): Promise<void>;
16
- handleCopyCommand(sub?: string): void;
17
16
  handleSessionCommand(): Promise<void>;
18
17
  handleJobsCommand(): Promise<void>;
19
18
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
@@ -35,6 +35,7 @@ export declare class SelectorController {
35
35
  }): void;
36
36
  showPluginSelector(mode?: "install" | "uninstall"): Promise<void>;
37
37
  showUserMessageSelector(): void;
38
+ showCopySelector(): void;
38
39
  showTreeSelector(): void;
39
40
  showSessionSelector(): Promise<void>;
40
41
  handleResumeSession(sessionPath: string): Promise<void>;
@@ -199,7 +199,6 @@ export declare class InteractiveMode implements InteractiveModeContext {
199
199
  handleDumpCommand(): void;
200
200
  handleDebugTranscriptCommand(): Promise<void>;
201
201
  handleShareCommand(): Promise<void>;
202
- handleCopyCommand(sub?: string): void;
203
202
  handleTodoCommand(args: string): Promise<void>;
204
203
  handleSessionCommand(): Promise<void>;
205
204
  handleJobsCommand(): Promise<void>;
@@ -236,6 +235,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
236
235
  }): void;
237
236
  showPluginSelector(mode?: "install" | "uninstall"): void;
238
237
  showUserMessageSelector(): void;
238
+ showCopySelector(): void;
239
239
  showTreeSelector(): void;
240
240
  showSessionSelector(): void;
241
241
  handleResumeSession(sessionPath: string): Promise<void>;
@@ -197,7 +197,6 @@ export interface InteractiveModeContext {
197
197
  toggleTodoExpansion(): void;
198
198
  handleExportCommand(text: string): Promise<void>;
199
199
  handleShareCommand(): Promise<void>;
200
- handleCopyCommand(sub?: string): void;
201
200
  handleTodoCommand(args: string): Promise<void>;
202
201
  handleSessionCommand(): Promise<void>;
203
202
  handleJobsCommand(): Promise<void>;
@@ -235,6 +234,7 @@ export interface InteractiveModeContext {
235
234
  }): void;
236
235
  showPluginSelector(mode?: "install" | "uninstall"): void;
237
236
  showUserMessageSelector(): void;
237
+ showCopySelector(): void;
238
238
  showTreeSelector(): void;
239
239
  showSessionSelector(): void;
240
240
  handleResumeSession(sessionPath: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ /** A fenced code block extracted from assistant markdown. */
3
+ export interface CodeBlock {
4
+ /** Info string after the opening fence (language id), trimmed. */
5
+ lang: string;
6
+ /** Block body with the trailing newline stripped. */
7
+ code: string;
8
+ }
9
+ /** A runnable command found in the transcript. */
10
+ export interface LastCommand {
11
+ kind: "bash" | "eval";
12
+ code: string;
13
+ /** Highlight language: "bash" for bash, "python"/"javascript" for eval. */
14
+ language: string;
15
+ }
16
+ /**
17
+ * A node in the `/copy` picker tree. Leaves carry `content` (placed on the
18
+ * clipboard) plus `copyMessage` (the status shown afterwards); groups carry
19
+ * `children` to drill into.
20
+ */
21
+ export interface CopyTarget {
22
+ /** Stable identifier (e.g. "msg:1", "msg:1:code:0", "msg:1:all", "cmd:1"). */
23
+ id: string;
24
+ label: string;
25
+ /** Dim annotation: line/block counts, language, or tool name. */
26
+ hint?: string;
27
+ /** Full text rendered in the preview pane. */
28
+ preview: string;
29
+ /** Highlight language for code/command previews (undefined = plain/markdown). */
30
+ language?: string;
31
+ /** Leaf: text copied to the clipboard. */
32
+ content?: string;
33
+ /** Leaf: status message shown after copying. */
34
+ copyMessage?: string;
35
+ /** Group: nested targets to drill into. */
36
+ children?: CopyTarget[];
37
+ }
38
+ /** Minimal session surface needed to assemble copy targets (eases testing). */
39
+ export interface CopySource {
40
+ readonly messages: readonly AgentMessage[];
41
+ getLastVisibleHandoffText(): string | undefined;
42
+ }
43
+ /** Extract fenced code blocks from assistant markdown, in document order. */
44
+ export declare function extractCodeBlocks(text: string): CodeBlock[];
45
+ /** Walk the transcript backwards for the most recent bash command or eval code. */
46
+ export declare function extractLastCommand(messages: readonly AgentMessage[]): LastCommand | undefined;
47
+ /**
48
+ * Assemble the unified `/copy` target tree: recent assistant messages
49
+ * (most recent first, each drillable into its code blocks), runnable command
50
+ * targets interleaved after the assistant message that issued them, and a
51
+ * fresh-handoff fallback when no assistant message exists yet.
52
+ */
53
+ export declare function buildCopyTargets(source: CopySource): CopyTarget[];
@@ -13,6 +13,14 @@ import type { EvalStatusEvent, EvalToolDetails } from "../eval/types";
13
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
14
  import { type Theme } from "../modes/theme/theme";
15
15
  export declare const EVAL_DEFAULT_PREVIEW_LINES = 10;
16
+ /**
17
+ * Rows of source kept in the *pending* eval preview. The window follows the
18
+ * streaming edge (newest lines pinned to the bottom) so you can watch the code
19
+ * being written, while staying bounded — a volatile tool block taller than the
20
+ * viewport would otherwise strand its scrolled-off head out of native scrollback
21
+ * on ED3-risk terminals. Matches the streaming windows used by edit/write.
22
+ */
23
+ export declare const EVAL_STREAMING_PREVIEW_LINES = 12;
16
24
  interface EvalRenderCellArg {
17
25
  language?: string;
18
26
  code?: string;
@@ -76,6 +76,31 @@ export declare function formatBadge(label: string, color: ToolUIColor, theme: Th
76
76
  * Uses consistent wording pattern.
77
77
  */
78
78
  export declare function formatMoreItems(remaining: number, itemType: string): string;
79
+ /**
80
+ * Maximum rows a tool's streaming/pending *call* preview may render before it is
81
+ * capped. This is intentionally conservative: the preview still sits inside a
82
+ * transcript that already consumed some viewport rows, and tool blocks carry
83
+ * extra chrome (status/header/border/"more lines"), so a "reasonable" raw code
84
+ * or command preview like 10-12 lines can still overflow and strand its top
85
+ * while the block is volatile. Keeping the live call window short avoids that
86
+ * across terminals without turning the transcript into an interactive scroller.
87
+ */
88
+ export declare const CALL_PREVIEW_MAX_LINES = 6;
89
+ /**
90
+ * Cap a pre-rendered pending/call preview to a bounded window. When truncated,
91
+ * show both the head and the live tail so the user can still see what the tool
92
+ * is currently writing while the volatile block stays short enough not to strand
93
+ * its top above the viewport. `Ctrl+O` widens the bounded window, but does not
94
+ * fully uncap live tool previews for the same reason.
95
+ *
96
+ * `prefix` (raw, e.g. a dim tree gutter) is prepended to the summary line so
97
+ * nested previews stay aligned.
98
+ */
99
+ export declare function capPreviewLines(lines: string[], theme: Theme, options?: {
100
+ max?: number;
101
+ expanded?: boolean;
102
+ prefix?: string;
103
+ }): string[];
79
104
  export declare function formatMeta(meta: string[], theme: Theme): string;
80
105
  export declare function formatErrorMessage(message: string | undefined, theme: Theme): string;
81
106
  export declare function formatEmptyMessage(message: string, theme: Theme): string;
@@ -11,6 +11,12 @@ export interface CodeCellOptions {
11
11
  output?: string;
12
12
  outputMaxLines?: number;
13
13
  codeMaxLines?: number;
14
+ /**
15
+ * Show the LAST `codeMaxLines` rows (the live streaming edge) instead of the
16
+ * first, with a "… N earlier lines" marker on top. Lets a pending preview
17
+ * follow code as it is written while staying bounded. Ignored when `expanded`.
18
+ */
19
+ codeTail?: boolean;
14
20
  expanded?: boolean;
15
21
  /** Animate the cell border with a sweeping segment while pending/running. */
16
22
  animate?: boolean;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Bordered output container with optional header and sections.
3
+ */
4
+ import type { Component } from "@oh-my-pi/pi-tui";
1
5
  import type { Theme } from "../modes/theme/theme";
2
6
  import type { State } from "./types";
3
7
  export interface OutputBlockOptions {
@@ -13,6 +17,12 @@ export interface OutputBlockOptions {
13
17
  /** Animate the border with a sweeping dark segment (pending/running state). */
14
18
  animate?: boolean;
15
19
  }
20
+ declare const FRAMED_BLOCK_COMPONENT: unique symbol;
21
+ export type FramedBlockComponent = Component & {
22
+ [FRAMED_BLOCK_COMPONENT]?: true;
23
+ };
24
+ export declare function markFramedBlockComponent<T extends Component>(component: T): T & FramedBlockComponent;
25
+ export declare function isFramedBlockComponent(component: Component): boolean;
16
26
  /**
17
27
  * Monotonic frame counter for animated borders, quantized to the TUI's ~16ms
18
28
  * render cap so the cache key advances once per ~60fps frame — fine enough for a
@@ -44,3 +54,4 @@ export declare class CachedOutputBlock {
44
54
  /** Invalidate the cache, forcing a rebuild on next render. */
45
55
  invalidate(): void;
46
56
  }
57
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.9.5",
4
+ "version": "15.9.67",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.9.5",
51
- "@oh-my-pi/omp-stats": "15.9.5",
52
- "@oh-my-pi/pi-agent-core": "15.9.5",
53
- "@oh-my-pi/pi-ai": "15.9.5",
54
- "@oh-my-pi/pi-mnemopi": "15.9.5",
55
- "@oh-my-pi/pi-natives": "15.9.5",
56
- "@oh-my-pi/pi-tui": "15.9.5",
57
- "@oh-my-pi/pi-utils": "15.9.5",
50
+ "@oh-my-pi/hashline": "15.9.67",
51
+ "@oh-my-pi/omp-stats": "15.9.67",
52
+ "@oh-my-pi/pi-agent-core": "15.9.67",
53
+ "@oh-my-pi/pi-ai": "15.9.67",
54
+ "@oh-my-pi/pi-mnemopi": "15.9.67",
55
+ "@oh-my-pi/pi-natives": "15.9.67",
56
+ "@oh-my-pi/pi-tui": "15.9.67",
57
+ "@oh-my-pi/pi-utils": "15.9.67",
58
58
  "@opentelemetry/api": "^1.9.1",
59
59
  "@opentelemetry/context-async-hooks": "^2.7.1",
60
60
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,4 +1,4 @@
1
- import { matchesKey, replaceTabs, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { matchesKey, replaceTabs, ScrollView, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import type { Theme } from "../modes/theme/theme";
3
3
  import { formatElapsed, formatNum, isBetter } from "./helpers";
4
4
  import { currentResults, findBaselineMetric, findBaselineRunNumber, findBaselineSecondary } from "./state";
@@ -76,14 +76,14 @@ export function createDashboardController(): DashboardController {
76
76
  const viewportRows = Math.max(4, terminalRows - 4);
77
77
  const maxScroll = Math.max(0, body.length - viewportRows);
78
78
  if (scrollOffset > maxScroll) scrollOffset = maxScroll;
79
- const visible = body.slice(scrollOffset, scrollOffset + viewportRows);
80
- const footer = renderOverlayFooter(width, scrollOffset, viewportRows, body.length, theme);
81
- return [
82
- header,
83
- ...visible,
84
- ...Array.from({ length: Math.max(0, viewportRows - visible.length) }, () => ""),
85
- footer,
86
- ];
79
+ const sv = new ScrollView(body.slice(scrollOffset, scrollOffset + viewportRows), {
80
+ height: viewportRows,
81
+ scrollbar: "auto",
82
+ totalRows: body.length,
83
+ theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
84
+ });
85
+ sv.setScrollOffset(scrollOffset);
86
+ return [header, ...sv.render(width), renderOverlayFooter(width, theme)];
87
87
  },
88
88
  handleInput(data: string): void {
89
89
  const totalRows =
@@ -406,18 +406,8 @@ function renderOverlayRunningLine(
406
406
  );
407
407
  }
408
408
 
409
- function renderOverlayFooter(
410
- width: number,
411
- scrollOffset: number,
412
- viewportRows: number,
413
- totalRows: number,
414
- theme: Theme,
415
- ): string {
416
- const position =
417
- totalRows > viewportRows
418
- ? ` ${scrollOffset + 1}-${Math.min(totalRows, scrollOffset + viewportRows)}/${totalRows}`
419
- : "";
420
- const hint = theme.fg("dim", ` up/down j/k pageup pagedown g G esc${position} `);
409
+ function renderOverlayFooter(width: number, theme: Theme): string {
410
+ const hint = theme.fg("dim", " up/down j/k pageup pagedown g G esc ");
421
411
  const fill = Math.max(0, width - visibleWidth(hint));
422
412
  return theme.fg("borderMuted", "-".repeat(fill)) + hint;
423
413
  }