@oh-my-pi/pi-coding-agent 15.9.67 → 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 (128) hide show
  1. package/CHANGELOG.md +63 -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 +6 -1
  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 +32 -6
  22. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  23. package/dist/types/lsp/types.d.ts +10 -0
  24. package/dist/types/main.d.ts +3 -2
  25. package/dist/types/memory-backend/index.d.ts +2 -1
  26. package/dist/types/memory-backend/resolve.d.ts +1 -1
  27. package/dist/types/memory-backend/types.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  29. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/index.d.ts +5 -4
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/setup-version.d.ts +11 -0
  34. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  36. package/dist/types/modes/types.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/task/executor.d.ts +7 -0
  39. package/dist/types/telemetry-export.d.ts +1 -1
  40. package/dist/types/tools/eval-render.d.ts +1 -8
  41. package/dist/types/tools/fetch.d.ts +15 -7
  42. package/dist/types/tools/render-utils.d.ts +8 -0
  43. package/dist/types/tools/renderers.d.ts +16 -2
  44. package/dist/types/tools/search.d.ts +1 -1
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/web/scrapers/github.d.ts +22 -0
  47. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  48. package/dist/types/web/search/types.d.ts +1 -1
  49. package/package.json +9 -9
  50. package/scripts/dev-launch +42 -0
  51. package/scripts/dev-launch-preload.ts +19 -0
  52. package/src/cli/args.ts +2 -2
  53. package/src/cli/gallery-cli.ts +223 -0
  54. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  56. package/src/cli/gallery-fixtures/edit.ts +194 -0
  57. package/src/cli/gallery-fixtures/fs.ts +153 -0
  58. package/src/cli/gallery-fixtures/index.ts +40 -0
  59. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  60. package/src/cli/gallery-fixtures/memory.ts +81 -0
  61. package/src/cli/gallery-fixtures/misc.ts +221 -0
  62. package/src/cli/gallery-fixtures/search.ts +213 -0
  63. package/src/cli/gallery-fixtures/shell.ts +167 -0
  64. package/src/cli/gallery-fixtures/types.ts +41 -0
  65. package/src/cli/gallery-fixtures/web.ts +158 -0
  66. package/src/cli/gallery-screenshot.ts +279 -0
  67. package/src/cli-commands.ts +1 -0
  68. package/src/commands/gallery.ts +52 -0
  69. package/src/commands/launch.ts +1 -1
  70. package/src/config/keybindings.ts +15 -6
  71. package/src/config/model-equivalence.ts +35 -12
  72. package/src/config/model-id-affixes.ts +39 -22
  73. package/src/config/model-registry.ts +16 -16
  74. package/src/config/settings-schema.ts +18 -5
  75. package/src/config/settings.ts +11 -0
  76. package/src/dap/client.ts +14 -16
  77. package/src/edit/renderer.ts +36 -48
  78. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  79. package/src/eval/agent-bridge.ts +34 -7
  80. package/src/extensibility/extensions/runner.ts +1 -0
  81. package/src/extensibility/plugins/doctor.ts +0 -1
  82. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  83. package/src/goals/tools/goal-tool.ts +2 -2
  84. package/src/internal-urls/docs-index.generated.ts +5 -5
  85. package/src/lsp/client.ts +104 -55
  86. package/src/lsp/types.ts +10 -0
  87. package/src/main.ts +44 -49
  88. package/src/memory-backend/index.ts +13 -1
  89. package/src/memory-backend/resolve.ts +3 -5
  90. package/src/memory-backend/types.ts +1 -1
  91. package/src/modes/components/custom-editor.ts +10 -1
  92. package/src/modes/components/status-line.ts +3 -5
  93. package/src/modes/components/tool-execution.ts +61 -16
  94. package/src/modes/controllers/command-controller.ts +13 -2
  95. package/src/modes/controllers/input-controller.ts +11 -3
  96. package/src/modes/controllers/selector-controller.ts +2 -2
  97. package/src/modes/index.ts +5 -4
  98. package/src/modes/interactive-mode.ts +17 -3
  99. package/src/modes/setup-version.ts +11 -0
  100. package/src/modes/setup-wizard/index.ts +3 -2
  101. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/context-usage.ts +10 -6
  104. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  105. package/src/sdk.ts +21 -23
  106. package/src/session/agent-session.ts +7 -7
  107. package/src/slash-commands/builtin-registry.ts +1 -1
  108. package/src/slash-commands/helpers/usage-report.ts +2 -0
  109. package/src/task/executor.ts +20 -2
  110. package/src/task/render.ts +1 -2
  111. package/src/telemetry-export.ts +25 -7
  112. package/src/tools/eval-backends.ts +6 -17
  113. package/src/tools/eval-render.ts +21 -18
  114. package/src/tools/eval.ts +5 -4
  115. package/src/tools/fetch.ts +94 -84
  116. package/src/tools/render-utils.ts +17 -3
  117. package/src/tools/renderers.ts +16 -1
  118. package/src/tools/report-tool-issue.ts +1 -1
  119. package/src/tools/search.ts +173 -81
  120. package/src/tools/todo.ts +20 -7
  121. package/src/tools/write.ts +22 -1
  122. package/src/web/scrapers/github.ts +255 -3
  123. package/src/web/scrapers/youtube.ts +3 -2
  124. package/src/web/search/providers/perplexity.ts +199 -51
  125. package/src/web/search/render.ts +39 -54
  126. package/src/web/search/types.ts +5 -1
  127. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  128. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -2,7 +2,8 @@ import type { SetupSceneHost, SetupTab } from "./types";
