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

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 (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli.js +3076 -3050
  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/modes/controllers/selector-controller.d.ts +0 -1
  6. package/dist/types/sdk.d.ts +1 -0
  7. package/dist/types/system-prompt.d.ts +2 -0
  8. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
  9. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -0
  10. package/dist/types/tools/browser.d.ts +1 -0
  11. package/dist/types/tools/find.d.ts +1 -2
  12. package/dist/types/tools/write.d.ts +1 -2
  13. package/package.json +12 -12
  14. package/scripts/generate-aria-snapshot.ts +134 -0
  15. package/src/config/settings-schema.ts +12 -0
  16. package/src/edit/streaming.ts +5 -5
  17. package/src/internal-urls/docs-index.generated.txt +1 -1
  18. package/src/modes/controllers/event-controller.ts +20 -0
  19. package/src/modes/controllers/selector-controller.ts +3 -5
  20. package/src/prompts/system/project-prompt.md +2 -0
  21. package/src/prompts/system/system-prompt.md +0 -1
  22. package/src/prompts/tools/browser.md +2 -0
  23. package/src/sdk.ts +8 -1
  24. package/src/system-prompt.ts +15 -3
  25. package/src/tools/bash.ts +3 -3
  26. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  27. package/src/tools/browser/aria/aria-snapshot.ts +103 -0
  28. package/src/tools/browser/cmux/cmux-tab.ts +36 -1
  29. package/src/tools/browser/tab-worker.ts +89 -17
  30. package/src/tools/browser.ts +5 -0
  31. package/src/tools/find.ts +1 -2
  32. package/src/tools/index.ts +1 -1
  33. package/src/tools/write.ts +1 -2
  34. package/src/utils/git.ts +3 -2
@@ -912,6 +912,16 @@ export declare const SETTINGS_SCHEMA: {
912
912
  readonly description: "Surface the active model identifier in the system prompt so the agent knows which model it is";
913
913
  };
914
914
  };
