@oh-my-pi/pi-coding-agent 16.1.10 → 16.1.11

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 (58) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli.js +2811 -2812
  3. package/dist/types/export/html/index.d.ts +31 -2
  4. package/dist/types/export/html/web-palette.d.ts +117 -0
  5. package/dist/types/hindsight/content.d.ts +7 -0
  6. package/dist/types/hindsight/transcript.d.ts +1 -1
  7. package/dist/types/modes/components/hook-editor.d.ts +2 -1
  8. package/dist/types/modes/interactive-mode.d.ts +5 -0
  9. package/dist/types/modes/types.d.ts +17 -0
  10. package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
  11. package/dist/types/session/agent-session.d.ts +4 -0
  12. package/dist/types/session/session-context.d.ts +2 -0
  13. package/dist/types/session/session-entries.d.ts +6 -0
  14. package/dist/types/session/session-manager.d.ts +2 -1
  15. package/dist/types/tools/acp-bridge.d.ts +29 -0
  16. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +7 -0
  17. package/dist/types/tools/browser/tab-worker.d.ts +20 -0
  18. package/dist/types/utils/image-loading.d.ts +4 -3
  19. package/dist/types/utils/jj.d.ts +25 -0
  20. package/dist/types/utils/title-generator.d.ts +0 -2
  21. package/package.json +12 -12
  22. package/scripts/generate-share-viewer.ts +4 -2
  23. package/src/autoresearch/git.ts +12 -0
  24. package/src/discovery/opencode.ts +47 -4
  25. package/src/edit/hashline/filesystem.ts +8 -0
  26. package/src/edit/modes/patch.ts +18 -2
  27. package/src/edit/modes/replace.ts +13 -10
  28. package/src/export/html/index.ts +50 -8
  29. package/src/export/html/web-palette.ts +142 -0
  30. package/src/hindsight/backend.ts +4 -4
  31. package/src/hindsight/content.ts +17 -1
  32. package/src/hindsight/transcript.ts +2 -2
  33. package/src/internal-urls/docs-index.generated.txt +1 -1
  34. package/src/modes/components/agent-dashboard.ts +8 -8
  35. package/src/modes/components/hook-editor.ts +13 -10
  36. package/src/modes/components/session-selector.ts +3 -0
  37. package/src/modes/controllers/event-controller.ts +9 -5
  38. package/src/modes/controllers/extension-ui-controller.ts +6 -2
  39. package/src/modes/interactive-mode.ts +69 -29
  40. package/src/modes/theme/dark.json +1 -1
  41. package/src/modes/types.ts +18 -0
  42. package/src/modes/utils/keybinding-matchers.ts +36 -1
  43. package/src/modes/utils/ui-helpers.ts +1 -0
  44. package/src/prompts/tools/browser.md +3 -2
  45. package/src/sdk.ts +12 -1
  46. package/src/session/agent-session.ts +38 -14
  47. package/src/session/session-context.ts +5 -0
  48. package/src/session/session-entries.ts +6 -0
  49. package/src/session/session-manager.ts +3 -1
  50. package/src/task/worktree.ts +12 -4
  51. package/src/thinking.ts +1 -1
  52. package/src/tools/acp-bridge.ts +66 -0
  53. package/src/tools/browser/cmux/cmux-tab.ts +37 -0
  54. package/src/tools/browser/tab-worker.ts +160 -37
  55. package/src/tools/write.ts +2 -25
  56. package/src/utils/image-loading.ts +6 -3
  57. package/src/utils/jj.ts +47 -0
  58. package/src/utils/title-generator.ts +31 -99
@@ -5,12 +5,41 @@ import { SessionManager } from "../../session/session-manager";
5
5
  export declare function getTemplate(): string;
6
6
  export interface ExportOptions {
7
7
  outputPath?: string;
8
+ /**
9
+ * Which color palette the export ships with.
10
+ * - `"web"` (default) — the omp brand identity (collab-web pink/purple),
11
+ * so public HTML exports and the `/s/<id>` share viewer match the live
12
+ * `my.omp.sh` client. See `web-palette.ts`.
13
+ * - `"theme"` — derive from `themeName` (or the active TUI theme), preserving
14
+ * the pre-15.12 behavior where an export mirrored the user's terminal.
15
+ */
16
+ palette?: "web" | "theme";
17
+ /**
18
+ * TUI theme to derive colors from when `palette: "theme"`. Ignored for the
19
+ * default `"web"` palette. Resolves to the active TUI theme when omitted.
20
+ */
8
21
  themeName?: string;
9
22
  /** Embed subagent session transcripts found next to the session file (default true). */
10
23
  includeSubSessions?: boolean;
11
24
  }
