@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.
- package/CHANGELOG.md +30 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +17 -3
- package/src/edit/index.ts +2 -0
- package/src/edit/modes/chunk.ts +37 -13
- package/src/edit/modes/hashline.ts +2 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +34 -13
- package/src/ipy/executor.ts +3 -7
- package/src/ipy/kernel.ts +3 -3
- package/src/lsp/config.ts +18 -2
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/utils.ts +1 -143
- package/src/modes/components/session-observer-overlay.ts +21 -12
- package/src/modes/theme/theme.ts +2 -161
- package/src/prompts/review-request.md +6 -0
- package/src/prompts/system/system-prompt.md +40 -2
- package/src/prompts/tools/chunk-edit.md +46 -13
- package/src/prompts/tools/read-chunk.md +6 -0
- package/src/sdk.ts +2 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/index.ts +2 -3
- package/src/tools/read.ts +2 -0
- package/src/tools/report-tool-issue.ts +2 -2
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/lang-from-path.ts +239 -0
- package/src/utils/sixel.ts +2 -2
|
@@ -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)
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -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
|
|
|
@@ -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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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") || $
|
|
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()) : [];
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -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 =
|
|
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, "/");
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
|
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 { $
|
|
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 $
|
|
22
|
+
return $flag("PI_AUTO_QA") || !!settings?.get("dev.autoqa");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function getAutoQaDbPath(): string {
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -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 (
|
|
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";
|