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

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 (63) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.js +2480 -2481
  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/export/share.d.ts +10 -5
  6. package/dist/types/hindsight/content.d.ts +7 -0
  7. package/dist/types/hindsight/transcript.d.ts +1 -1
  8. package/dist/types/modes/components/hook-editor.d.ts +2 -1
  9. package/dist/types/modes/interactive-mode.d.ts +5 -0
  10. package/dist/types/modes/types.d.ts +17 -0
  11. package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
  12. package/dist/types/secrets/index.d.ts +1 -1
  13. package/dist/types/secrets/obfuscator.d.ts +43 -9
  14. package/dist/types/session/agent-session.d.ts +4 -0
  15. package/dist/types/session/session-context.d.ts +2 -0
  16. package/dist/types/session/session-entries.d.ts +6 -0
  17. package/dist/types/session/session-manager.d.ts +2 -1
  18. package/dist/types/tools/acp-bridge.d.ts +29 -0
  19. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +7 -0
  20. package/dist/types/tools/browser/tab-worker.d.ts +20 -0
  21. package/dist/types/utils/jj.d.ts +25 -0
  22. package/dist/types/utils/title-generator.d.ts +0 -2
  23. package/package.json +12 -12
  24. package/scripts/generate-share-viewer.ts +4 -2
  25. package/src/autoresearch/git.ts +12 -0
  26. package/src/discovery/opencode.ts +47 -4
  27. package/src/edit/hashline/filesystem.ts +8 -0
  28. package/src/edit/modes/patch.ts +18 -2
  29. package/src/edit/modes/replace.ts +13 -10
  30. package/src/export/html/index.ts +50 -8
  31. package/src/export/html/web-palette.ts +142 -0
  32. package/src/export/share.ts +198 -8
  33. package/src/hindsight/backend.ts +4 -4
  34. package/src/hindsight/content.ts +17 -1
  35. package/src/hindsight/transcript.ts +2 -2
  36. package/src/internal-urls/docs-index.generated.txt +1 -1
  37. package/src/main.ts +8 -0
  38. package/src/modes/components/agent-dashboard.ts +8 -8
  39. package/src/modes/components/hook-editor.ts +13 -10
  40. package/src/modes/components/session-selector.ts +3 -0
  41. package/src/modes/controllers/event-controller.ts +9 -5
  42. package/src/modes/controllers/extension-ui-controller.ts +6 -2
  43. package/src/modes/interactive-mode.ts +69 -29
  44. package/src/modes/theme/dark.json +1 -1
  45. package/src/modes/types.ts +18 -0
  46. package/src/modes/utils/keybinding-matchers.ts +36 -1
  47. package/src/modes/utils/ui-helpers.ts +1 -0
  48. package/src/prompts/tools/browser.md +3 -2
  49. package/src/sdk.ts +14 -2
  50. package/src/secrets/index.ts +1 -1
  51. package/src/secrets/obfuscator.ts +220 -71
  52. package/src/session/agent-session.ts +57 -43
  53. package/src/session/session-context.ts +5 -0
  54. package/src/session/session-entries.ts +6 -0
  55. package/src/session/session-manager.ts +3 -1
  56. package/src/task/worktree.ts +12 -4
  57. package/src/thinking.ts +1 -1
  58. package/src/tools/acp-bridge.ts +66 -0
  59. package/src/tools/browser/cmux/cmux-tab.ts +37 -0
  60. package/src/tools/browser/tab-worker.ts +160 -37
  61. package/src/tools/write.ts +2 -25
  62. package/src/utils/jj.ts +47 -0
  63. 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;
@@ -1,6 +1,6 @@
1
1
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
2
2
  import { DEFAULT_SHARE_URL } from "@oh-my-pi/pi-wire";
3
- import type { SecretObfuscator } from "../secrets/obfuscator";
3
+ import { type SecretObfuscator } from "../secrets/obfuscator";
4
4
  import type { SessionManager } from "../session/session-manager";
5
5
  import { type SessionData } from "./html";
6
6
  export { DEFAULT_SHARE_URL };
@@ -12,10 +12,15 @@ export interface ShareSessionOptions {
12
12
  /** Agent state for system prompt + tool descriptions in the snapshot. */
13
13
  state?: AgentState;
14
14
  /**
15
- * Redacts the snapshot before sealing: deep-walks every string (entries,
16
- * header, system prompt, tool descriptions) through the obfuscator, so
17
- * secrets that landed in persisted entries (tool outputs reading .env,
18
- * etc.) never leave the machine. Pass undefined to skip.
15
+ * Redacts the snapshot before sealing via a typed, per-field walk over the
16
+ * session (header title/cwd, system prompt, tool descriptions, entry summaries,
17
+ * labels, and message text including tool-result output and `@file` mentions),
18
+ * so secrets that landed in persisted entries (tool outputs reading .env, etc.)
19
+ * never leave the machine. Inline image bytes are preserved (size-trimmed
20
+ * separately); opaque provider-replay blobs (`providerPayload`,
21
+ * `redactedThinking`, `compaction.preserveData`) and untyped extension payloads
22
+ * (`details`/`data`/`outputSchema`) are dropped rather than walked. Pass
23
+ * undefined to skip redaction entirely.
19
24
  */
20
25
  obfuscator?: SecretObfuscator;
21
26
  }
