@oh-my-pi/pi-coding-agent 16.1.9 → 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 (81) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cli.js +3455 -3430
  3. package/dist/types/config/settings-schema.d.ts +10 -0
  4. package/dist/types/edit/streaming.d.ts +5 -5
  5. package/dist/types/export/html/index.d.ts +31 -2
  6. package/dist/types/export/html/web-palette.d.ts +117 -0
  7. package/dist/types/hindsight/content.d.ts +7 -0
  8. package/dist/types/hindsight/transcript.d.ts +1 -1
  9. package/dist/types/modes/components/hook-editor.d.ts +2 -1
  10. package/dist/types/modes/controllers/selector-controller.d.ts +0 -1
  11. package/dist/types/modes/interactive-mode.d.ts +5 -0
  12. package/dist/types/modes/types.d.ts +17 -0
  13. package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
  14. package/dist/types/sdk.d.ts +1 -0
  15. package/dist/types/session/agent-session.d.ts +4 -0
  16. package/dist/types/session/session-context.d.ts +2 -0
  17. package/dist/types/session/session-entries.d.ts +6 -0
  18. package/dist/types/session/session-manager.d.ts +2 -1
  19. package/dist/types/system-prompt.d.ts +2 -0
  20. package/dist/types/tools/acp-bridge.d.ts +29 -0
  21. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
  22. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +10 -0
  23. package/dist/types/tools/browser/tab-worker.d.ts +20 -0
  24. package/dist/types/tools/browser.d.ts +1 -0
  25. package/dist/types/tools/find.d.ts +1 -2
  26. package/dist/types/tools/write.d.ts +1 -2
  27. package/dist/types/utils/image-loading.d.ts +4 -3
  28. package/dist/types/utils/jj.d.ts +25 -0
  29. package/dist/types/utils/title-generator.d.ts +0 -2
  30. package/package.json +12 -12
  31. package/scripts/generate-aria-snapshot.ts +134 -0
  32. package/scripts/generate-share-viewer.ts +4 -2
  33. package/src/autoresearch/git.ts +12 -0
  34. package/src/config/settings-schema.ts +12 -0
  35. package/src/discovery/opencode.ts +47 -4
  36. package/src/edit/hashline/filesystem.ts +8 -0
  37. package/src/edit/modes/patch.ts +18 -2
  38. package/src/edit/modes/replace.ts +13 -10
  39. package/src/edit/streaming.ts +5 -5
  40. package/src/export/html/index.ts +50 -8
  41. package/src/export/html/web-palette.ts +142 -0
  42. package/src/hindsight/backend.ts +4 -4
  43. package/src/hindsight/content.ts +17 -1
  44. package/src/hindsight/transcript.ts +2 -2
  45. package/src/internal-urls/docs-index.generated.txt +1 -1
  46. package/src/modes/components/agent-dashboard.ts +8 -8
  47. package/src/modes/components/hook-editor.ts +13 -10
  48. package/src/modes/components/session-selector.ts +3 -0
  49. package/src/modes/controllers/event-controller.ts +29 -5
  50. package/src/modes/controllers/extension-ui-controller.ts +6 -2
  51. package/src/modes/controllers/selector-controller.ts +3 -5
  52. package/src/modes/interactive-mode.ts +69 -29
  53. package/src/modes/theme/dark.json +1 -1
  54. package/src/modes/types.ts +18 -0
  55. package/src/modes/utils/keybinding-matchers.ts +36 -1
  56. package/src/modes/utils/ui-helpers.ts +1 -0
  57. package/src/prompts/system/project-prompt.md +2 -0
  58. package/src/prompts/system/system-prompt.md +0 -1
  59. package/src/prompts/tools/browser.md +5 -2
  60. package/src/sdk.ts +20 -2
  61. package/src/session/agent-session.ts +38 -14
  62. package/src/session/session-context.ts +5 -0
  63. package/src/session/session-entries.ts +6 -0
  64. package/src/session/session-manager.ts +3 -1
  65. package/src/system-prompt.ts +15 -3
  66. package/src/task/worktree.ts +12 -4
  67. package/src/thinking.ts +1 -1
  68. package/src/tools/acp-bridge.ts +66 -0
  69. package/src/tools/bash.ts +3 -3
  70. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  71. package/src/tools/browser/aria/aria-snapshot.ts +103 -0
  72. package/src/tools/browser/cmux/cmux-tab.ts +73 -1
  73. package/src/tools/browser/tab-worker.ts +242 -47
  74. package/src/tools/browser.ts +5 -0
  75. package/src/tools/find.ts +1 -2
  76. package/src/tools/index.ts +1 -1
  77. package/src/tools/write.ts +3 -27
  78. package/src/utils/git.ts +3 -2
  79. package/src/utils/image-loading.ts +6 -3
  80. package/src/utils/jj.ts +47 -0
  81. package/src/utils/title-generator.ts +31 -99
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.9",
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.9",
52
- "@oh-my-pi/omp-stats": "16.1.9",
53
- "@oh-my-pi/pi-agent-core": "16.1.9",
54
- "@oh-my-pi/pi-ai": "16.1.9",
55
- "@oh-my-pi/pi-catalog": "16.1.9",
56
- "@oh-my-pi/pi-mnemopi": "16.1.9",
57
- "@oh-my-pi/pi-natives": "16.1.9",
58
- "@oh-my-pi/pi-tui": "16.1.9",
59
- "@oh-my-pi/pi-utils": "16.1.9",
60
- "@oh-my-pi/pi-wire": "16.1.9",
61
- "@oh-my-pi/snapcompact": "16.1.9",
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",
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Regenerates the committed browser asset
4
+ *
5
+ * src/tools/browser/aria/aria-snapshot.bundle.txt ← bundled CJS module
6
+ *
7
+ * by fetching Playwright's injected ARIA-snapshot sources (pinned to
8
+ * PLAYWRIGHT_TAG), wrapping them with a small entry, and bundling — all in a
9
+ * throwaway temp dir. Only the bundle is committed; the upstream sources are NOT
10
+ * vendored into the repo (no shipping both source + generated copies). This is a
11
+ * dev-time, network-bound step, exactly like `generate-models`.
12
+ *
13
+ * The tab worker imports the `.txt` with `{ type: "text" }`, wraps it in a
14
+ * `new Function` worker-side, and runs it via puppeteer's CDP evaluate (it
15
+ * installs nothing on `window`). The committed output means binary and source
16
+ * installs need no network or build step at runtime.
17
+ *
18
+ * Usage: bun scripts/generate-aria-snapshot.ts
19
+ */
20
+ import * as fs from "node:fs/promises";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+
24
+ const PLAYWRIGHT_TAG = "v1.61.0";
25
+ const RAW_BASE = `https://raw.githubusercontent.com/microsoft/playwright/${PLAYWRIGHT_TAG}/packages`;
26
+
27
+ const OUTPUT = path.join(import.meta.dir, "..", "src", "tools", "browser", "aria", "aria-snapshot.bundle.txt");
28
+
29
+ // Upstream source path -> temp path (relative to the temp root).
30
+ const VENDOR_FILES: Array<[string, string]> = [
31
+ ["injected/src/ariaSnapshot.ts", "injected/ariaSnapshot.ts"],
32
+ ["injected/src/roleUtils.ts", "injected/roleUtils.ts"],
33
+ ["injected/src/domUtils.ts", "injected/domUtils.ts"],
34
+ ["isomorphic/ariaSnapshot.ts", "isomorphic/ariaSnapshot.ts"],
35
+ ["isomorphic/stringUtils.ts", "isomorphic/stringUtils.ts"],
36
+ ["isomorphic/cssTokenizer.ts", "isomorphic/cssTokenizer.ts"],
37
+ ["isomorphic/yaml.ts", "isomorphic/yaml.ts"],
38
+ ];
39
+
40
+ // Entry wrapping the upstream modules. Always runs Playwright's `ai` mode so every
41
+ // node carries a `[ref=eN]` id; matched nodes get an `_ariaRef` expando. Existing
42
+ // expandos are cleared first so the fresh module's counter renumbers from e1
43
+ // deterministically (refs are valid until the next snapshot). Installs nothing on
44
+ // `window`.
45
+ const ENTRY_SOURCE = `
46
+ import { generateAriaTree, renderAriaTree } from "./injected/ariaSnapshot";
47
+
48
+ export interface AriaSnapshotRequest {
49
+ depth?: number;
50
+ boxes?: boolean;
51
+ }
52
+
53
+ function walkElements(fn: (el: Element) => void): void {
54
+ const walk = (root: { querySelectorAll(s: string): ArrayLike<Element> }): void => {
55
+ for (const el of Array.from(root.querySelectorAll("*"))) {
56
+ fn(el);
57
+ const shadow = (el as Element & { shadowRoot?: { querySelectorAll(s: string): ArrayLike<Element> } | null }).shadowRoot;
58
+ if (shadow) walk(shadow);
59
+ }
60
+ };
61
+ walk(document as unknown as { querySelectorAll(s: string): ArrayLike<Element> });
62
+ }
63
+ type RefElement = Element & { _ariaRef?: { role: string; name: string; ref: string } };
64
+
65
+ export function ariaSnapshot(root: Element | null, request: AriaSnapshotRequest = {}): string {
66
+ walkElements(el => {
67
+ if ((el as RefElement)._ariaRef) delete (el as RefElement)._ariaRef;
68
+ });
69
+ const target = root ?? document.body ?? document.documentElement;
70
+ const options = { mode: "ai", depth: request.depth, boxes: request.boxes } as const;
71
+ const tree = generateAriaTree(target, options);
72
+ return renderAriaTree(tree, options).text;
73
+ }
74
+
75
+ export function resolveAriaRef(ref: string): Element | null {
76
+ let found: Element | null = null;
77
+ walkElements(el => {
78
+ if (!found && (el as RefElement)._ariaRef?.ref === ref) found = el;
79
+ });
80
+ return found;
81
+ }
82
+ `;
83
+
84
+ async function main(): Promise<void> {
85
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "omp-aria-"));
86
+ try {
87
+ // Fetch pinned upstream sources into the temp dir.
88
+ for (const [src, dst] of VENDOR_FILES) {
89
+ const url = `${RAW_BASE}/${src}`;
90
+ const res = await fetch(url);
91
+ if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
92
+ await Bun.write(path.join(tmp, dst), await res.text());
93
+ }
94
+ const entry = path.join(tmp, "entry.ts");
95
+ await Bun.write(entry, ENTRY_SOURCE);
96
+
97
+ // The injected sources import isomorphic modules via the `@isomorphic/*`
98
+ // alias and the `yaml` package (type-only). Resolve the alias to the fetched
99
+ // copies and stub `yaml` (only referenced from erased `import type`).
100
+ const aliasPlugin: Bun.BunPlugin = {
101
+ name: "aria-vendor-alias",
102
+ setup(build) {
103
+ build.onResolve({ filter: /^@isomorphic\// }, args => ({
104
+ path: path.join(tmp, "isomorphic", `${args.path.slice("@isomorphic/".length)}.ts`),
105
+ }));
106
+ build.onResolve({ filter: /^yaml$/ }, () => ({ path: "yaml", namespace: "aria-yaml-stub" }));
107
+ build.onLoad({ filter: /.*/, namespace: "aria-yaml-stub" }, () => ({
108
+ contents: "export {};",
109
+ loader: "ts",
110
+ }));
111
+ },
112
+ };
113
+
114
+ const result = await Bun.build({
115
+ entrypoints: [entry],
116
+ target: "browser",
117
+ format: "cjs",
118
+ minify: true,
119
+ plugins: [aliasPlugin],
120
+ });
121
+ if (!result.success) {
122
+ for (const log of result.logs) console.error(log);
123
+ throw new Error("aria snapshot bundle failed");
124
+ }
125
+ const code = await result.outputs[0].text();
126
+ const header = `// @generated by scripts/generate-aria-snapshot.ts from Playwright ${PLAYWRIGHT_TAG}\n// Bundled from Playwright's injected ARIA-snapshot sources (Apache-2.0, (c) Microsoft).\n// Do not edit by hand. Regenerate with: bun scripts/generate-aria-snapshot.ts\n`;
127
+ await Bun.write(OUTPUT, header + code);
128
+ console.log(`bundled ${path.relative(process.cwd(), OUTPUT)} (${code.length}b)`);
129
+ } finally {
130
+ await fs.rm(tmp, { recursive: true, force: true });
131
+ }
132
+ }
133
+
134
+ await main();
@@ -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 {
@@ -967,6 +967,18 @@ export const SETTINGS_SCHEMA = {
967
967
  },
968
968
  },