915
+ readonly includeWorkspaceTree: {
916
+ readonly type: "boolean";
917
+ readonly default: false;
918
+ readonly ui: {
919
+ readonly tab: "model";
920
+ readonly group: "Prompt";
921
+ readonly label: "Include Workspace Tree";
922
+ readonly description: "Render the workspace directory tree in the system prompt. WARNING: This can bust prompt caching across sessions when files are modified.";
923
+ };
924
+ };
915
925
  readonly personality: {
916
926
  readonly type: "enum";
917
927
  readonly values: readonly ["default", "friendly", "pragmatic", "none"];
@@ -63,12 +63,12 @@ export interface EditStreamingStrategy<Args = unknown> {
63
63
  }
64
64
  /**
65
65
  * Given an edits array parsed from partial JSON, drop the last entry when the
66
- * corresponding object in `partialJson` does not yet end with a closed `}`.
66
+ * corresponding object in `partialJson` has not yet closed with `}`.
67
67
  *
68
- * This guards against `partial-json` silently coercing truncated tails like
69
- * `"write":nu` / `"write":nul` into `{ write: null }`, which would make the
70
- * last entry render a spurious null-write error until the value finishes
71
- * streaming.
68
+ * The streaming parser materializes a trailing edit object from the fields seen
69
+ * so far before its closing `}` arrives, so an unfinished last entry can render
70
+ * as a (partial) edit mid-stream. Dropping it until the object closes keeps the
71
+ * preview from showing an incomplete edit.
72
72
  */
73
73
  export declare function dropIncompleteLastEdit<T>(edits: readonly T[], partialJson: string | undefined, listKey: string): T[];
74
74
  export declare const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>>;
@@ -1,7 +1,6 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
2
  import type { InteractiveModeContext } from "../../modes/types";
3
3
  import type { SessionObserverRegistry } from "../session-observer-registry";
4
- export declare function formatTemporaryModelStatus(modelLabel: string, roleSelectorHint?: string): string;
5
4
  export declare class SelectorController {
6
5
  #private;
7
6
  private ctx;
@@ -318,6 +318,7 @@ export interface BuildSystemPromptOptions {
318
318
  customPrompt?: string;
319
319
  appendPrompt?: string;
320
320
  inlineToolDescriptors?: boolean;
321
+ includeWorkspaceTree?: boolean;
321
322
  }
322
323
  /**
323
324
  * Build the default provider-facing system prompt blocks.
@@ -110,6 +110,8 @@ export interface BuildSystemPromptOptions {
110
110
  model?: string;
111
111
  /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
112
112
  personality?: Personality;
113
+ /** Whether to include the workspace directory tree in the system prompt. Default: false */
114
+ includeWorkspaceTree?: boolean;
113
115
  }
114
116
  /** Result of building provider-facing system prompt messages. */
115
117
  export interface BuildSystemPromptResult {
@@ -0,0 +1,39 @@
1
+ import type { ElementHandle, Page } from "puppeteer-core";
2
+ export interface AriaSnapshotOptions {
3
+ /** Maximum tree depth to render. */
4
+ depth?: number;
5
+ /** Append `[box=x,y,w,h]` bounding boxes to each node. */
6
+ boxes?: boolean;
7
+ }
8
+ /**
9
+ * Capture a Playwright-format ARIA snapshot of `root` (or the whole document when
10
+ * null). Always runs in `ai` mode so every node carries a `[ref=eN]` id; resolve
11
+ * those to elements with {@link resolveAriaRefHandle}. Ids are renumbered from e1
12
+ * on each call and remain valid until the next snapshot.
13
+ */
14
+ export declare function captureAriaSnapshot(page: Page, root: ElementHandle | null, options?: AriaSnapshotOptions): Promise<string>;
15
+ /**
16
+ * Resolve a `[ref=eN]` id from the latest snapshot to a live `ElementHandle`, or
17
+ * null when the ref no longer matches any element. Runs in the main world so it
18
+ * sees the `_ariaRef` expandos the snapshot wrote.
19
+ */
20
+ export declare function resolveAriaRefHandle(page: Page, ref: string): Promise<ElementHandle | null>;
21
+ /**
22
+ * Recognize the explicit `[ref=eN]` selector forms and return the bare ref id,
23
+ * else null. Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, and
24
+ * `ariaref/e5` — lets `tab.click("aria-ref=e5")` etc. act on snapshot refs. A
25
+ * bare `e5` is intentionally NOT a ref selector: the cmux backend already uses
26
+ * bare `eN`/`@eN` for its own observe ids, so requiring the prefix keeps action
27
+ * selectors meaning the same thing on both backends. (`tab.ref("e5")` still
28
+ * accepts a bare id directly.)
29
+ */
30
+ export declare function parseAriaRefSelector(selector: string): string | null;
31
+ /**
32
+ * Build a self-contained expression script that runs the vendored bundle in the
33
+ * page and returns the ARIA snapshot YAML. Used by the cmux backend, whose
34
+ * `browser.eval` RPC takes a script string and returns the completion value (it
35
+ * has no ElementHandle to pass in). The script resolves `selector` via
36
+ * `document.querySelector` in-page (CSS selectors only) or falls back to the
37
+ * whole document. Like the puppeteer path it installs nothing on `window`.
38
+ */
39
+ export declare function buildAriaSnapshotScript(selector: string | undefined, options?: AriaSnapshotOptions): string;
@@ -1,5 +1,6 @@
1
1
  import { JsRuntime } from "../../../eval/js/shared/runtime";
2
2
  import type { ToolSession } from "../../../sdk";
3
+ import { type AriaSnapshotOptions } from "../aria/aria-snapshot";
3
4
  import { type ReadableFormat } from "../readable";
4
5
  import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
5
6
  import type { CmuxSocketClient } from "./socket-client";
@@ -75,6 +76,8 @@ export declare class CmuxTab {
75
76
  timeoutMs?: number;
76
77
  }): Promise<void>;
77
78
  observe(opts?: ObserveOptions): Promise<Observation>;
79
+ ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise<string>;
80
+ ref(id: string): Promise<CmuxElementHandle>;
78
81
  click(selector: string): Promise<void>;
79
82
  dblclick(selector: string): Promise<void>;
80
83
  hover(selector: string): Promise<void>;
@@ -4,6 +4,7 @@ import type { ToolSession } from "../sdk";
4
4
  import { type BrowserKindTag } from "./browser/registry";
5
5
  import type { Observation, ScreenshotResult } from "./browser/tab-protocol";
6
6
  import type { OutputMeta } from "./output-meta";
7
+ export { type AriaSnapshotOptions, buildAriaSnapshotScript, parseAriaRefSelector, } from "./browser/aria/aria-snapshot";
7
8
  export { cmuxSnapshotToObservation, mapWaitUntil, resolveCmuxKind, serializeEval } from "./browser/cmux/rpc";
8
9
  export { CmuxSocketClient } from "./browser/cmux/socket-client";
9
10
  export { extractReadableFromHtml, type ReadableFormat, type ReadableResult } from "./browser/readable";
@@ -60,8 +60,7 @@ export declare class FindTool implements AgentTool<typeof findSchema, FindToolDe
60
60
  private readonly session;
61
61
  readonly name = "find";
62
62
  readonly approval: "read";
63
- readonly summary = "Find files and directories matching a glob pattern";
64
- readonly loadMode = "discoverable";
63
+ readonly loadMode = "essential";
65
64
  readonly label = "Find";
66
65
  readonly description: string;
67
66
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
@@ -40,8 +40,7 @@ export declare class WriteTool implements AgentTool<typeof writeSchema, WriteToo
40
40
  }, {}>;
41
41
  readonly strict = true;
42
42
  readonly concurrency = "exclusive";
43
- readonly loadMode = "discoverable";
44
- readonly summary = "Write content to a file (creates or overwrites)";
43
+ readonly loadMode = "essential";
45
44
  /** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
46
45
  matcherDigest(args: unknown): string | undefined;
47
46
  constructor(session: ToolSession);
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.10",
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.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",
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();
@@ -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,
@@ -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 ?? [])];