@@ -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;
@@ -1,5 +1,5 @@
1
1
  import type { SecretEntry } from "./obfuscator";
2
- export { deobfuscateSessionContext, obfuscateMessages, obfuscateProviderContext, obfuscateProviderTools, type SecretEntry, SecretObfuscator, } from "./obfuscator";
2
+ export { deobfuscateSessionContext, deobfuscateToolArguments, obfuscateMessages, obfuscateProviderContext, type SecretEntry, SecretObfuscator, } from "./obfuscator";
3
3
  /**
4
4
  * Load secrets from project-local and global secrets.yml files.
5
5
  * Project-local entries override global entries with matching content.
@@ -1,4 +1,5 @@
1
- import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Context, Message } from "@oh-my-pi/pi-ai";
2
3
  import type { SessionContext } from "../session/session-context";
3
4
  export interface SecretEntry {
4
5
  type: "plain" | "regex";
@@ -7,6 +8,12 @@ export interface SecretEntry {
7
8
  replacement?: string;
8
9
  flags?: string;
9
10
  }
11
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
12
+ [key: string]: JsonValue | undefined;
13
+ };
14
+ export type JsonRecord = {
15
+ [key: string]: JsonValue | undefined;
16
+ };
10
17
  export declare class SecretObfuscator {
11
18
  #private;
12
19
  constructor(entries: SecretEntry[]);
@@ -15,15 +22,42 @@ export declare class SecretObfuscator {
15
22
  obfuscate(text: string): string;
16
23
  /** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
17
24
  deobfuscate(text: string): string;
18
- /** Deep-walk an object, deobfuscating all string values. */
19
- deobfuscateObject<T>(obj: T): T;
20
- /** Deep-walk an object, obfuscating all string values. */
21
- obfuscateObject<T>(obj: T): T;
22
25
  }
26
+ /**
27
+ * Restore secret placeholders for local display. Only message kinds the model
28
+ * itself authored from obfuscated context carry placeholders — assistant
29
+ * content and the LLM-written branch/compaction summaries. User, developer, and
30
+ * tool-result messages are persisted with their literal text, so a literal
31
+ * `#ABCD#` the operator typed must survive untouched; those roles are never
32
+ * walked.
33
+ */
23
34
  export declare function deobfuscateSessionContext(sessionContext: SessionContext, obfuscator: SecretObfuscator | undefined): SessionContext;
24
- /** Obfuscate all string content in LLM messages (for outbound interception). */
35
+ export declare function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[];
36
+ /**
37
+ * Restore placeholders in assistant content: visible text, thinking text, and
38
+ * tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
39
+ * are opaque provider-replay data and pass through byte-identical.
40
+ */
41
+ export declare function deobfuscateAssistantContent(obfuscator: SecretObfuscator, content: AssistantMessage["content"]): AssistantMessage["content"];
42
+ /**
43
+ * Restore placeholders inside a tool call's arguments. Arguments are arbitrary
44
+ * model-authored JSON, so tool-call arguments are the ONLY place a recursive
45
+ * JSON walk runs.
46
+ */
47
+ export declare function deobfuscateToolArguments(obfuscator: SecretObfuscator, args: Record<string, unknown>): Record<string, unknown>;
48
+ /** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
49
+ export declare function obfuscateToolArguments(obfuscator: SecretObfuscator, args: Record<string, unknown>): Record<string, unknown>;
50
+ /**
51
+ * Redact secrets from outbound messages. Opt-in by origin: only user messages,
52
+ * tool results, and user-authored developer messages (e.g. `@file` mentions)
53
+ * can carry operator secrets. System prompts, tool schemas, and assistant
54
+ * output are author-controlled or model-generated and pass through untouched.
55
+ * Within a targeted message only `text` blocks are rewritten — inline image
56
+ * bytes are never walked.
57
+ */
25
58
  export declare function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[];
26
- /** Obfuscate provider request context without walking live tool schema instances. */
59
+ /**
60
+ * Redact outbound provider context. Only conversation messages are rewritten;
61
+ * the static system prompt and tool schemas pass through unchanged.
62
+ */
27
63
  export declare function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context;
28
- /** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
29
- export declare function obfuscateProviderTools(obfuscator: SecretObfuscator | undefined, tools: Tool[] | undefined): Tool[] | undefined;
@@ -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;
@@ -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.12",
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.12",
52
+ "@oh-my-pi/omp-stats": "16.1.12",
53
+ "@oh-my-pi/pi-agent-core": "16.1.12",
54
+ "@oh-my-pi/pi-ai": "16.1.12",
55
+ "@oh-my-pi/pi-catalog": "16.1.12",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.12",
57
+ "@oh-my-pi/pi-natives": "16.1.12",
58
+ "@oh-my-pi/pi-tui": "16.1.12",
59
+ "@oh-my-pi/pi-utils": "16.1.12",
60
+ "@oh-my-pi/pi-wire": "16.1.12",
61
+ "@oh-my-pi/snapcompact": "16.1.12",
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 {