@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
@@ -5,7 +5,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
5
5
  import type { Theme } from "../modes/theme/theme";
6
6
  import { type TruncationResult } from "../session/streaming-output";
7
7
  import type { ToolSession } from ".";
8
- import { type OutputMeta } from "./output-meta";
8
+ import type { OutputMeta } from "./output-meta";
9
9
  declare const searchSchema: z.ZodObject<{
10
10
  pattern: z.ZodString;
11
11
  paths: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
@@ -53,12 +53,14 @@ interface WriteRenderArgs {
53
53
  }
54
54
  export declare const writeToolRenderer: {
55
55
  renderCall(args: WriteRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component;
56
+ isStreamingPreviewAppendOnly(args: WriteRenderArgs, options: RenderResultOptions, _result?: unknown): boolean;
56
57
  renderResult(result: {
57
58
  content: Array<{
58
59
  type: string;
59
60
  text?: string;
60
61
  }>;
61
62
  details?: WriteToolDetails;
63
+ isError?: boolean;
62
64
  }, options: RenderResultOptions, uiTheme: Theme, args?: WriteRenderArgs): Component;
63
65
  mergeCallAndResult: boolean;
64
66
  };
@@ -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 {};
@@ -1,4 +1,18 @@
1
1
  import type { SpecialHandler } from "./types";
2
+ interface GitHubUrl {
3
+ type: "blob" | "tree" | "repo" | "issue" | "issues" | "pull" | "pulls" | "discussion" | "discussions" | "actions-run" | "actions-job" | "other";
4
+ owner: string;
5
+ repo: string;
6
+ ref?: string;
7
+ path?: string;
8
+ number?: number;
9
+ runId?: number;
10
+ jobId?: number;
11
+ }
12
+ /**
13
+ * Parse GitHub URL into components
14
+ */
15
+ export declare function parseGitHubUrl(url: string): GitHubUrl | null;
2
16
  /**
3
17
  * Fetch from GitHub API
4
18
  */
@@ -6,7 +20,15 @@ export declare function fetchGitHubApi(endpoint: string, timeout: number, signal
6
20
  data: unknown;
7
21
  ok: boolean;
8
22
  }>;
23
+ /**
24
+ * Strip the per-line ISO-8601 timestamp prefix GitHub prepends to every job log line.
25
+ * Cuts ~28 bytes/line of noise while preserving the message text. Also drops the leading
26
+ * UTF-8 BOM GitHub puts at the start of the log file (otherwise the first line's timestamp
27
+ * survives because `^` no longer sits before a digit).
28
+ */
29
+ export declare function stripActionsLogTimestamps(logs: string): string;
9
30
  /**
10
31
  * Handle GitHub URLs specially
11
32
  */
12
33
  export declare const handleGitHub: SpecialHandler;
34
+ export {};
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Perplexity Web Search Provider
3
3
  *
4
- * Supports three auth modes:
4
+ * Supports four auth modes:
5
5
  * - Cookies (`PERPLEXITY_COOKIES`) via `www.perplexity.ai/rest/sse/perplexity_ask`
6
6
  * - OAuth/session bearer via `AuthStorage` and `www.perplexity.ai/rest/sse/perplexity_ask`
7
7
  * - API key (`PERPLEXITY_API_KEY`) via `api.perplexity.ai/chat/completions`
8
+ * - Anonymous via `www.perplexity.ai/rest/sse/perplexity_ask`
8
9
  */
9
10
  import { type AuthStorage } from "@oh-my-pi/pi-ai";
10
11
  import type { SearchResponse } from "../../../web/search/types";
@@ -34,5 +35,11 @@ export declare class PerplexityProvider extends SearchProvider {
34
35
  readonly id = "perplexity";
35
36
  readonly label = "Perplexity";
36
37
  isAvailable(authStorage: AuthStorage): boolean;
38
+ /**
39
+ * Perplexity accepts anonymous browser-style ask requests, but keep auto
40
+ * provider selection credential-gated so a configured provider keeps priority
41
+ * over the anonymous fallback.
42
+ */
43
+ isExplicitlyAvailable(_authStorage: AuthStorage): boolean;
37
44
  search(params: SearchParams): Promise<SearchResponse>;
38
45
  }
@@ -18,7 +18,7 @@ export declare const SEARCH_PROVIDER_OPTIONS: readonly [{
18
18
  }, {
19
19
  readonly value: "perplexity";
20
20
  readonly label: "Perplexity";
21
- readonly description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY";
21
+ readonly description: "Uses auth when configured; explicit selection falls back to anonymous search";
22
22
  }, {
23
23
  readonly value: "brave";
24
24
  readonly label: "Brave";
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.10.0",
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.10.0",
51
+ "@oh-my-pi/omp-stats": "15.10.0",
52
+ "@oh-my-pi/pi-agent-core": "15.10.0",
53
+ "@oh-my-pi/pi-ai": "15.10.0",
54
+ "@oh-my-pi/pi-mnemopi": "15.10.0",
55
+ "@oh-my-pi/pi-natives": "15.10.0",
56
+ "@oh-my-pi/pi-tui": "15.10.0",
57
+ "@oh-my-pi/pi-utils": "15.10.0",
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",
@@ -0,0 +1,42 @@
1
+ #!/bin/sh
2
+ # Dev launcher for the omp CLI, installed by `bun run install:dev`.
3
+ #
4
+ # Problem it solves: Bun reads `bunfig.toml` from the *current working
5
+ # directory* at startup and evaluates its `preload` entries before running the
6
+ # script. A bun-shebang bin (what `bun link` creates for `src/cli.ts`)
7
+ # therefore inherits whatever `preload` the directory you happen to be in
8
+ # declares. Running `omp`/`pi` inside an unrelated Bun project can execute — and
9
+ # crash on — that project's preload, e.g.
10
+ # error: Cannot find module '@v12sh/utils/frontmatter' from '.../loader.ts'
11
+ #
12
+ # Bun only reads the *exact* cwd (it does not walk parents) and ignores
13
+ # `--config`/`BUN_BE_BUN` for this, so the fix is to launch Bun from an empty,
14
+ # bunfig-free directory and restore the real cwd inside the process via the
15
+ # preload shim alongside this file.
16
+ set -e
17
+
18
+ # Resolve this script's real location even when invoked through a symlink
19
+ # (`$HOME/.bun/bin/omp` -> this file).
20
+ self=$0
21
+ while [ -L "$self" ]; do
22
+ link=$(readlink "$self")
23
+ case $link in
24
+ /*) self=$link ;;
25
+ *) self=$(dirname "$self")/$link ;;
26
+ esac
27
+ done
28
+ scripts_dir=$(CDPATH= cd -- "$(dirname -- "$self")" && pwd -P)
29
+ cli=$scripts_dir/../src/cli.ts
30
+ preload=$scripts_dir/dev-launch-preload.ts
31
+ timing_preload=$scripts_dir/../../utils/src/module-timer.ts
32
+
33
+ launch_dir=${OMP_DEV_LAUNCH_DIR:-${HOME}/.omp/.dev-cwd}
34
+ mkdir -p "$launch_dir"
35
+
36
+ OMP_LAUNCH_CWD=$PWD
37
+ export OMP_LAUNCH_CWD
38
+ cd "$launch_dir"
39
+ if [ -n "${PI_TIMING:-}" ]; then
40
+ exec bun --preload "$preload" --preload "$timing_preload" "$cli" "$@"
41
+ fi
42
+ exec bun --preload "$preload" "$cli" "$@"
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bun `--preload` shim for the omp dev launcher (`scripts/dev-launch`).
3
+ *
4
+ * The launcher starts Bun from an empty, bunfig-free directory so a foreign
5
+ * project's `bunfig.toml` `preload` cannot run inside the omp CLI: Bun reads
6
+ * `bunfig.toml` from the *current working directory* on startup and evaluates
7
+ * its `preload` entries before the entrypoint, so a bun-shebang bin inherits
8
+ * whatever `preload` the directory you launched from declares (and crashes if
9
+ * that preload can't resolve). This shim is loaded before the entrypoint's
10
+ * imports run, so it restores the user's real working directory in time for
11
+ * import-time snapshots (e.g. `getProjectDir()` in `@oh-my-pi/pi-utils/dirs`).
12
+ */
13
+ const launchCwd = process.env.OMP_LAUNCH_CWD;
14
+ if (launchCwd) {
15
+ delete process.env.OMP_LAUNCH_CWD;
16
+ try {
17
+ process.chdir(launchCwd);
18
+ } catch {}
19
+ }
@@ -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
  }
package/src/cli/args.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI argument parsing and help display
3
3
  */
4
- import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
4
+ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai/effort";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
@@ -284,7 +284,7 @@ export function getExtraHelpText(): string {
284
284
  ${chalk.dim("# Search & Tools")}
285
285
  EXA_API_KEY - Exa web search
286
286
  BRAVE_API_KEY - Brave web search
287
- PERPLEXITY_API_KEY - Perplexity web search (API)
287
+ PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
288
288
  PERPLEXITY_COOKIES - Perplexity web search (session cookie)
289
289
  TAVILY_API_KEY - Tavily web search
290
290
  ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
@@ -380,6 +380,18 @@ function isMessagesRequest(message: ParsedHttpMessage): boolean {
380
380
  return pathNameFromRequestTarget(message.path ?? "") === "/v1/messages";
381
381
  }
382
382
 
383
+ // Claude Code fires a background warmup/classification call on its small fast
384
+ // model (a haiku variant, ANTHROPIC_SMALL_FAST_MODEL) before sending the user's
385
+ // real message. Skip it so the capture lands on the actual prompt.
386
+ function isBackgroundModelRequest(message: ParsedHttpMessage): boolean {
387
+ try {
388
+ const parsed = JSON.parse(decodeBody(message.headers, message.body)) as { model?: unknown };
389
+ return typeof parsed.model === "string" && parsed.model.toLowerCase().includes("haiku");
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+
383
395
  function decodeBody(headers: readonly HeaderEntry[], body: Buffer): string {
384
396
  const encoding = headerValue(headers, "content-encoding")?.toLowerCase().trim();
385
397
  try {
@@ -636,7 +648,7 @@ export class ClaudeMessagesProxy {
636
648
  upstreamTls.write(data);
637
649
  const messages = requestParser.push(data);
638
650
  for (const message of messages) {
639
- if (!isMessagesRequest(message)) {
651
+ if (!isMessagesRequest(message) || isBackgroundModelRequest(message)) {
640
652
  responseQueue.push(null);
641
653
  continue;
642
654
  }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * `omp gallery` — render every built-in tool's renderer across its lifecycle.
3
+ *
4
+ * For each tool with a registered renderer, the gallery drives a real
5
+ * {@link ToolExecutionComponent} through four states — streaming arguments,
6
+ * arguments complete (in progress), success, and failure — and prints the
7
+ * rendered output to stdout. It exists for visual QA of tool renderers without
8
+ * having to provoke each state through a live agent session.
9
+ */
10
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
+ import type { TUI } from "@oh-my-pi/pi-tui";
12
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
13
+ import { Settings } from "../config/settings";
14
+ import { ToolExecutionComponent } from "../modes/components/tool-execution";
15
+ import { initTheme, theme } from "../modes/theme/theme";
16
+ import { toolRenderers } from "../tools/renderers";
17
+ import { type GalleryFixture, type GalleryResult, galleryFixtures } from "./gallery-fixtures";
18
+ import { captureGalleryScreenshots } from "./gallery-screenshot";
19
+
20
+ /** Lifecycle states the gallery renders, in display order. */
21
+ export const GALLERY_STATES = ["streaming", "progress", "success", "error"] as const;
22
+ export type GalleryState = (typeof GALLERY_STATES)[number];
23
+
24
+ const STATE_LABELS: Record<GalleryState, string> = {
25
+ streaming: "streaming args",
26
+ progress: "in progress",
27
+ success: "done",
28
+ error: "failed",
29
+ };
30
+
31
+ export interface GalleryCommandArgs {
32
+ /** Render width in columns (defaults to terminal width, clamped). */
33
+ width?: number;
34
+ /** Restrict to a single tool name. */
35
+ tool?: string;
36
+ /** Restrict to specific lifecycle states. */
37
+ states?: GalleryState[];
38
+ /** Render the expanded variant of each renderer. */
39
+ expanded?: boolean;
40
+ /** Strip ANSI styling from the output (useful when redirecting to a file). */
41
+ plain?: boolean;
42
+ /** Capture the rendered gallery as PNG screenshot(s) via VHS instead of printing ANSI. */
43
+ screenshot?: boolean;
44
+ /** Screenshot output path (single image) or base path (suffixed when split across images). */
45
+ out?: string;
46
+ /** Font family for screenshots (must be installed; Nerd Font recommended for icon glyphs). */
47
+ font?: string;
48
+ /** Font size in points for screenshots. */
49
+ fontSize?: number;
50
+ }
51
+
52
+ /** One tool's rendered lifecycle, as ANSI lines: a leading blank, the section rule, then each state. */
53
+ export interface GallerySection {
54
+ heading: string;
55
+ lines: string[];
56
+ }
57
+
58
+ const GENERIC_ERROR: GalleryResult = {
59
+ content: [{ type: "text", text: "Error: operation failed" }],
60
+ isError: true,
61
+ };
62
+
63
+ /**
64
+ * Build the fake `AgentTool` the component needs for its label, edit mode, and —
65
+ * for `customRendered` fixtures — the renderer functions that route it through
66
+ * the same custom-tool branch production uses (see {@link GalleryFixture}).
67
+ */
68
+ function fakeToolFor(name: string, fixture: GalleryFixture | undefined): AgentTool | undefined {
69
+ if (!fixture?.label && !fixture?.editMode && !fixture?.customRendered) return undefined;
70
+ const tool: Record<string, unknown> = { name, label: fixture.label ?? name, mode: fixture.editMode };
71
+ if (fixture.customRendered) {
72
+ const renderer = toolRenderers[name] as
73
+ | { renderCall?: unknown; renderResult?: unknown; mergeCallAndResult?: unknown; inline?: unknown }
74
+ | undefined;
75
+ if (renderer) {
76
+ tool.renderCall = renderer.renderCall;
77
+ tool.renderResult = renderer.renderResult;
78
+ tool.mergeCallAndResult = renderer.mergeCallAndResult;
79
+ tool.inline = renderer.inline;
80
+ }
81
+ }
82
+ return tool as unknown as AgentTool;
83
+ }
84
+
85
+ /** The curated fixture for a tool, or a generic one for registry tools lacking sample data. */
86
+ export function resolveFixture(name: string): GalleryFixture {
87
+ return (
88
+ galleryFixtures[name] ??
89
+ ({
90
+ args: { note: `sample ${name} call` },
91
+ result: { content: [{ type: "text", text: `${name} completed` }] },
92
+ } satisfies GalleryFixture)
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Render a single tool/state pair to lines. Builds a fresh component, drives it
98
+ * to the requested state, settles any async edit preview, then snapshots the
99
+ * render and stops all animation timers.
100
+ */
101
+ export async function renderGalleryState(
102
+ name: string,
103
+ fixture: GalleryFixture,
104
+ state: GalleryState,
105
+ width: number,
106
+ expanded = false,
107
+ ): Promise<string[]> {
108
+ const tool = fakeToolFor(name, fixture);
109
+ const streamingArgs = state === "streaming" ? (fixture.streamingArgs ?? fixture.args) : fixture.args;
110
+ // The component only calls `requestRender` during a static render;
111
+ // `imageBudget` is consulted solely when images render, which the gallery
112
+ // disables. A cast avoids constructing a real terminal.
113
+ const ui = { requestRender() {} } as unknown as TUI;
114
+ const component = new ToolExecutionComponent(name, streamingArgs, { showImages: false }, tool, ui, getProjectDir());
115
+ component.setExpanded(expanded);
116
+
117
+ if (state !== "streaming") {
118
+ component.setArgsComplete();
119
+ }
120
+ if (state === "success") {
121
+ component.updateResult(fixture.result, false);
122
+ } else if (state === "error") {
123
+ component.updateResult(fixture.errorResult ?? GENERIC_ERROR, false);
124
+ }
125
+
126
+ // Edit-like renderers compute their diff preview off the render path; wait
127
+ // for it to settle so the snapshot is deterministic instead of racing a tick.
128
+ await component.whenPreviewSettled();
129
+
130
+ const lines = component.render(width);
131
+ component.stopAnimation();
132
+ return lines;
133
+ }
134
+
135
+ function resolveWidth(requested: number | undefined): number {
136
+ const fallback = process.stdout.columns ?? 100;
137
+ const width = requested ?? fallback;
138
+ return Math.max(40, Math.min(200, width));
139
+ }
140
+
141
+ function sectionRule(label: string, width: number): string {
142
+ const prefix = `── ${label} `;
143
+ const fill = Math.max(0, width - prefix.length);
144
+ return theme.fg("accent", theme.bold(`${prefix}${"─".repeat(fill)}`));
145
+ }
146
+
147
+ /**
148
+ * Render each requested tool's lifecycle into ANSI section blocks. The block
149
+ * layout (leading blank, section rule, then a blank + dim label + body per
150
+ * state) is shared by the stdout and screenshot paths so both stay identical.
151
+ */
152
+ async function renderGallerySections(
153
+ names: string[],
154
+ states: GalleryState[],
155
+ width: number,
156
+ expanded: boolean,
157
+ ): Promise<GallerySection[]> {
158
+ const sections: GallerySection[] = [];
159
+ for (const name of names) {
160
+ const fixture = resolveFixture(name);
161
+ const heading = fixture.label && fixture.label !== name ? `${name} — ${fixture.label}` : name;
162
+ const lines: string[] = ["", sectionRule(heading, width)];
163
+ for (const state of states) {
164
+ lines.push("", theme.fg("dim", ` · ${STATE_LABELS[state]}`));
165
+ try {
166
+ for (const line of await renderGalleryState(name, fixture, state, width, expanded)) lines.push(line);
167
+ } catch (err) {
168
+ lines.push(theme.fg("error", ` render failed: ${String(err)}`));
169
+ }
170
+ }
171
+ sections.push({ heading, lines });
172
+ }
173
+ return sections;
174
+ }
175
+
176
+ /**
177
+ * Render the gallery. Iterates the renderer registry (or a single tool),
178
+ * printing each requested lifecycle state under a labeled section — or, with
179
+ * `screenshot`, capturing the rendered output as PNG(s) via VHS.
180
+ */
181
+ export async function runGalleryCommand(args: GalleryCommandArgs): Promise<void> {
182
+ const settingsInstance = await Settings.init();
183
+ // Screenshots must carry exact theme RGB regardless of how the invoking
184
+ // terminal advertises its color support, so force truecolor before the theme
185
+ // (and therefore every SGR escape it emits) is built.
186
+ if (args.screenshot) process.env.COLORTERM = "truecolor";
187
+ await initTheme(
188
+ false,
189
+ settingsInstance.get("symbolPreset"),
190
+ settingsInstance.get("colorBlindMode"),
191
+ settingsInstance.get("theme.dark"),
192
+ settingsInstance.get("theme.light"),
193
+ );
194
+
195
+ const width = resolveWidth(args.width);
196
+ const expanded = args.expanded ?? false;
197
+ const states = args.states && args.states.length > 0 ? args.states : [...GALLERY_STATES];
198
+
199
+ const allNames = Object.keys(toolRenderers).sort();
200
+ const names = args.tool ? allNames.filter(name => name === args.tool) : allNames;
201
+ if (args.tool && names.length === 0) {
202
+ process.stdout.write(`Unknown tool '${args.tool}'. Known tools: ${allNames.join(", ")}\n`);
203
+ return;
204
+ }
205
+
206
+ const sections = await renderGallerySections(names, states, width, expanded);
207
+
208
+ if (args.screenshot) {
209
+ const paths = await captureGalleryScreenshots(sections, {
210
+ width,
211
+ font: args.font,
212
+ fontSize: args.fontSize,
213
+ out: args.out,
214
+ });
215
+ process.stdout.write(`${paths.join("\n")}\n`);
216
+ return;
217
+ }
218
+
219
+ const lines = sections.flatMap(section => section.lines);
220
+ lines.push("");
221
+ const text = lines.map(line => (args.plain ? Bun.stripANSI(line) : line)).join("\n");
222
+ process.stdout.write(`${text}\n`);
223
+ }