@oh-my-pi/pi-coding-agent 14.0.3 → 14.0.5

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 (43) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/package.json +11 -8
  3. package/src/config/model-registry.ts +3 -2
  4. package/src/config/model-resolver.ts +33 -25
  5. package/src/config/settings.ts +9 -2
  6. package/src/dap/session.ts +31 -39
  7. package/src/debug/log-formatting.ts +2 -2
  8. package/src/edit/index.ts +2 -0
  9. package/src/edit/modes/chunk.ts +45 -16
  10. package/src/edit/modes/hashline.ts +2 -2
  11. package/src/ipy/executor.ts +3 -7
  12. package/src/ipy/kernel.ts +3 -3
  13. package/src/lsp/client.ts +4 -2
  14. package/src/lsp/index.ts +4 -9
  15. package/src/lsp/lspmux.ts +2 -2
  16. package/src/lsp/utils.ts +27 -143
  17. package/src/modes/components/diff.ts +1 -1
  18. package/src/modes/controllers/event-controller.ts +438 -426
  19. package/src/modes/theme/mermaid-cache.ts +5 -7
  20. package/src/modes/theme/theme.ts +2 -161
  21. package/src/priority.json +8 -0
  22. package/src/prompts/agents/designer.md +1 -2
  23. package/src/prompts/system/system-prompt.md +40 -2
  24. package/src/prompts/tools/chunk-edit.md +66 -38
  25. package/src/prompts/tools/read-chunk.md +10 -1
  26. package/src/sdk.ts +2 -1
  27. package/src/session/agent-session.ts +10 -0
  28. package/src/session/compaction/compaction.ts +1 -1
  29. package/src/tools/ast-edit.ts +2 -2
  30. package/src/tools/browser.ts +84 -21
  31. package/src/tools/fetch.ts +1 -1
  32. package/src/tools/find.ts +40 -94
  33. package/src/tools/gemini-image.ts +1 -0
  34. package/src/tools/index.ts +2 -3
  35. package/src/tools/read.ts +2 -0
  36. package/src/tools/render-utils.ts +1 -1
  37. package/src/tools/report-tool-issue.ts +2 -2
  38. package/src/utils/edit-mode.ts +2 -2
  39. package/src/utils/image-resize.ts +73 -37
  40. package/src/utils/lang-from-path.ts +239 -0
  41. package/src/utils/sixel.ts +2 -2
  42. package/src/web/scrapers/types.ts +50 -32
  43. package/src/web/search/providers/codex.ts +21 -2
@@ -1,7 +1,6 @@
1
1
  import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
2
2
 
3
- const cache = new Map<bigint, string>();
4
- const failed = new Set<bigint>();
3
+ const cache = new Map<bigint | number, string | null>();
5
4
 
6
5
  let onRenderNeeded: (() => void) | null = null;
7
6
 
