@oh-my-pi/pi-coding-agent 14.0.2 → 14.0.4

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.
@@ -43,6 +43,8 @@ export class SessionObserverOverlayComponent extends Container {
43
43
  #observeKeys: KeyId[];
44
44
  /** Cached parsed transcript per session file to avoid reparsing on every refresh */
45
45
  #transcriptCache?: { path: string; bytesRead: number; entries: SessionMessageEntry[] };
46
+ /** Live stats text component, placed after transcript to avoid above-viewport diffs */
47
+ #statsText?: Text;
46
48
 
47
49
  constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
48
50
  super();
@@ -87,11 +89,13 @@ export class SessionObserverOverlayComponent extends Container {
87
89
  this.#mode = "viewer";
88
90
  this.children = [];
89
91
  this.#viewerContainer = new Container();
92
+ this.#statsText = new Text("", 1, 0);
90
93
  this.#refreshViewer();
91
94
 
92
95
  this.addChild(new DynamicBorder());
93
96
  this.addChild(this.#viewerContainer);
94
97
  this.addChild(new Spacer(1));
98
+ this.addChild(this.#statsText);
95
99
  this.addChild(new Text(theme.fg("dim", "Esc: back to picker | Ctrl+S: back to picker"), 1, 0));
96
100
  this.addChild(new DynamicBorder());
97
101
  }
@@ -133,16 +137,17 @@ export class SessionObserverOverlayComponent extends Container {
133
137
  const session = sessions.find(s => s.id === this.#selectedSessionId);
134
138
  if (!session) {
135
139
  this.#viewerContainer.addChild(new Text(theme.fg("dim", "Session no longer available."), 1, 0));
140
+ this.#updateStats(undefined);
136
141
  return;
137
142
  }
138
143
 
139
144
  this.#renderSessionHeader(session);
140
145
  this.#renderSessionTranscript(session);
146
+ this.#updateStats(session);
141
147
  }
142
148
 
143
149
  #renderSessionHeader(session: ObservableSession): void {
144
150
  const c = this.#viewerContainer;
145
- const progress = session.progress;
146
151
 
147
152
  // Header: label + status + [agent]
148
153
  const statusColor = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
@@ -154,17 +159,6 @@ export class SessionObserverOverlayComponent extends Container {
154
159
  c.addChild(new Text(theme.fg("muted", session.description), 1, 0));
155
160
  }
156
161
 
157
- // Stats from progress
158
- if (progress) {
159
- const stats: string[] = [];
160
- if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
161
- if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
162
- if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
163
- if (stats.length > 0) {
164
- c.addChild(new Text(theme.fg("dim", stats.join(theme.sep.dot)), 1, 0));
165
- }
166
- }
167
-
168
162
  if (session.sessionFile) {
169
163
  c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(session.sessionFile)}`), 1, 0));
170
164
  }
@@ -172,6 +166,21 @@ export class SessionObserverOverlayComponent extends Container {
172
166
  c.addChild(new DynamicBorder());
173
167
  }
174
168
 
169
+ /** Update live stats in-place (below transcript, within viewport). */
170
+ #updateStats(session: ObservableSession | undefined): void {
171
+ if (!this.#statsText) return;
172
+ const progress = session?.progress;
173
+ if (!progress) {
174
+ this.#statsText.setText("");
175
+ return;
176
+ }
177
+ const stats: string[] = [];
178
+ if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
179
+ if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
180
+ if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
181
+ this.#statsText.setText(stats.length > 0 ? theme.fg("dim", stats.join(theme.sep.dot)) : "");
182
+ }
183
+
175
184
  /** Incrementally read and parse the session JSONL, caching already-parsed entries. */
176
185
  #loadTranscript(sessionFile: string): SessionMessageEntry[] | null {
177
186
  // Invalidate cache if session file changed (e.g. switched to different subagent)
@@ -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
 
@@ -62,3 +62,9 @@ _Full diff too large ({{len files}} files). Showing first ~{{linesPerFile}} line
62
62
  {{rawDiff}}
63
63
  </diff>
64
64
  {{/if}}
65
+
66
+ {{#if additionalInstructions}}
67
+ ### Additional Instructions
68
+
69
+ {{additionalInstructions}}
70
+ {{/if}}
@@ -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}}
@@ -7,11 +7,20 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
7
7
  - replacements: `chunk#CRC` or `chunk#CRC@region`
8
8
  - Without a `@region` it defaults to the entire chunk including leading trivia. Valid regions: `head`, `body`, `tail`, `decl`.
9
9
  - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
10
- - Use a single leading space per indent level 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 `@body` of a method, write the body starting at column 0:
11
12
  ```
12
- content: "if (x) {\n return true;\n}"
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.
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 `@body` 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}}
15
24
  - `@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
25
  - `replace` requires the current CRC. Insertions do not.
17
26
  - **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.
@@ -19,6 +28,10 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
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.
31
+
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 `@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.
33
+
34
+ **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.
22
35
  </critical>
23
36
 
24
37
  <regions>
@@ -108,9 +121,13 @@ Given this `read` output for `example.ts`:
108
121
  ```
109
122
 
110
123
  **Replace a whole chunk** (rename a function):
111
- ```
112
- { "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" }
113
- ```
124
+ ~~~json
125
+ {{#if chunkAutoIndent}}
126
+ { "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" }
127
+ {{else}}
128
+ { "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" }
129
+ {{/if}}
130
+ ~~~
114
131
  Result — the entire chunk is rewritten:
115
132
  ```
116
133
  function makeCounter(start: number): Counter {
@@ -158,9 +175,13 @@ function createCounter(initial: number): Counter {
158
175
  ```
159
176
 
160
177
  **Insert after a chunk** (`after`):
161
- ```
162
- { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
163
- ```
178
+ ~~~json
179
+ {{#if chunkAutoIndent}}
180
+ { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
181
+ {{else}}
182
+ { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
183
+ {{/if}}
184
+ ~~~
164
185
  Result — a new function appears after the enum:
165
186
  ```
166
187
  enum Status {
@@ -189,9 +210,13 @@ class Counter {
189
210
  ```
190
211
 
191
212
  **Append inside a container** (`@body` + `append`):
192
- ```
193
- { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
194
- ```
213
+ ~~~json
214
+ {{#if chunkAutoIndent}}
215
+ { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
216
+ {{else}}
217
+ { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
218
+ {{/if}}
219
+ ~~~
195
220
  Result — a new method is added at the end of the class body, before the closing `}`:
196
221
  ```
197
222
  toString(): string {
@@ -210,10 +235,18 @@ Result — a new method is added at the end of the class body, before the closin
210
235
  ```
211
236
  Result — the method is removed from the class.
212
237
  - Indentation rules (important):
213
- - Use one leading space for each indent level in canonical chunk-edit content. The tool expands those levels to the file's actual style (2-space, 4-space, tabs, etc.).
238
+ {{#if chunkAutoIndent}}
239
+ - Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
240
+ {{else}}
241
+ - 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.
242
+ {{/if}}
214
243
  - Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
215
244
  - For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
216
245
  - For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
217
- - For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
246
+ {{#if chunkAutoIndent}}
247
+ - For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
248
+ {{else}}
249
+ - For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
250
+ {{/if}}
218
251
  - The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
219
252
  </examples>
@@ -9,6 +9,12 @@ Each opening anchor `[< full.chunk.path#CCCC ]` in the default output identifies
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
+ {{#if chunkAutoIndent}}
13
+ Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
14
+ {{else}}
15
+ Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
16
+ {{/if}}
17
+
12
18
  Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
13
19
  </instruction>
14
20
 
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()) : [];
@@ -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, "/");
@@ -1,7 +1,7 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ToolChoice } from "@oh-my-pi/pi-ai";
3
3
  import type { SearchDb } from "@oh-my-pi/pi-natives";
4
- import { $env, logger } from "@oh-my-pi/pi-utils";
4
+ import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
5
5
  import type { AsyncJobManager } from "../async";
6
6
  import type { PromptTemplate } from "../config/prompt-templates";
7
7
  import type { Settings } from "../config/settings";
@@ -297,8 +297,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
297
297
  !skipPythonPreflight &&
298
298
  pythonMode !== "bash-only" &&
299
299
  (requestedTools === undefined || requestedTools.includes("python"));
300
- const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
301
- const skipPythonWarm = isTestEnv || $env.PI_PYTHON_SKIP_CHECK === "1";
300
+ const skipPythonWarm = isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK");
302
301
  if (shouldCheckPython) {
303
302
  const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
304
303
  pythonAvailable = availability.ok;
package/src/tools/read.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  parseChunkReadPath,
15
15
  parseChunkSelector,
16
16
  resolveAnchorStyle,
17
+ resolveChunkAutoIndent,
17
18
  } from "../edit/modes/chunk";
18
19
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
19
20
  import { parseInternalUrl } from "../internal-urls/parse";
@@ -449,6 +450,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
449
450
  resolveEditMode(session) === "chunk"
450
451
  ? prompt.render(readChunkDescription, {
451
452
  anchorStyle: resolveAnchorStyle(session.settings),
453
+ chunkAutoIndent: resolveChunkAutoIndent(),
452
454
  })
453
455
  : prompt.render(readDescription, {
454
456
  DEFAULT_LIMIT: String(this.#defaultLimit),
@@ -8,7 +8,7 @@
8
8
  import { Database } from "bun:sqlite";
9
9
  import path from "node:path";
10
10
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
- import { $env, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
11
+ import { $flag, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
12
12
  import { Type } from "@sinclair/typebox";
13
13
  import type { Settings } from "..";
14
14
  import type { ToolSession } from "./index";
@@ -19,7 +19,7 @@ const ReportToolIssueParams = Type.Object({
19
19
  });
20
20
 
21
21
  export function isAutoQaEnabled(settings?: Settings): boolean {
22
- return $env.PI_AUTO_QA === "1" || !!settings?.get("dev.autoqa");
22
+ return $flag("PI_AUTO_QA") || !!settings?.get("dev.autoqa");
23
23
  }
24
24
 
25
25
  export function getAutoQaDbPath(): string {
@@ -1,4 +1,4 @@
1
- import { $env } from "@oh-my-pi/pi-utils";
1
+ import { $env, $flag } from "@oh-my-pi/pi-utils";
2
2
 
3
3
  export type EditMode = "replace" | "patch" | "hashline" | "chunk";
4
4
 
@@ -36,7 +36,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
36
36
  const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
37
37
  if (envMode) return envMode;
38
38
 
39
- if ($env.PI_STRICT_EDIT_MODE === "1") {
39
+ if (!$flag("PI_STRICT_EDIT_MODE")) {
40
40
  if (activeModel?.includes("spark")) return "replace";
41
41
  if (activeModel?.includes("nano")) return "replace";
42
42
  if (activeModel?.includes("mini")) return "replace";