2
2
  /**
3
3
  * "Web search" panel: picks the provider the web_search tool should prefer and
4
4
  * reports whether the highlighted provider is ready to use given current
5
- * credentials (env keys or OAuth sign-ins from the Sign in tab).
5
+ * credentials (env keys or OAuth sign-ins from the Sign in tab) or an
6
+ * unauthenticated fallback.
6
7
  */
7
8
  export declare class WebSearchTab implements SetupTab {
8
9
  #private;
@@ -241,7 +241,7 @@ export interface InteractiveModeContext {
241
241
  handleSessionDeleteCommand(): Promise<void>;
242
242
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
243
243
  showHookConfirm(title: string, message: string): Promise<boolean>;
244
- showDebugSelector(): void;
244
+ showDebugSelector(): Promise<void>;
245
245
  showSessionObserver(): void;
246
246
  resetObserverRegistry(): void;
247
247
  handleCtrlC(): void;
@@ -13,7 +13,7 @@ import { type FileSlashCommand } from "./extensibility/slash-commands";
13
13
  import type { HindsightSessionState } from "./hindsight/state";
14
14
  import { type LocalProtocolOptions } from "./internal-urls";
15
15
  import { MCPManager, type MCPToolsLoadResult } from "./mcp";
16
- import { type MnemopiSessionState } from "./mnemopi/state";
16
+ import type { MnemopiSessionState } from "./mnemopi/state";
17
17
  import { AgentRegistry } from "./registry/agent-registry";
18
18
  import { AgentSession } from "./session/agent-session";
19
19
  import { AuthStorage } from "./session/auth-storage";
@@ -48,6 +48,13 @@ export interface ExecutorOptions {
48
48
  outputSchema?: unknown;
49
49
  /** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
50
50
  taskDepth?: number;
51
+ /**
52
+ * Override the `task.maxRuntimeMs` wall-clock cap for this run. When provided
53
+ * it wins over the settings value; `0` disables the per-subagent wall-clock
54
+ * limit entirely. Used by the eval `agent()` bridge, whose parent cell
55
+ * watchdog is already suspended for the call's duration.
56
+ */
57
+ maxRuntimeMs?: number;
51
58
  enableLsp?: boolean;
52
59
  signal?: AbortSignal;
53
60
  onProgress?: (progress: AgentProgress) => void;
@@ -10,7 +10,7 @@ export declare function isTelemetryExportEnabled(): boolean;
10
10
  * the OTEL kill-switches are engaged), so it is safe to call unconditionally at
11
11
  * startup.
12
12
  */
13
- export declare function initTelemetryExport(): void;
13
+ export declare function initTelemetryExport(): Promise<void>;
14
14
  /**
15
15
  * Flush any buffered spans to the exporter. No-op when export is disabled.
16
16
  * Hosts embedding the agent can call this at natural boundaries (e.g. the end
@@ -13,14 +13,6 @@ 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;
24
16
  interface EvalRenderCellArg {
25
17
  language?: string;
26
18
  code?: string;
@@ -54,6 +46,7 @@ export declare const evalToolRenderer: {
54
46
  }, options: RenderResultOptions & {
55
47
  renderContext?: EvalRenderContext;
56
48
  }, uiTheme: Theme, _args?: EvalRenderArgs): Component;
49
+ isStreamingPreviewAppendOnly(_args: EvalRenderArgs, _options: RenderResultOptions, result?: unknown): boolean;
57
50
  mergeCallAndResult: boolean;
58
51
  inline: boolean;
59
52
  };
@@ -17,14 +17,22 @@ export interface ParsedReadUrlTarget {
17
17
  ranges?: readonly LineRange[];
18
18
  }
19
19
  export declare function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null;
20
+ /** Reader backends for {@link renderHtmlToText}, in default priority order. */
21
+ export type FetchProvider = "native" | "trafilatura" | "lynx" | "parallel" | "jina";
20
22
  /**
21
- * Render HTML to markdown using Parallel, jina, trafilatura, lynx, then the
22
- * in-process native converter. The overall `timeout` budget bounds the call,
23
- * but remote reader requests are additionally capped at `REMOTE_READER_MAX_MS`
24
- * so that a hung remote endpoint cannot prevent local fallbacks from running.
25
- * Only a real `userSignal` cancellation aborts the chain remote per-attempt
26
- * timeouts and the overall reader-mode timeout still allow later renderers
27
- * (especially the purely-local native converter) to be tried.
23
+ * Render HTML to markdown by trying reader backends in priority order: native
24
+ * (in-process), trafilatura, lynx, Parallel, then Jina. The `providers.fetch`
25
+ * setting picks the order `auto` uses the default above; any specific backend
26
+ * is tried first, then the remaining backends as fallbacks. Every backend's
27
+ * output must clear the same quality gate (>100 non-whitespace chars and not
28
+ * {@link isLowQualityOutput}) before it is accepted, otherwise the next backend
29
+ * is tried.
30
+ *
31
+ * The overall `timeout` budget bounds the whole call; remote backends (Parallel,
32
+ * Jina) are additionally capped at `REMOTE_READER_MAX_MS` so a hung endpoint
33
+ * cannot starve later renderers — especially the purely-local native converter,
34
+ * which always works on already-loaded HTML. Only a real `userSignal`
35
+ * cancellation aborts the chain (#1449).
28
36
  */
29
37
  export declare function renderHtmlToText(url: string, html: string, timeout: number, settings: Settings, userSignal: AbortSignal | undefined, storage: AgentStorage | null): Promise<{
30
38
  content: string;
@@ -103,6 +103,14 @@ export declare function capPreviewLines(lines: string[], theme: Theme, options?:
103
103
  }): string[];
104
104
  export declare function formatMeta(meta: string[], theme: Theme): string;
105
105
  export declare function formatErrorMessage(message: string | undefined, theme: Theme): string;
106
+ /**
107
+ * Error message rendered as a subordinate detail line beneath a status header
108
+ * that already carries the error icon (e.g. `✘ Write: <path>`). The header's
109
+ * icon already signals failure, so this omits the redundant error symbol and
110
+ * "Error:" prefix that `formatErrorMessage` adds for standalone single-line
111
+ * errors, indenting two columns to sit under the header title instead.
112
+ */
113
+ export declare function formatErrorDetail(message: string | undefined, theme: Theme): string;
106
114
  export declare function formatEmptyMessage(message: string, theme: Theme): string;
107
115
  export type CodeFrameMarker = "" | " " | "*" | "+" | "-" | ">";
108
116
  export declare function formatCodeFrameLine(marker: CodeFrameMarker, lineNumber: string | number, content: string, lineNumberWidth: number): string;
@@ -6,7 +6,7 @@
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
- type ToolRenderer = {
9
+ export type ToolRenderer = {
10
10
  renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
11
11
  renderResult: (result: {
12
12
  content: Array<{
@@ -19,8 +19,22 @@ type ToolRenderer = {
19
19
  renderContext?: Record<string, unknown>;
20
20
  }, theme: Theme, args?: unknown) => Component;
21
21
  mergeCallAndResult?: boolean;
22
+ /**
23
+ * While a tool's preview is still streaming, report whether the
24
+ * currently-rendered preview is append-only: its rows only grow at the bottom
25
+ * and never re-layout above the bottom live region (a full, top-anchored
26
+ * content/code preview). The transcript reports this up to the TUI so a
27
+ * streaming preview taller than the viewport commits its scrolled-off head to
28
+ * native scrollback instead of dropping it (see
29
+ * `ToolExecutionComponent.isTranscriptBlockAppendOnly`). `result` is the
30
+ * latest (possibly partial) tool result, or `undefined` before one exists —
31
+ * `eval`/`bash` use its presence to defer committing until the streamed input
32
+ * (code) has finalized. Omit (or return `false`) for previews that slide a
33
+ * tail window or later collapse to a compact result — committing their head
34
+ * would strand stale rows.
35
+ */
36
+ isStreamingPreviewAppendOnly?: (args: unknown, options: RenderResultOptions, result?: unknown) => boolean;
22
37
  /** Render without background box, inline in the response flow */
23
38
  inline?: boolean;
24
39
  };
25
40
  export declare const toolRenderers: Record<string, ToolRenderer>;
26
- export {};
@@ -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
  };
@@ -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.67",
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.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",
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
+ }
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)
@@ -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
+ }