969
969
 
970
+ includeWorkspaceTree: {
971
+ type: "boolean",
972
+ default: false,
973
+ ui: {
974
+ tab: "model",
975
+ group: "Prompt",
976
+ label: "Include Workspace Tree",
977
+ description:
978
+ "Render the workspace directory tree in the system prompt. WARNING: This can bust prompt caching across sessions when files are modified.",
979
+ },
980
+ },
981
+
970
982
  personality: {
971
983
  type: "enum",
972
984
  values: ["default", "friendly", "pragmatic", "none"] as const,
@@ -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,
@@ -17,6 +17,7 @@ import {
17
17
  type WritethroughDeferredHandle,
18
18
  } from "../../lsp";
19
19
  import type { ToolSession } from "../../tools";
20
+ import { routeWriteThroughBridge } from "../../tools/acp-bridge";
20
21
  import { assertEditableFile } from "../../tools/auto-generated-guard";
21
22
  import {
22
23
  invalidateFsScanAfterDelete,
@@ -1663,6 +1664,8 @@ class LspFileSystem implements FileSystem {
1663
1664
  #fileCache: Record<string, Bun.BunFile> = {};
1664
1665
 
1665
1666
  constructor(
1667
+ private readonly session: ToolSession,
1668
+ private readonly requestedPath: string,
1666
1669
  private readonly writethrough: WritethroughCallback,
1667
1670
  private readonly signal?: AbortSignal,
1668
1671
  private readonly batchRequest?: LspBatchRequest,
@@ -1692,8 +1695,14 @@ class LspFileSystem implements FileSystem {
1692
1695
  }
1693
1696
 
1694
1697
  async write(path: string, content: string): Promise<void> {
1695
- const file = this.#getFile(path);
1696
1698
  const finalContent = await serializeEditFileText(path, path, content);
1699
+
1700
+ // Route through ACP bridge when available; skips internal artifacts and local:// paths.
1701
+ if (await routeWriteThroughBridge(this.session, this.requestedPath, path, finalContent)) {
1702
+ return;
1703
+ }
1704
+
1705
+ const file = this.#getFile(path);
1697
1706
  const deferredForPath = this.deferredForPath;
1698
1707
  const result = await this.writethrough(
1699
1708
  path,
@@ -1785,7 +1794,14 @@ export async function executePatchSingle(
1785
1794
  }
1786
1795
 
1787
1796
  const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
1788
- const patchFileSystem = new LspFileSystem(writethrough, signal, batchRequest, beginDeferredDiagnosticsForPath);
1797
+ const patchFileSystem = new LspFileSystem(
1798
+ session,
1799
+ path, // original user-provided path for bridge guard (may be local://, vault://, etc.)
1800
+ writethrough,
1801
+ signal,
1802
+ batchRequest,
1803
+ beginDeferredDiagnosticsForPath,
1804
+ );
1789
1805
  const result = await applyPatch(input, {
1790
1806
  cwd: session.cwd,
1791
1807
  fs: patchFileSystem,
@@ -6,8 +6,9 @@
6
6
  */
7
7
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
8
8
  import { type } from "arktype";
9
- import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
9
+ import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
10
10
  import type { ToolSession } from "../../tools";
11
+ import { routeWriteThroughBridge } from "../../tools/acp-bridge";
11
12
  import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
12
13
  import { outputMeta } from "../../tools/output-meta";
13
14
  import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
@@ -1098,15 +1099,17 @@ export async function executeReplaceSingle(
1098
1099
  path,
1099
1100
  bom + restoreLineEndings(result.content, originalEnding),
1100
1101
  );
1101
- const diagnostics = await writethrough(
1102
- absolutePath,
1103
- finalContent,
1104
- signal,
1105
- Bun.file(absolutePath),
1106
- batchRequest,
1107
- dst => (dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined),
1108
- );
1109
- invalidateFsScanAfterWrite(absolutePath);
1102
+
1103
+ // Route through ACP bridge when available; skips internal artifacts.
1104
+ let diagnostics: FileDiagnosticsResult | undefined;
1105
+ if (await routeWriteThroughBridge(session, path, absolutePath, finalContent)) {
1106
+ // bridge handled the write; diagnostics not available via writethrough
1107
+ } else {
1108
+ diagnostics = await writethrough(absolutePath, finalContent, signal, Bun.file(absolutePath), batchRequest, dst =>
1109
+ dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined,
1110
+ );
1111
+ invalidateFsScanAfterWrite(absolutePath);
1112
+ }
1110
1113
 
1111
1114
  const diffResult = generateDiffString(normalizedContent, result.content, undefined, { path });
1112
1115
  const resultText =
@@ -85,12 +85,12 @@ export interface EditStreamingStrategy<Args = unknown> {
85
85
 
86
86
  /**
87
87
  * Given an edits array parsed from partial JSON, drop the last entry when the
88
- * corresponding object in `partialJson` does not yet end with a closed `}`.
88
+ * corresponding object in `partialJson` has not yet closed with `}`.
89
89
  *
90
- * This guards against `partial-json` silently coercing truncated tails like
91
- * `"write":nu` / `"write":nul` into `{ write: null }`, which would make the
92
- * last entry render a spurious null-write error until the value finishes
93
- * streaming.
90
+ * The streaming parser materializes a trailing edit object from the fields seen
91
+ * so far before its closing `}` arrives, so an unfinished last entry can render
92
+ * as a (partial) edit mid-stream. Dropping it until the object closes keeps the
93
+ * preview from showing an incomplete edit.
94
94
  */
95
95
  export function dropIncompleteLastEdit<T>(edits: readonly T[], partialJson: string | undefined, listKey: string): T[] {
96
96
  if (!Array.isArray(edits) || edits.length === 0) return [...(edits ?? [])];
@@ -12,6 +12,7 @@ import templateJs from "./template.js" with { type: "text" };
12
12
  // Pre-built React tool renderers: built by `bun --cwd=packages/collab-web run build:tool-views`,
13
13
  // run automatically by root `prepare` on install and by `prepack` at publish.
14
14
  import toolViewsJs from "./tool-views.generated.js" with { type: "text" };
15
+ import { webExportThemeVars } from "./web-palette";
15
16
 
16
17
  let cachedTemplate: string | undefined;
17
18
 
@@ -36,6 +37,19 @@ export function getTemplate(): string {
36
37
 
37
38
  export interface ExportOptions {
38
39
  outputPath?: string;
40
+ /**
41
+ * Which color palette the export ships with.
42
+ * - `"web"` (default) — the omp brand identity (collab-web pink/purple),
43
+ * so public HTML exports and the `/s/<id>` share viewer match the live
44
+ * `my.omp.sh` client. See `web-palette.ts`.
45
+ * - `"theme"` — derive from `themeName` (or the active TUI theme), preserving
46
+ * the pre-15.12 behavior where an export mirrored the user's terminal.
47
+ */
48
+ palette?: "web" | "theme";
49
+ /**
50
+ * TUI theme to derive colors from when `palette: "theme"`. Ignored for the
51
+ * default `"web"` palette. Resolves to the active TUI theme when omitted.
52
+ */
39
53
  themeName?: string;
40
54
  /** Embed subagent session transcripts found next to the session file (default true). */
41
55
  includeSubSessions?: boolean;
@@ -101,12 +115,38 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string
101
115
  };
102
116
  }
103
117
 
104
- /** Generate CSS custom properties for theme. Exported for the share-viewer build script. */
105
- export async function generateThemeVars(themeName?: string): Promise<string> {
118
+ /**
119
+ * Generate CSS custom properties for the export `:root`.
120
+ *
121
+ * Two call shapes:
122
+ * • `generateThemeVars("web" | "theme", themeName?)` — explicit palette.
123
+ * `"web"` (the default for public artifacts) returns the fixed omp brand
124
+ * palette from `web-palette.ts` — collab-web pink/purple identity, shared
125
+ * with the live `my.omp.sh` client, so exports and the share viewer render
126
+ * identically to it. `"theme"` derives from the TUI theme via
127
+ * `getResolvedThemeColors(themeName)` plus the three
128
+ * `export.{pageBg,cardBg,infoBg}` surface overrides.
129
+ * • `generateThemeVars(themeName)` — legacy single-arg form: derive from the
130
+ * named TUI theme. Kept so existing callers (and the theme-islight test)
131
+ * keep working; equivalent to `generateThemeVars("theme", themeName)`.
132
+ *
133
+ * Exported for the share-viewer build script.
134
+ */
135
+ export async function generateThemeVars(
136
+ palette: "web" | "theme" | (string & {}) = "web",
137
+ themeName?: string,
138
+ ): Promise<string> {
139
+ // Legacy single-arg form: `generateThemeVars("my-theme")` — the first arg
140
+ // is a theme name, not a palette. Route it to the themed path.
141
+ if (palette !== "web" && palette !== "theme") {
142
+ return generateThemeVars("theme", palette);
143
+ }
144
+ if (palette === "web") return webExportThemeVars();
145
+
106
146
  const colors = await getResolvedThemeColors(themeName);
107
147
  const lines: string[] = [];
108
- for (const [key, value] of Object.entries(colors)) {
109
- lines.push(`--${key}: ${value};`);
148
+ for (const key in colors) {
149
+ lines.push(`--${key}: ${colors[key]};`);
110
150
  }
111
151
 
112
152
  const themeExport = await getThemeExportColors(themeName);
@@ -200,8 +240,8 @@ async function collectSubSessionsFromDir(
200
240
  }
201
241
 
202
242
  /** Generate HTML from bundled template with runtime substitutions. */
203
- async function generateHtml(sessionData: SessionData, themeName?: string): Promise<string> {
204
- const themeVars = await generateThemeVars(themeName);
243
+ async function generateHtml(sessionData: SessionData, palette: "web" | "theme", themeName?: string): Promise<string> {
244
+ const themeVars = await generateThemeVars(palette, themeName);
205
245
  const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
206
246
 
207
247
  // Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
@@ -229,7 +269,8 @@ export async function exportSessionToHtml(
229
269
  if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions;
230
270
  }
231
271
 
232
- const html = await generateHtml(sessionData, opts.themeName);
272
+ const palette = opts.palette ?? (opts.themeName ? "theme" : "web");
273
+ const html = await generateHtml(sessionData, palette, opts.themeName);
233
274
  const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`;
234
275
 
235
276
  await Bun.write(outputPath, html);
@@ -258,7 +299,8 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
258
299
  if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions;
259
300
  }
260
301
 
261
- const html = await generateHtml(sessionData, opts.themeName);
302
+ const palette = opts.palette ?? (opts.themeName ? "theme" : "web");
303
+ const html = await generateHtml(sessionData, palette, opts.themeName);
262
304
  const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
263
305
 
264
306
  await Bun.write(outputPath, html);