12
- /** Generate CSS custom properties for theme. Exported for the share-viewer build script. */
13
- export declare function generateThemeVars(themeName?: string): Promise<string>;
25
+ /**
26
+ * Generate CSS custom properties for the export `:root`.
27
+ *
28
+ * Two call shapes:
29
+ * • `generateThemeVars("web" | "theme", themeName?)` — explicit palette.
30
+ * `"web"` (the default for public artifacts) returns the fixed omp brand
31
+ * palette from `web-palette.ts` — collab-web pink/purple identity, shared
32
+ * with the live `my.omp.sh` client, so exports and the share viewer render
33
+ * identically to it. `"theme"` derives from the TUI theme via
34
+ * `getResolvedThemeColors(themeName)` plus the three
35
+ * `export.{pageBg,cardBg,infoBg}` surface overrides.
36
+ * • `generateThemeVars(themeName)` — legacy single-arg form: derive from the
37
+ * named TUI theme. Kept so existing callers (and the theme-islight test)
38
+ * keep working; equivalent to `generateThemeVars("theme", themeName)`.
39
+ *
40
+ * Exported for the share-viewer build script.
41
+ */
42
+ export declare function generateThemeVars(palette?: "web" | "theme" | (string & {}), themeName?: string): Promise<string>;
14
43
  /** Embedded subagent session transcript, keyed by slash-joined agent path in `SessionData.subSessions`. */