@@ -16,7 +15,7 @@ export function setMermaidRenderCallback(callback: (() => void) | null): void {
16
15
  * Get a pre-rendered mermaid ASCII diagram by hash.
17
16
  * Returns null if not cached or rendering failed.
18
17
  */
19
- export function getMermaidAscii(hash: bigint): string | null {
18
+ export function getMermaidAscii(hash: bigint | number): string | null {
20
19
  return cache.get(hash) ?? null;
21
20
  }
22
21
 
@@ -31,14 +30,14 @@ export function prerenderMermaid(markdown: string): void {
31
30
  let hasNew = false;
32
31
 
33
32
  for (const { source, hash } of blocks) {
34
- if (cache.has(hash) || failed.has(hash)) continue;
33
+ if (cache.has(hash)) continue;
35
34
 
36
35
  const ascii = renderMermaidAsciiSafe(source);
37
36
  if (ascii) {
38
37
  cache.set(hash, ascii);
39
38
  hasNew = true;
40
39
  } else {
41
- failed.add(hash);
40
+ cache.set(hash, null);
42
41
  }
43
42
  }
44
43
 
@@ -58,7 +57,7 @@ export function prerenderMermaid(markdown: string): void {
58
57
  */
59
58
  export function hasPendingMermaid(markdown: string): boolean {
60
59
  const blocks = extractMermaidBlocks(markdown);
61
- return blocks.some(({ hash }) => !cache.has(hash) && !failed.has(hash));
60
+ return blocks.some(({ hash }) => !cache.has(hash));
62
61
  }
63
62
 
64
63
  /**
@@ -66,5 +65,4 @@ export function hasPendingMermaid(markdown: string): boolean {
66
65
  */
67
66
  export function clearMermaidCache(): void {
68
67
  cache.clear();
69
- failed.clear();
70
68
  }
@@ -20,6 +20,8 @@ import { defaultThemes } from "./defaults";
20
20
  import lightThemeJson from "./light.json" with { type: "json" };
21
21
  import { getMermaidAscii } from "./mermaid-cache";
22
22
 
23
+ export { getLanguageFromPath } from "../../utils/lang-from-path";
24
+
23
25
  // ============================================================================
24
26
  // Symbol Presets
25
27
  // ============================================================================
@@ -2305,167 +2307,6 @@ export function highlightCode(code: string, lang?: string): string[] {
2305
2307
  }
2306
2308
  }
2307
2309
 
2308
- /**
2309
- * Get language identifier from file path extension.
2310
- */
2311
- export function getLanguageFromPath(filePath: string): string | undefined {
2312
- const baseName = path.basename(filePath).toLowerCase();
2313
- if (baseName === ".env" || baseName.startsWith(".env.")) return "env";
2314
- if (
2315
- baseName === ".gitignore" ||
2316
- baseName === ".gitattributes" ||
2317
- baseName === ".gitmodules" ||
2318
- baseName === ".editorconfig" ||
2319
- baseName === ".npmrc" ||
2320
- baseName === ".prettierrc" ||
2321
- baseName === ".eslintrc"
2322
- ) {
2323
- return "conf";
2324
- }
2325
- if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
2326
- return "dockerfile";
2327
- }
2328
- if (baseName === "justfile") return "just";
2329
- if (baseName === "cmakelists.txt") return "cmake";
2330
-
2331
- const ext = filePath.split(".").pop()?.toLowerCase();
2332
- if (!ext) return undefined;
2333
-
2334
- const extToLang: Record<string, string> = {
2335
- ts: "typescript",
2336
- cts: "typescript",
2337
- mts: "typescript",
2338
- tsx: "tsx",
2339
- js: "javascript",
2340
- jsx: "javascript",
2341
- mjs: "javascript",
2342
- cjs: "javascript",
2343
- py: "python",
2344
- pyi: "python",
2345
- rb: "ruby",
2346
- rbw: "ruby",
2347
- gemspec: "ruby",
2348
- rs: "rust",
2349
- go: "go",
2350
- java: "java",
2351
- kt: "kotlin",
2352
- ktm: "kotlin",
2353
- kts: "kotlin",
2354
- swift: "swift",
2355
- c: "c",
2356
- h: "c",
2357
- cpp: "cpp",
2358
- cc: "cpp",
2359
- cxx: "cpp",
2360
- hh: "cpp",
2361
- hpp: "cpp",
2362
- cu: "cpp",
2363
- ino: "cpp",
2364
- cs: "csharp",
2365
- clj: "clojure",
2366
- cljc: "clojure",
2367
- cljs: "clojure",
2368
- edn: "clojure",
2369
- php: "php",
2370
- sh: "bash",
2371
- bash: "bash",
2372
- zsh: "bash",
2373
- ksh: "bash",
2374
- bats: "bash",
2375
- tmux: "bash",
2376
- cgi: "bash",
2377
- fcgi: "bash",
2378
- command: "bash",
2379
- tool: "bash",
2380
- fish: "fish",
2381
- ps1: "powershell",
2382
- psm1: "powershell",
2383
- sql: "sql",
2384
- html: "html",
2385
- htm: "html",
2386
- xhtml: "html",
2387
- astro: "astro",
2388
- vue: "vue",
2389
- svelte: "svelte",
2390
- css: "css",
2391
- scss: "scss",
2392
- sass: "sass",
2393
- less: "less",
2394
- json: "json",
2395
- ipynb: "ipynb",
2396
- hbs: "handlebars",
2397
- hsb: "handlebars",
2398
- handlebars: "handlebars",
2399
- yaml: "yaml",
2400
- yml: "yaml",
2401
- toml: "toml",
2402
- xml: "xml",
2403
- xsl: "xml",
2404
- xslt: "xml",
2405
- svg: "xml",
2406
- plist: "xml",
2407
- md: "markdown",
2408
- markdown: "markdown",
2409
- mdx: "markdown",
2410
- diff: "diff",
2411
- patch: "diff",
2412
- dockerfile: "dockerfile",
2413
- containerfile: "dockerfile",
2414
- makefile: "make",
2415
- justfile: "just",
2416
- mk: "make",
2417
- mak: "make",
2418
- cmake: "cmake",
2419
- lua: "lua",
2420
- jl: "julia",
2421
- pl: "perl",
2422
- pm: "perl",
2423
- perl: "perl",
2424
- r: "r",
2425
- scala: "scala",
2426
- sc: "scala",
2427
- sbt: "scala",
2428
- ex: "elixir",
2429
- exs: "elixir",
2430
- erl: "erlang",
2431
- hs: "haskell",
2432
- nix: "nix",
2433
- odin: "odin",
2434
- zig: "zig",
2435
- star: "starlark",
2436
- bzl: "starlark",
2437
- sol: "solidity",
2438
- v: "verilog",
2439
- sv: "verilog",
2440
- svh: "verilog",
2441
- vh: "verilog",
2442
- m: "objc",
2443
- mm: "objc",
2444
- ml: "ocaml",
2445
- vim: "vim",
2446
- graphql: "graphql",
2447
- proto: "protobuf",
2448
- tf: "hcl",
2449
- hcl: "hcl",
2450
- tfvars: "hcl",
2451
- txt: "text",
2452
- text: "text",
2453
- log: "log",
2454
- csv: "csv",
2455
- tsv: "tsv",
2456
- ini: "ini",
2457
- cfg: "conf",
2458
- conf: "conf",
2459
- config: "conf",
2460
- properties: "conf",
2461
- tla: "tlaplus",
2462
- tlaplus: "tlaplus",
2463
- env: "env",
2464
- };
2465
-
2466
- return extToLang[ext];
2467
- }
2468
-
2469
2310
  export function getSymbolTheme(): SymbolTheme {
2470
2311
  const preset = theme.getSymbolPreset();
2471
2312
 
package/src/priority.json CHANGED
@@ -25,5 +25,13 @@
25
25
  "opus-4.1",
26
26
  "opus-4-1",
27
27
  "pro"
28
+ ],
29
+ "designer": [
30
+ "google-gemini-cli/gemini-3.1-pro",
31
+ "google-gemini-cli/gemini-3-pro",
32
+ "gemini-3.1-pro",
33
+ "gemini-3-1-pro",
34
+ "gemini-3-pro",
35
+ "gemini-3"
28
36
  ]
29
37
  }
@@ -1,8 +1,7 @@
1
1
  ---
2
2
  name: designer
3
3
  description: UI/UX specialist for design implementation, review, visual refinement
4
- spawns: explore
5
- model: google-gemini-cli/gemini-3.1-pro, google-gemini-cli/gemini-3-pro, gemini-3.1-pro, gemini-3-1-pro, gemini-3-pro, gemini-3, pi/default
4
+ model: pi/designer
6
5
  ---
7
6
 
8
7
  You are an expert UI/UX designer implementing and reviewing UI designs.
@@ -52,9 +52,29 @@ Push back when warranted: state the downside, propose an alternative, but **MUST
52
52
  <communication>
53
53
  - No emojis, filler, or ceremony.
54
54
  - (1) Correctness first, (2) Brevity second, (3) Politeness third.
55
- - User-supplied content **MUST** override any other guidelines.
55
+ - Prefer concise, information-dense writing.
56
+ - Avoid repeating the user's request or narrating routine tool calls.
56
57
  </communication>
57
58
 
59
+ <instruction-priority>
60
+ - User instructions override default style, tone, formatting, and initiative preferences.
61
+ - Higher-priority system constraints about safety, permissions, tool boundaries, and task completion do not yield.
62
+ - If a newer user instruction conflicts with an earlier user instruction, follow the newer one.
63
+ - Preserve earlier instructions that do not conflict.
64
+ </instruction-priority>
65
+
66
+ <output-contract>
67
+ - Brief preambles are allowed when they improve orientation, but they **MUST** stay short and **MUST NOT** be treated as completion.
68
+ - Claims about code, tools, tests, docs, or external sources **MUST** be grounded in what you actually observed. If a statement is an inference, say so.
69
+ - Apply brevity to prose, not to evidence, verification, or blocking details.
70
+ </output-contract>
71
+
72
+ <default-follow-through>
73
+ - If the user's intent is clear and the next step is reversible and low-risk, proceed without asking.
74
+ - Ask only when the next step is irreversible, has external side effects, or requires a missing choice that would materially change the outcome.
75
+ - If you proceed, state what you did, what you verified, and what remains optional.
76
+ </default-follow-through>
77
+
58
78
  <behavior>
59
79
  You **MUST** guard against the completion reflex — the urge to ship something that compiles before you've understood the problem:
60
80
  - Compiling ≠ Correctness. "It works" ≠ "Works in all cases".
@@ -248,6 +268,15 @@ Don't open a file hoping. Hope is not a strategy.
248
268
  {{#has tools "task"}}- `task` for investigate+edit in one pass — prefer this over a separate explore→task chain{{/has}}
249
269
  {{/ifAny}}
250
270
 
271
+ <tool-persistence>
272
+ - Use tools whenever they materially improve correctness, completeness, or grounding.
273
+ - Do not stop at the first plausible answer if another tool call would materially reduce uncertainty, verify a dependency, or improve coverage.
274
+ - Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required. Resolve prerequisites first.
275
+ - If a lookup is empty, partial, or suspiciously narrow, retry with a different strategy before concluding nothing exists.
276
+ - When multiple retrieval steps are independent, parallelize them. When one result determines the next step, keep the workflow sequential.
277
+ - After parallel retrieval, pause to synthesize before making more calls.
278
+ </tool-persistence>
279
+
251
280
  {{#if (includes tools "inspect_image")}}
252
281
  ### Image inspection
253
282
  - For image understanding tasks: **MUST** use `inspect_image` over `read` to avoid overloading main session context.
@@ -262,7 +291,14 @@ These are inviolable. Violation is system failure.
262
291
  - You **MUST NOT** suppress tests to make code pass. You **MUST NOT** fabricate outputs not observed.
263
292
  - You **MUST NOT** solve the wished-for problem instead of the actual problem.
264
293
  - You **MUST NOT** ask for information obtainable from tools, repo context, or files.
265
- - You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibiltity layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
294
+ - You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibility layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
295
+
296
+ <completeness-contract>
297
+ - Treat the task as incomplete until every requested deliverable is done or explicitly marked [blocked].
298
+ - Keep an internal checklist of requested outcomes, implied cleanup, affected callsites, tests, docs, and follow-on edits.
299
+ - For lists, batches, paginated results, or multi-file migrations, determine expected scope when possible and confirm coverage before yielding.
300
+ - If something is blocked, label it [blocked], say exactly what is missing, and distinguish it from work that is complete.
301
+ </completeness-contract>
266
302
 
267
303
  # Design Integrity
268
304
 
@@ -280,6 +316,7 @@ Design integrity means the code tells the truth about what the system currently
280
316
  {{#has tools "task"}}- You **MUST** determine if the task is parallelizable via `task` tool.{{/has}}
281
317
  - If multi-file or imprecisely scoped, you **MUST** write out a step-by-step plan, phased if it warrants, before touching any file.
282
318
  - For new work, you **MUST**: (1) think about architecture, (2) search official docs/papers on best practices, (3) review existing codebase, (4) compare research with codebase, (5) implement the best fit or surface tradeoffs.
319
+ - If required context is missing, do **NOT** guess. Prefer tool-based retrieval first, ask a minimal question only when the answer cannot be recovered from tools, repo context, or files.
283
320
  ## 2. Before You Edit
284
321
  - Read the relevant section of any file before editing. Don't edit from a grep snippet alone — context above and below the match changes what the correct edit is.
285
322
  - You **MUST** grep for existing examples before implementing any pattern, utility, or abstraction. If the codebase already solves it, you **MUST** use that. Inventing a parallel convention is **PROHIBITED**.
@@ -313,6 +350,7 @@ When a tool call fails, read the full error before doing anything else. When a f
313
350
  - Test everything rigorously → Future contributor cannot break behavior without failure. Prefer unit/e2e.
314
351
  - You **MUST NOT** rely on mocks — they invent behaviors that never happen in production and hide real bugs.
315
352
  - You **SHOULD** run only tests you added/modified unless asked otherwise.
353
+ - Before yielding, verify: (1) every requirement is satisfied, (2) claims match files/tool output/source material, (3) the output format matches the ask, and (4) any high-impact action was either verified or explicitly held for permission.
316
354
  - You **MUST NOT** yield without proof when non-trivial work, self-assessment is deceptive: tests, linters, type checks, repro steps… exhaust all external verification.
317
355
 
318
356
  {{#if secretsEnabled}}
@@ -3,58 +3,66 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
3
3
  <rules>
4
4
  - **MUST** `read` first. Never invent chunk paths or CRCs. Copy them from the latest `read` output or edit response.
5
5
  - `sel` format:
6
- - insertions: `chunk` or `chunk@region`
7
- - replacements: `chunk#CRC` or `chunk#CRC@region`
8
- - Without a `@region` it defaults to the entire chunk including leading trivia. Valid regions: `head`, `body`, `tail`, `decl`.
6
+ - insertions: `chunk`, `chunk~`, or `chunk^`
7
+ - replacements: `chunk#CRC`, `chunk#CRC~`, or `chunk#CRC^`
8
+ - Without a suffix it defaults to the entire chunk including leading trivia. `~` targets the body, `^` targets the head.
9
9
  - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
10
- - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
10
+ {{#if chunkAutoIndent}}
11
+ - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0:
11
12
  ```
12
13
  content: "if (x) {\n\treturn true;\n}"
13
14
  ```
14
15
  The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
15
- - `@region` only works on container chunks (classes, functions, impl blocks, sections). Do **not** use `@head`/`@body`/`@tail` on leaf chunks (enum variants, fields, single statements) — use the whole chunk instead.
16
+ {{else}}
17
+ - Match the file's literal tabs/spaces in `content`. Do not convert indentation to canonical `\t`.
18
+ - Write content at indent-level 0 relative to the target region. For example, to replace `~` of a method, write:
19
+ ```
20
+ content: "if (x) {\n return true;\n}"
21
+ ```
22
+ The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
23
+ {{/if}}
24
+ - Region suffixes only apply to container chunks (classes, functions, impl blocks, sections). On leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` silently fall back to whole-chunk replacement — prefer the unsuffixed form and always supply the complete replacement (condition + body, not just the body) to avoid dropping structural parts.
16
25
  - `replace` requires the current CRC. Insertions do not.
17
- - **CRCs change after every edit.** Always use the selectors/CRCs from the most recent `read` or edit response. Never reuse a CRC from a previous edit.
26
+ - **CRCs change after every edit.** The edit response always carries the new CRCs use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse a CRC from before the latest edit.
18
27
  </rules>
19
28
 
20
29
  <critical>
21
30
  You **MUST** use the narrowest region that covers your change. Replacing without a region replaces the **entire chunk including leading comments, decorators, and attributes** — omitting them from `content` deletes them.
22
31
 
23
- **`replace` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before replacing `@body` on any chunk, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent.
32
+ **`replace` is total, not surgical.** The `content` you supply becomes the *complete* new content for the targeted region. Everything in the original region that you omit from `content` is deleted. Before replacing `~` on any chunk, verify the chunk does not contain children you intend to keep. If a chunk spans hundreds of lines and your change touches only a few, target a specific child chunk — not the parent.
24
33
 
25
- **Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). Replacing `@body` on a group chunk replaces **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not replace the parent group.
34
+ **Group chunks (`stmts_*`, `imports_*`, `decls_*`) are containers.** They hold many sibling items (test functions, import statements, declarations). Replacing `~` on a group chunk replaces **all** of its children. To edit one item inside a group, target that item's own chunk path. If no child chunk exists, use the specific child's chunk selector from `read` output — do not replace the parent group.
26
35
  </critical>
27
36
 
28
37
  <regions>
38
+ Given a chunk like:
29
39
  ```
30
- @head ··· ┊ /// doc comment
31
- · ┊ #[attr]
32
- @decl ··· ┊ fn foo(x: i32) {
33
- @body ·· ┊ body();
34
- @tail ·· ┊ }
40
+ /// doc comment <-- leading trivia
41
+ #[attr] <-- leading trivia
42
+ fn foo(x: i32) { <-- signature + opening delimiter
43
+ body(); <-- body
44
+ } <-- closing delimiter
35
45
  ```
36
- - `@body` — the interior only. **Use for most edits.**
37
- - `@head` — leading trivia + signature + opening delimiter.
38
- - `@tail` — the closing delimiter.
39
- - `@decl` — everything except leading trivia (signature + body + closing delimiter).
40
- - *(no region)* — the entire chunk including leading trivia. Same as `@head` + `@body` + `@tail`.
41
46
 
42
- For leaf chunks (fields, variants, single-line items), `@body` falls back to the full chunk.
47
+ Append `~` to target the body, `^` to target the head (trivia + signature), or nothing for the whole chunk:
48
+ - `fn_foo#CRC~` — body only. **Use for most edits.** On leaf chunks, falls back to whole chunk.
49
+ - `fn_foo#CRC^` — head (decorators, attributes, doc comments, signature, opening delimiter).
50
+ - `fn_foo#CRC` — entire chunk including leading trivia.
51
+ - `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*.
52
+
53
+ **Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#CRC` in the `?` listing) rather than as part of the function's `^`. If you need to rewrite a decorator, check the `?` listing for a sibling `chunk#CRC` directly above your target.
43
54
 
44
- `append`/`prepend` without a `@region` inserts _outside_ the chunk. To add children _inside_ a class, struct, enum, or function body, use `@body`:
45
- - `class_Foo@body` + `append` → adds inside the class before `}`
46
- - `class_Foo@body` + `prepend` → adds inside the class after `{`
47
- - `class_Foo` + `append` → adds after the entire class (after `}`)
55
+ **Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, fenced code blocks, frontmatter), `^` and `~` fall back to the whole chunk. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers) in your `content` omitting them deletes them. For markdown sections (`sect_*`), always use unsuffixed whole-chunk replace — `^` and `~` on section containers also fall back to whole-chunk replace. When editing fenced code blocks in markdown, use the exact whitespace from the file (read with `raw` first) — the tool preserves literal indentation inside fenced blocks, but any content you supply is written verbatim. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
48
56
  </regions>
49
57
 
50
58
  <ops>
51
59
  |op|sel|effect|
52
60
  |---|---|---|
53
- |`replace`|`chunk#CRC(@region)?`|rewrite the addressed region|
54
- |`before`|`chunk(@region)?`|insert before the region span|
55
- |`after`|`chunk(@region)?`|insert after the region span|
56
- |`prepend`|`chunk(@region)?`|insert at the start inside the region|
57
- |`append`|`chunk(@region)?`|insert at the end inside the region|
61
+ |`replace`|`chunk#CRC`, `chunk#CRC~`, or `chunk#CRC^`|rewrite the addressed region|
62
+ |`before`|`chunk`, `chunk~`, or `chunk^`|insert before the region span|
63
+ |`after`|`chunk`, `chunk~`, or `chunk^`|insert after the region span|
64
+ |`prepend`|`chunk`, `chunk~`, or `chunk^`|insert at the start inside the region|
65
+ |`append`|`chunk`, `chunk~`, or `chunk^`|insert at the end inside the region|
58
66
  </ops>
59
67
 
60
68
  <examples>
@@ -113,7 +121,11 @@ Given this `read` output for `example.ts`:
113
121
 
114
122
  **Replace a whole chunk** (rename a function):
115
123
  ~~~json
124
+ {{#if chunkAutoIndent}}
116
125
  { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n\tconst c = new Counter();\n\tc.value = start;\n\treturn c;\n}\n" }
126
+ {{else}}
127
+ { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n const c = new Counter();\n c.value = start;\n return c;\n}\n" }
128
+ {{/if}}
117
129
  ~~~
118
130
  Result — the entire chunk is rewritten:
119
131
  ```
@@ -124,9 +136,9 @@ function makeCounter(start: number): Counter {
124
136
  }
125
137
  ```
126
138
 
127
- **Replace a method body** (`@body`):
139
+ **Replace a method body** (`~`):
128
140
  ```
129
- { "sel": "class_Counter.fn_increment#NQWY@body", "op": "replace", "content": "this.value += 1;\nconsole.log('incremented to', this.value);\n" }
141
+ { "sel": "class_Counter.fn_increment#NQWY~", "op": "replace", "content": "this.value += 1;\nconsole.log('incremented to', this.value);\n" }
130
142
  ```
131
143
  Result — only the body changes, signature and braces are kept:
132
144
  ```
@@ -136,9 +148,9 @@ Result — only the body changes, signature and braces are kept:
136
148
  }
137
149
  ```
138
150
 
139
- **Replace a function header** (`@head` — signature and doc comment):
151
+ **Replace a function header** (`^` — signature and doc comment):
140
152
  ```
141
- { "sel": "fn_createCounter#PQQY@head", "op": "replace", "content": "/** Creates a counter with the given start value. */\nfunction createCounter(initial: number, label?: string): Counter {\n" }
153
+ { "sel": "fn_createCounter#PQQY^", "op": "replace", "content": "/** Creates a counter with the given start value. */\nfunction createCounter(initial: number, label?: string): Counter {\n" }
142
154
  ```
143
155
  Result — adds a doc comment and updates the signature, body untouched:
144
156
  ```
@@ -163,7 +175,11 @@ function createCounter(initial: number): Counter {
163
175
 
164
176
  **Insert after a chunk** (`after`):
165
177
  ~~~json
178
+ {{#if chunkAutoIndent}}
166
179
  { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
180
+ {{else}}
181
+ { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
182
+ {{/if}}
167
183
  ~~~
168
184
  Result — a new function appears after the enum:
169
185
  ```
@@ -180,9 +196,9 @@ function isActive(s: Status): boolean {
180
196
  function createCounter(initial: number): Counter {
181
197
  ```
182
198
 
183
- **Prepend inside a container** (`@body` + `prepend`):
199
+ **Prepend inside a container** (`~` + `prepend`):
184
200
  ```
185
- { "sel": "class_Counter@body", "op": "prepend", "content": "label: string = 'default';\n\n" }
201
+ { "sel": "class_Counter~", "op": "prepend", "content": "label: string = 'default';\n\n" }
186
202
  ```
187
203
  Result — a new field is added at the top of the class body, before existing members:
188
204
  ```
@@ -192,9 +208,13 @@ class Counter {
192
208
  value: number = 0;
193
209
  ```
194
210
 
195
- **Append inside a container** (`@body` + `append`):
211
+ **Append inside a container** (`~` + `append`):
196
212
  ~~~json
197
- { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
213
+ {{#if chunkAutoIndent}}
214
+ { "sel": "class_Counter~", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
215
+ {{else}}
216
+ { "sel": "class_Counter~", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
217
+ {{/if}}
198
218
  ~~~
199
219
  Result — a new method is added at the end of the class body, before the closing `}`:
200
220
  ```
@@ -214,10 +234,18 @@ Result — a new method is added at the end of the class body, before the closin
214
234
  ```
215
235
  Result — the method is removed from the class.
216
236
  - Indentation rules (important):
237
+ {{#if chunkAutoIndent}}
217
238
  - Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
239
+ {{else}}
240
+ - Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
241
+ {{/if}}
218
242
  - Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
219
- - For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
220
- - For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
243
+ - For `~` of a function: write at column 0, and use `\t` for *relative* nesting. Flat body: `"return x;\n"`. Nested body: `"if (cond) {\n\treturn x;\n}\n"` — the `if` is at column 0, the `return` is one tab in, and the tool adds the method's base indent to both.
244
+ - For `^`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
245
+ {{#if chunkAutoIndent}}
221
246
  - For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
247
+ {{else}}
248
+ - For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
249
+ {{/if}}
222
250
  - The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
223
251
  </examples>
@@ -2,16 +2,25 @@ Reads files using syntax-aware chunks.
2
2
 
3
3
  <instruction>
4
4
  - `path` — file path or URL; may include `:selector` suffix
5
- - `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD@body`, `?`, `L50`, `L50-L120`, or `raw`
5
+ - `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD~`, `?`, `L50`, `L50-L120`, or `raw`
6
6
  - `timeout` — seconds, for URLs only
7
7
 
8
8
  Each opening anchor `[< full.chunk.path#CCCC ]` in the default output identifies a chunk. Use `full.chunk.path#CCCC` as-is to read truncated chunks.
9
9
  If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with CRCs.
10
10
  Line numbers in the gutter are absolute file line numbers.
11
11
 
12
+ `L20` (single line, no explicit end) is shorthand for `L20` to end-of-file. Use `L20-L20` for a one-line window.
13
+
14
+ {{#if chunkAutoIndent}}
15
+ Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
16
+ {{else}}
17
+ Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
18
+ {{/if}}
19
+
12
20
  Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
13
21
  </instruction>
14
22
 
15
23
  <critical>
16
24
  - **MUST** `read` before editing — never invent chunk names or CRCs.
25
+ - Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers.
17
26
  </critical>
package/src/sdk.ts CHANGED
@@ -15,6 +15,7 @@ import { SearchDb } from "@oh-my-pi/pi-natives";
15
15
  import type { Component } from "@oh-my-pi/pi-tui";
16
16
  import {
17
17
  $env,
18
+ $flag,
18
19
  getAgentDbPath,
19
20
  getAgentDir,
20
21
  getProjectDir,
@@ -1262,7 +1263,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1262
1263
 
1263
1264
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1264
1265
  const eagerTasks = settings.get("task.eager");
1265
- const intentField = settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1" ? INTENT_FIELD : undefined;
1266
+ const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1266
1267
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1267
1268
  toolContextStore.setToolNames(toolNames);
1268
1269
  const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
@@ -997,6 +997,16 @@ export class AgentSession {
997
997
  this.#lastAssistantMessage = undefined;
998
998
  if (!msg) return;
999
999
 
1000
+ // Invalidate GitHub Copilot credentials on auth failure so stale tokens
1001
+ // aren't reused on the next request
1002
+ if (
1003
+ msg.stopReason === "error" &&
1004
+ msg.provider === "github-copilot" &&
1005
+ msg.errorMessage?.includes("GitHub Copilot authentication failed")
1006
+ ) {
1007
+ await this.#modelRegistry.authStorage.remove("github-copilot");
1008
+ }
1009
+
1000
1010
  if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
1001
1011
  this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
1002
1012
  return;
@@ -761,7 +761,7 @@ function buildOpenAiNativeHistory(
761
761
  if (!msgId) {
762
762
  msgId = `msg_${msgIndex}`;
763
763
  } else if (msgId.length > 64) {
764
- msgId = `msg_${Bun.hash.xxHash64(msgId).toString(36)}`;
764
+ msgId = `msg_${Bun.hash(msgId).toString(36)}`;
765
765
  }
766
766
  input.push({
767
767
  type: "message",
@@ -3,7 +3,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
6
+ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import { computeLineHash } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -103,7 +103,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
103
103
  if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
104
104
  throw new ToolError("limit must be a positive number");
105
105
  }
106
- const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
106
+ const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
107
107
 
108
108
  const formatScopePath = (targetPath: string): string => {
109
109
  const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");