15
44
  export interface SubSession {
16
45
  /** Bare agent id (session file stem), e.g. "ToolAsk". */
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Web/export palette — the omp brand identity shared by the collab-web live
3
+ * client (`my.omp.sh/`) and every public HTML export / share viewer (`/s/<id>`).
4
+ *
5
+ * Why this exists separately from `modes/theme/dark.json`: the `dark` theme is
6
+ * the **default TUI theme** — its amber accent (`#febc38`) drives the terminal
7
+ * status line, syntax highlighting, thinking levels, and bash/python mode
8
+ * colors for every omp user. The public web artifacts want the collab-web
9
+ * pink/purple identity instead, so they pin this palette rather than inheriting
10
+ * the TUI's. Editing `dark.json` to repurpose it for the web would repaint
11
+ * every terminal; this file keeps the two surfaces decoupled.
12
+ *
13
+ * Token layout — emitted as CSS custom properties on `:root`:
14
+ * • Legacy export names consumed by `template.css` / `template.js`
15
+ * (`--text`, `--body-bg`, `--container-bg`, `--info-bg`, `--accent`,
16
+ * `--border`, `--success`, `--error`, `--warning`, `--muted`, `--dim`,
17
+ * `--borderAccent`, `--selectedBg`, `--userMessageBg`, `--customMessageBg`,
18
+ * `--customMessageLabel`, `--mdHeading`, `--mdLink`, `--mdCode`,
19
+ * `--mdListBullet`, `--toolOutput`, `--thinkingText`, syntax*, …).
20
+ * • collab-web-native aliases consumed by the `tv-` tool-render bridge
21
+ * (`tool-render.css`: `var(--bg-inset, …)`, `var(--fg, …)`, …) so embedded
22
+ * tool cards resolve to the *real* collab-web tokens and render
23
+ * pixel-identical to the live client.
24
+ *
25
+ * Alpha-bearing tokens (`--border`, `--ring`, `--accent-muted`, …) keep their
26
+ * `oklch(… / N%)` form — flattening them to opaque hex would produce harsh
27
+ * white borders and non-matching translucent focus rings. Opaque surfaces are
28
+ * sRGB hex (the collab-web `tokens.css` OKLCH dark-theme tokens converted via
29
+ * the standard OKLab→linear-sRGB→gamma path); if the live client palette
30
+ * changes, regenerate those from there.
31
+ */
32
+ export declare const WEB_EXPORT_PALETTE: {
33
+ readonly "--bg": "#0f0b14";
34
+ readonly "--bg-raised": "#16111c";
35
+ readonly "--bg-inset": "#09060c";
36
+ readonly "--bg-overlay": "#211b28";
37
+ readonly "--fg": "#e6e3ea";
38
+ readonly "--fg-muted": "#a49faa";
39
+ readonly "--fg-faint": "#6e6974";
40
+ readonly "--accent": "#ed4abf";
41
+ readonly "--accent-muted": "oklch(0.674 0.23 341 / 18%)";
42
+ readonly "--ok": "#68ca80";
43
+ readonly "--err": "#f05653";
44
+ readonly "--warn": "#e4b33f";
45
+ readonly "--ring": "oklch(0.817 0.112 205 / 70%)";
46
+ readonly "--font-mono": 'ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, Consolas, monospace';
47
+ readonly "--body-bg": "#0f0b14";
48
+ readonly "--container-bg": "#16111c";
49
+ readonly "--info-bg": "#09060c";
50
+ readonly "--text": "#e6e3ea";
51
+ readonly "--muted": "#a49faa";
52
+ readonly "--dim": "#6e6974";
53
+ readonly "--thinkingText": "#a49faa";
54
+ readonly "--border": "oklch(1 0 0 / 9%)";
55
+ readonly "--borderMuted": "oklch(1 0 0 / 6%)";
56
+ readonly "--borderAccent": "#945ff9";
57
+ readonly "--selectedBg": "#2d2535";
58
+ readonly "--success": "#68ca80";
59
+ readonly "--error": "#f05653";
60
+ readonly "--warning": "#e4b33f";
61
+ readonly "--userMessageBg": "oklch(0.674 0.23 341 / 6%)";
62
+ readonly "--userMessageText": "#e6e3ea";
63
+ readonly "--customMessageBg": "#211b28";
64
+ readonly "--customMessageText": "#a49faa";
65
+ readonly "--customMessageLabel": "#b281d6";
66
+ readonly "--toolPendingBg": "#16111c";
67
+ readonly "--toolSuccessBg": "#09060c";
68
+ readonly "--toolErrorBg": "oklch(0.66 0.19 25 / 14%)";
69
+ readonly "--toolTitle": "#e6e3ea";
70
+ readonly "--toolOutput": "#a49faa";
71
+ readonly "--mdHeading": "#ed4abf";
72
+ readonly "--mdLink": "#5ad8e5";
73
+ readonly "--mdLinkUrl": "#6e6974";
74
+ readonly "--mdCode": "#e6e3ea";
75
+ readonly "--mdCodeBlock": "#e6e3ea";
76
+ readonly "--mdCodeBlockBorder": "oklch(1 0 0 / 9%)";
77
+ readonly "--mdQuote": "#a49faa";
78
+ readonly "--mdQuoteBorder": "oklch(1 0 0 / 13%)";
79
+ readonly "--mdHr": "oklch(1 0 0 / 9%)";
80
+ readonly "--mdListBullet": "#ed4abf";
81
+ readonly "--toolDiffAdded": "#68ca80";
82
+ readonly "--toolDiffRemoved": "#f05653";
83
+ readonly "--toolDiffContext": "#6e6974";
84
+ readonly "--syntaxComment": "#6e6974";
85
+ readonly "--syntaxKeyword": "#945ff9";
86
+ readonly "--syntaxFunction": "#e4b33f";
87
+ readonly "--syntaxVariable": "#5ad8e5";
88
+ readonly "--syntaxString": "#68ca80";
89
+ readonly "--syntaxNumber": "#ed4abf";
90
+ readonly "--syntaxType": "#b281d6";
91
+ readonly "--syntaxOperator": "#e6e3ea";
92
+ readonly "--syntaxPunctuation": "#a49faa";
93
+ readonly "--thinkingOff": "#6e6974";
94
+ readonly "--thinkingMinimal": "#6e6974";
95
+ readonly "--thinkingLow": "#945ff9";
96
+ readonly "--thinkingMedium": "#b281d6";
97
+ readonly "--thinkingHigh": "#ed4abf";
98
+ readonly "--thinkingXhigh": "#e4b33f";
99
+ readonly "--bashMode": "#5ad8e5";
100
+ readonly "--pythonMode": "#e4b33f";
101
+ readonly "--statusLineBg": "#0f0b14";
102
+ readonly "--statusLineSep": "#6e6974";
103
+ readonly "--statusLineModel": "#ed4abf";
104
+ readonly "--statusLinePath": "#5ad8e5";
105
+ readonly "--statusLineGitClean": "#68ca80";
106
+ readonly "--statusLineGitDirty": "#e4b33f";
107
+ readonly "--statusLineContext": "#a49faa";
108
+ readonly "--statusLineSpend": "#5ad8e5";
109
+ readonly "--statusLineStaged": "#68ca80";
110
+ readonly "--statusLineDirty": "#e4b33f";
111
+ readonly "--statusLineUntracked": "#945ff9";
112
+ readonly "--statusLineOutput": "#b281d6";
113
+ readonly "--statusLineCost": "#b281d6";
114
+ readonly "--statusLineSubagents": "#ed4abf";
115
+ };
116
+ /** Serialize the palette as `--key: value;` declarations for `:root { … }`. */
117
+ export declare function webExportThemeVars(): string;
@@ -28,6 +28,13 @@ export interface RecallResultLike {
28
28
  * retaining.
29
29
  */
30
30
  export declare function stripMemoryTags(content: string): string;
31
+ /**
32
+ * True when `content` carries at least one letter or digit. Used by retain
33
+ * and recall paths to drop placeholder assistant turns ("." / "..." / pure
34
+ * whitespace) that would otherwise pollute the bank and waste tokens on
35
+ * embeddings with no semantic content.
36
+ */
37
+ export declare function hasSubstantiveContent(content: string): boolean;
31
38
  /** Format recall results into a bullet list for context injection. */
32
39
  export declare function formatMemories(results: RecallResultLike[]): string;
33
40
  /** Format current UTC time for the recall preamble. */
@@ -7,7 +7,7 @@
7
7
  * surviving message's `TextContent` parts are joined with newlines.
8
8
  */
9
9
  import type { SessionEntry } from "../session/session-entries";
10
- import type { HindsightMessage } from "./content";
10
+ import { type HindsightMessage } from "./content";
11
11
  export interface ReadonlySessionManagerLike {
12
12
  getEntries(): SessionEntry[];
13
13
  }
@@ -3,7 +3,8 @@
3
3
  * Supports Ctrl+G for external editor.
4
4
  *
5
5
  * Two modes:
6
- * - Default (hook): Enter inserts newline, Ctrl+Enter submits, bordered popup
6
+ * - Default (hook): Enter inserts newline, the `app.message.followUp` chord
7
+ * (Ctrl+Q / Ctrl+Enter) submits, bordered popup
7
8
  * - Prompt-style (ask): Enter submits, Shift+Enter inserts newline, legacy ask chrome
8
9
  */
9
10
  import { Container, type TUI } from "@oh-my-pi/pi-tui";
@@ -95,6 +95,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
95
95
  hookWidgetContainerBelow: Container;
96
96
  statusLine: StatusLineComponent;
97
97
  isInitialized: boolean;
98
+ initialChatRendered: boolean;
98
99
  isBashMode: boolean;
99
100
  toolOutputExpanded: boolean;
100
101
  todoExpanded: boolean;
@@ -183,6 +184,10 @@ export declare class InteractiveMode implements InteractiveModeContext {
183
184
  withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: {
184
185
  imageCount?: number;
185
186
  }): Promise<T>;
187
+ clearOptimisticUserMessage(): void;
188
+ replaceOptimisticUserMessage(message: AgentMessage, options?: {
189
+ imageLinks?: readonly (string | undefined)[];
190
+ }): void;
186
191
  startPendingSubmission(input: {
187
192
  text: string;
188
193
  images?: ImageContent[];
@@ -118,6 +118,17 @@ export interface InteractiveModeContext {
118
118
  eventController: EventController;
119
119
  eventBus?: EventBus;
120
120
  isInitialized: boolean;
121
+ /**
122
+ * `true` once `renderInitialMessages` has rendered the session transcript
123
+ * into `chatContainer` at least once.
124
+ *
125
+ * Extension chat-rebuilds (`ExtensionUiController.#applyCustomMessageDisplay`)
126
+ * are gated on this: rebuilding before the initial render would plant a
127
+ * session-derived component into the chat that `renderInitialMessages` then
128
+ * both re-renders from session entries AND re-appends via
129
+ * `preserveExistingChat`, duplicating the message (issue #1955).
130
+ */
131
+ initialChatRendered: boolean;
121
132
  isBashMode: boolean;
122
133
  toolOutputExpanded: boolean;
123
134
  todoExpanded: boolean;
@@ -237,6 +248,12 @@ export interface InteractiveModeContext {
237
248
  withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: {
238
249
  imageCount?: number;
239
250
  }): Promise<T>;
251
+ /** Clears bookkeeping for an optimistic local user message once the matching session event arrives. */
252
+ clearOptimisticUserMessage(): void;
253
+ /** Replaces the raw optimistic user render with the canonical message emitted by the session. */
254
+ replaceOptimisticUserMessage(message: AgentMessage, options?: {
255
+ imageLinks?: readonly (string | undefined)[];
256
+ }): void;
240
257
  isKnownSlashCommand(text: string): boolean;
241
258
  addMessageToChat(message: AgentMessage, options?: {
242
259
  populateHistory?: boolean;
@@ -17,3 +17,16 @@ export declare function matchesSelectPageUp(data: string): boolean;
17
17
  /** Match the generic selector page-down keybinding. */
18
18
  export declare function matchesSelectPageDown(data: string): boolean;
19
19
  export declare function matchesAppExternalEditor(data: string): boolean;
20
+ /**
21
+ * Match the "submit multi-line text input" keybinding (`app.message.followUp`).
22
+ *
23
+ * Used by forms where plain Enter inserts a newline and a modified-Enter chord
24
+ * submits — the main editor's follow-up handler, the agent dashboard's new-agent
25
+ * description, and the hook editor's hook-style mode. The keybinding defaults to
26
+ * `["ctrl+q", "ctrl+enter"]` so Windows Terminal (which can't deliver a distinct
27
+ * Ctrl+Enter event; #1903) still has a working chord without user remapping.
28
+ *
29
+ * Also recognizes modifier-tagged LF as Ctrl+Enter only when Ctrl+Enter is an
30
+ * effective follow-up binding.
31
+ */
32
+ export declare function matchesAppFollowUp(data: string): boolean;
@@ -156,6 +156,8 @@ export interface AgentSessionConfig {
156
156
  modelRegistry: ModelRegistry;
157
157
  /** Tool registry for LSP and settings */
158
158
  toolRegistry?: Map<string, AgentTool>;
159
+ /** Tool names whose current registry entry is still the built-in implementation. */
160
+ builtInToolNames?: Iterable<string>;
159
161
  /** Current session pre-LLM message transform pipeline */
160
162
  transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
161
163
  /** Provider payload hook used by the active session request path */
@@ -511,6 +513,8 @@ export declare class AgentSession {
511
513
  * Get a tool by name from the registry.
512
514
  */
513
515
  getToolByName(name: string): AgentTool | undefined;
516
+ /** True when the current registry entry for `name` came from a built-in factory. */
517
+ hasBuiltInTool(name: string): boolean;
514
518
  /**
515
519
  * Get all configured tool names (built-in via --tools or default, plus custom tools).
516
520
  */
@@ -4,6 +4,8 @@ import { type CompactionEntry, type SessionEntry } from "./session-entries";
4
4
  export interface SessionContext {
5
5
  messages: AgentMessage[];
6
6
  thinkingLevel?: string;
7
+ /** Configured thinking selector (`"auto"` or a concrete level) from the latest change. */
8
+ configuredThinkingLevel?: string;
7
9
  serviceTier?: ServiceTier;
8
10
  /** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
9
11
  models: Record<string, string>;
@@ -30,6 +30,12 @@ export interface SessionMessageEntry extends SessionEntryBase {
30
30
  export interface ThinkingLevelChangeEntry extends SessionEntryBase {
31
31
  type: "thinking_level_change";
32
32
  thinkingLevel?: string | null;
33
+ /**
34
+ * The user-configured selector at the time of this change: `"auto"` when auto
35
+ * mode was active, otherwise the concrete level. Absent on entries written
36
+ * before auto-mode persistence existed; readers fall back to `thinkingLevel`.
37
+ */
38
+ configured?: string | null;
33
39
  }
34
40
  export interface ModelChangeEntry extends SessionEntryBase {
35
41
  type: "model_change";
@@ -138,7 +138,8 @@ export declare class SessionManager {
138
138
  * top-level entries via appendCompaction()/branchWithSummary().
139
139
  */
140
140
  appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | PythonExecutionMessage | FileMentionMessage): string;
141
- appendThinkingLevelChange(thinkingLevel?: string): string;
141
+ /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
142
+ appendThinkingLevelChange(thinkingLevel?: string, configured?: string): string;
142
143
  appendServiceTierChange(serviceTier: ServiceTier | null): string;
143
144
  appendModeChange(mode: string, data?: Record<string, unknown>): string;
144
145
  /**
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared ACP client bridge routing for file-write sites.
3
+ *
4
+ * When an ACP client (e.g. Zed) advertises the `fs.writeTextFile` capability,
5
+ * all write-mode tools must route through it so the editor's open buffer is
6
+ * updated immediately. Internal artifacts ('/Users/theo/.omp/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local' plan files, other scheme
7
+ * URLs) are always written directly to disk — those are OMP-owned and should
8
+ * never be pushed into the editor.
9
+ */
10
+ import type { ToolSession } from ".";
11
+ /**
12
+ * Return `true` when an ACP client bridge write is appropriate for this path.
13
+ *
14
+ * Returns `false` for internal-URL paths (e.g. `'/Users/theo/.omp/agent/sessions/-Projects-oh-my-pi/2026-06-10T09-11-41-506Z_019eb0cd-3ec2-7000-92aa-1b82aa4d78f0/local/PLAN.md'`) and for the
15
+ * active plan file while plan mode is enabled — both are OMP-internal artifacts
16
+ * that must stay off the editor's buffer.
17
+ */
18
+ export declare function shouldRouteWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string): boolean;
19
+ /**
20
+ * Try to route a file write through the ACP client bridge.
21
+ *
22
+ * Performs the full guard check, bridge call (wrapped in {@link ToolError}),
23
+ * FS-scan cache invalidation, and session mutation-version bump.
24
+ *
25
+ * Returns `true` when the bridge was used and the caller must skip the
26
+ * writethrough path. Returns `false` when the bridge is unavailable or the
27
+ * path should not be routed through it.
28
+ */
29
+ export declare function routeWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string, content: string): Promise<boolean>;
@@ -93,6 +93,9 @@ export declare class CmuxTab {
93
93
  waitFor(selector: string, opts?: {
94
94
  timeout?: number;
95
95
  }): Promise<CmuxElementHandle>;
96
+ waitForSelector(selector: string, opts?: {
97
+ timeout?: number;
98
+ }): Promise<CmuxElementHandle>;
96
99
  evaluate<TResult, TArgs extends unknown[]>(fn: string | ((...args: TArgs) => TResult | Promise<TResult>), ...args: TArgs): Promise<TResult>;
97
100
  scrollIntoView(selector: string): Promise<void>;
98
101
  select(selector: string, ...values: string[]): Promise<string[]>;
@@ -101,6 +104,10 @@ export declare class CmuxTab {
101
104
  waitForUrl(pattern: string | RegExp, opts?: {
102
105
  timeout?: number;
103
106
  }): Promise<string>;
107
+ waitForNavigation(opts?: {
108
+ waitUntil?: WaitUntil;
109
+ timeout?: number;
110
+ }): Promise<null>;
104
111
  drag(from: DragTarget, to: DragTarget): Promise<void>;
105
112
  uploadFile(selector: string, ...filePaths: string[]): Promise<void>;
106
113
  waitForResponse(pattern: string | RegExp | ((response: CmuxResponse) => boolean | Promise<boolean>), opts?: {
@@ -11,12 +11,32 @@ declare global {
11
11
  elementFromPoint(x: number, y: number): Element | null;
12
12
  };
13
13
  }
14
+ export interface OpTimeouts {
15
+ /** Largest per-op deadline allowed — strictly below the cell budget. */
16
+ budgetBound: number;
17
+ /** Ceiling for quick page reads. */
18
+ quickOpMs: number;
19
+ /** Ceiling for interactive actions + default for waits. */
20
+ actionOpMs: number;
21
+ }
22
+ /** Resolve the per-op fail-fast ceilings for a given cell budget. */
23
+ export declare function resolveOpTimeouts(cellTimeoutMs: number): OpTimeouts;
24
+ /**
25
+ * Effective timeout for a wait helper (`waitFor*`). A positive explicit `{ timeout }` is
26
+ * honored but clamped to the cell budget so it still fails fast + named; raising the tool
27
+ * `timeout` raises that cap, so a longer budget stays meaningful. No `{ timeout }` → the
28
+ * action ceiling. Puppeteer's `{ timeout: 0 }` / `Infinity` ("disable") maps to the largest
29
+ * bounded wait (`budgetBound`) — the harness never permits an unbounded wait. Garbage input
30
+ * (negative, `NaN`) falls back to the action ceiling rather than the longest wait.
31
+ */
32
+ export declare function resolveWaitTimeout(cellTimeoutMs: number, explicit?: number): number;
14
33
  interface ScreenshotOptions {
15
34
  selector?: string;
16
35
  fullPage?: boolean;
17
36
  save?: string;
18
37
  silent?: boolean;
19
38
  }
39
+ export declare function normalizeSelector(selector: string): string;
20
40
  export interface InflightOp {
21
41
  label: string;
22
42
  startedAt: number;
@@ -4,9 +4,10 @@ export declare const MAX_IMAGE_INPUT_BYTES: number;
4
4
  export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: Set<string>;
5
5
  /**
6
6
  * Ollama and its local-backend family decode image input through llama.cpp /
7
- * `stb_image`, which is compiled without WebP support, so a WebP upload fails
8
- * with an opaque HTTP 400. Detect those models so the resize pipeline encodes
9
- * to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
7
+ * `stb_image`, which is compiled without WebP support. The first-party Codex
8
+ * Responses backend also rejects WebP `input_image.image_url` data URLs. Detect
9
+ * those models so the resize pipeline encodes to PNG/JPEG instead — the
10
+ * automatic equivalent of `OMP_NO_WEBP=1`.
10
11
  */
11
12
  export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined): boolean;
12
13
  /**
@@ -47,3 +47,28 @@ export declare const repo: {
47
47
  /** Check whether `cwd` is inside a Jujutsu repository. */
48
48
  is(cwd: string): Promise<boolean>;
49
49
  };
50
+ /**
51
+ * Detect a "pure" Jujutsu workspace — one where Git-mutating automation has
52
+ * no safe Git target. Invoking `git checkout -b`, `git worktree add`, or
53
+ * `git apply` against a pure jj workspace either fails outright (no `.git/`
54
+ * present) or mutates state that jj itself cannot reconcile.
55
+ *
56
+ * `cwd` is "pure jj" iff its nearest jj workspace ancestor is **closer than**
57
+ * its nearest Git checkout ancestor (or no Git checkout is present at all).
58
+ * Both lookups walk upward from `cwd`, so the deeper ancestor is the one the
59
+ * user is actually working inside.
60
+ *
61
+ * Returns:
62
+ * - `false` for plain Git checkouts (no jj metadata anywhere up the tree).
63
+ * - `false` for colocated jj-git workspaces — `jj git init --colocate` keeps
64
+ * `.jj/` and `.git/` at the same root.
65
+ * - `false` when a nested Git checkout (e.g. a vendored repo or fixture)
66
+ * lives **under** an outer jj workspace; Git automation targets the inner
67
+ * repo and never touches the surrounding jj tree.
68
+ * - `true` when jj is the deeper ancestor — either a standalone pure jj
69
+ * workspace, or a jj workspace nested under an unrelated outer Git
70
+ * checkout, where Git automation against the outer root would silently
71
+ * bypass jj.
72
+ * - `false` for directories backed by neither tool.
73
+ */
74
+ export declare function isPureJjRepo(cwd: string): Promise<boolean>;
@@ -1,8 +1,6 @@
1
1
  import { type Api, type Model } from "@oh-my-pi/pi-ai";
2
2
  import type { ModelRegistry } from "../config/model-registry";
3
3
  import type { Settings } from "../config/settings";
4
- export declare const TITLE_LOCAL_FALLBACK_DELAY_MS = 10000;
5
- export declare function raceFirstNonNull<T>(primary: Promise<T | null>, startFallback: () => Promise<T | null>, delayMs?: number, onPrimaryWinAfterFallback?: () => void): Promise<T | null>;
6
4
  /**
7
5
  * Generate a title for a session based on the first user message.
8
6
  *
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": "16.1.10",
4
+ "version": "16.1.11",
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",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.10",
52
- "@oh-my-pi/omp-stats": "16.1.10",
53
- "@oh-my-pi/pi-agent-core": "16.1.10",
54
- "@oh-my-pi/pi-ai": "16.1.10",
55
- "@oh-my-pi/pi-catalog": "16.1.10",
56
- "@oh-my-pi/pi-mnemopi": "16.1.10",
57
- "@oh-my-pi/pi-natives": "16.1.10",
58
- "@oh-my-pi/pi-tui": "16.1.10",
59
- "@oh-my-pi/pi-utils": "16.1.10",
60
- "@oh-my-pi/pi-wire": "16.1.10",
61
- "@oh-my-pi/snapcompact": "16.1.10",
51
+ "@oh-my-pi/hashline": "16.1.11",
52
+ "@oh-my-pi/omp-stats": "16.1.11",
53
+ "@oh-my-pi/pi-agent-core": "16.1.11",
54
+ "@oh-my-pi/pi-ai": "16.1.11",
55
+ "@oh-my-pi/pi-catalog": "16.1.11",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.11",
57
+ "@oh-my-pi/pi-natives": "16.1.11",
58
+ "@oh-my-pi/pi-tui": "16.1.11",
59
+ "@oh-my-pi/pi-utils": "16.1.11",
60
+ "@oh-my-pi/pi-wire": "16.1.11",
61
+ "@oh-my-pi/snapcompact": "16.1.11",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -19,8 +19,10 @@ if (!outPath) {
19
19
  }
20
20
 
21
21
  const loaderJs = await Bun.file(new URL("../src/export/html/share-loader.js", import.meta.url).pathname).text();
22
- // Pin a built-in theme: the viewer is a public artifact, not a per-user export.
23
- const themeVars = await generateThemeVars("dark");
22
+ // Pin the omp brand palette (collab-web pink/purple identity) the viewer is
23
+ // a public artifact matching the live my.omp.sh client, not a per-user export
24
+ // that should mirror the host's terminal theme.
25
+ const themeVars = await generateThemeVars("web");
24
26
 
25
27
  const html = getTemplate()
26
28
  .replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`)
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "../extensibility/extensions";
2
2
  import * as git from "../utils/git";
3
+ import * as jj from "../utils/jj";
3
4
  import { normalizePathSpec } from "./helpers";
4
5
 
5
6
  const AUTORESEARCH_BRANCH_PREFIX = "autoresearch/";
@@ -37,6 +38,17 @@ export async function ensureAutoresearchBranch(
37
38
  workDir: string,
38
39
  goal: string | null,
39
40
  ): Promise<EnsureAutoresearchBranchResult> {
41
+ // Pure-jj check runs first so a jj workspace nested under an unrelated
42
+ // outer Git checkout is rejected at its own root rather than silently
43
+ // creating `autoresearch/*` branches and commits in the surrounding Git
44
+ // tree behind jj's back.
45
+ if (await jj.isPureJjRepo(workDir)) {
46
+ return {
47
+ ok: false,
48
+ error: "Autoresearch needs a Git checkout for branch isolation and baseline commits, but this workspace is pure Jujutsu (`.jj/` without a colocated `.git/`). Run `jj git init --colocate` to add a Git checkout before starting autoresearch.",
49
+ };
50
+ }
51
+
40
52
  const repoRoot = await git.repo.root(workDir);
41
53
  if (!repoRoot) {
42
54
  return {
@@ -91,15 +91,55 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
91
91
  /** OpenCode MCP server config (from opencode.json "mcp" key) */
92
92
  interface OpenCodeMCPConfig {
93
93
  type?: "local" | "remote";
94
- command?: string;
94
+ command?: string | string[];
95
95
  args?: string[];
96
96
  env?: Record<string, string>;
97
+ environment?: Record<string, string>;
97
98
  url?: string;
98
99
  headers?: Record<string, string>;
99
100
  enabled?: boolean;
100
101
  timeout?: number;
101
102
  }
102
103
 
104
+ function stringArray(value: unknown): string[] | undefined {
105
+ if (!Array.isArray(value)) return undefined;
106
+ for (const item of value) {
107
+ if (typeof item !== "string") return undefined;
108
+ }
109
+ return value;
110
+ }
111
+
112
+ function stringRecord(value: unknown): Record<string, string> | undefined {
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
114
+
115
+ const record: Record<string, string> = {};
116
+ for (const [key, item] of Object.entries(value)) {
117
+ if (typeof item !== "string") return undefined;
118
+ record[key] = item;
119
+ }
120
+ return record;
121
+ }
122
+
123
+ function normalizeCommand(
124
+ commandValue: string | string[] | undefined,
125
+ argsValue: unknown,
126
+ ): { command: string | undefined; args: string[] | undefined } {
127
+ const configuredArgs = stringArray(argsValue);
128
+ if (Array.isArray(commandValue)) {
129
+ const [command, ...commandArgs] = commandValue;
130
+ const args = configuredArgs ? [...commandArgs, ...configuredArgs] : commandArgs;
131
+ return {
132
+ command: typeof command === "string" ? command : undefined,
133
+ args: args.length > 0 ? args : undefined,
134
+ };
135
+ }
136
+
137
+ return {
138
+ command: typeof commandValue === "string" ? commandValue : undefined,
139
+ args: configuredArgs && configuredArgs.length > 0 ? configuredArgs : undefined,
140
+ };
141
+ }
142
+
103
143
  async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
104
144
  const items: MCPServer[] = [];
105
145
  const warnings: string[] = [];
@@ -161,11 +201,14 @@ function extractMCPServers(
161
201
  transport = "stdio";
162
202
  }
163
203
 
204
+ const command = normalizeCommand(serverConfig.command, serverConfig.args);
205
+ const env = stringRecord(serverConfig.environment) ?? stringRecord(serverConfig.env);
206
+
164
207
  items.push({
165
208
  name,
166
- command: serverConfig.command,
167
- args: Array.isArray(serverConfig.args) ? (serverConfig.args as string[]) : undefined,
168
- env: serverConfig.env && typeof serverConfig.env === "object" ? serverConfig.env : undefined,
209
+ command: command.command,
210
+ args: command.args,
211
+ env,
169
212
  url: typeof serverConfig.url === "string" ? serverConfig.url : undefined,
170
213
  headers: serverConfig.headers && typeof serverConfig.headers === "object" ? serverConfig.headers : undefined,
171
214
  enabled: serverConfig.enabled,
@@ -20,6 +20,7 @@ import { Filesystem, NotFoundError, type WriteResult } from "@oh-my-pi/hashline"
20
20
  import { isEnoent } from "@oh-my-pi/pi-utils";
21
21
  import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
22
22
  import type { ToolSession } from "../../tools";
23
+ import { routeWriteThroughBridge } from "../../tools/acp-bridge";
23
24
  import { assertEditableFileContent } from "../../tools/auto-generated-guard";
24
25
  import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
25
26
  import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
@@ -110,6 +111,13 @@ export class HashlineFilesystem extends Filesystem {
110
111
  await this.preflightWrite(relativePath);
111
112
  const absolutePath = this.resolveAbsolute(relativePath);
112
113
  const finalContent = await serializeEditFileText(absolutePath, relativePath, content);
114
+
115
+ // Route through ACP bridge when available; skips internal artifacts.
116
+ if (await routeWriteThroughBridge(this.session, relativePath, absolutePath, finalContent)) {
117
+ this.#diagnosticsByPath.set(relativePath, undefined);
118
+ return { text: finalContent };
119
+ }
120
+
113
121
  const diagnostics = await this.#writethrough(
114
122
  absolutePath,
115
123
  finalContent,