@oh-my-pi/pi-coding-agent 14.6.5 → 14.6.6
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 +6 -0
- package/package.json +7 -7
- package/src/edit/modes/hashline.ts +20 -1
- package/src/modes/components/custom-editor.ts +4 -5
- package/src/modes/controllers/input-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +24 -0
- package/src/prompts/tools/hashline.md +24 -6
- package/src/session/session-manager.ts +57 -0
- package/src/tools/image-gen.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.6.6] - 2026-05-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Ctrl+D draft persistence: pressing Ctrl+D with text in the editor now exits the app and saves the unsent text as a per-session draft. Resuming the same session (e.g. via `--resume`) restores the draft into the editor (one-shot, removed after restore).
|
|
10
|
+
|
|
5
11
|
## [14.6.4] - 2026-05-03
|
|
6
12
|
### Added
|
|
7
13
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.6.
|
|
4
|
+
"version": "14.6.6",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.6.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.6.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.6.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.6.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.6.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.6.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.6.6",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.6.6",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.6.6",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.6.6",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.6.6",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.6.6",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
|
@@ -1486,7 +1486,9 @@ async function executeHashlineSection(
|
|
|
1486
1486
|
export async function executeHashlineSingle(
|
|
1487
1487
|
options: ExecuteHashlineSingleOptions,
|
|
1488
1488
|
): Promise<AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>> {
|
|
1489
|
-
const sections =
|
|
1489
|
+
const sections = mergeSamePathSections(
|
|
1490
|
+
splitHashlineInputs(options.input, { cwd: options.session.cwd, path: options.path }),
|
|
1491
|
+
);
|
|
1490
1492
|
|
|
1491
1493
|
// Fast path: a single section needs no preflight pass.
|
|
1492
1494
|
if (sections.length === 1) return executeHashlineSection({ ...options, ...sections[0] });
|
|
@@ -1518,3 +1520,20 @@ export async function executeHashlineSingle(
|
|
|
1518
1520
|
},
|
|
1519
1521
|
};
|
|
1520
1522
|
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Collapse consecutive or interleaved sections targeting the same path into a
|
|
1526
|
+
* single section with concatenated diffs. Anchors authored against the same
|
|
1527
|
+
* file snapshot must be applied as one batch; otherwise the first sub-edit
|
|
1528
|
+
* shifts line numbers out from under the second's anchors and rebase fails.
|
|
1529
|
+
* Path order is preserved by first occurrence.
|
|
1530
|
+
*/
|
|
1531
|
+
function mergeSamePathSections(sections: HashlineInputSection[]): HashlineInputSection[] {
|
|
1532
|
+
const byPath = new Map<string, string[]>();
|
|
1533
|
+
for (const section of sections) {
|
|
1534
|
+
const existing = byPath.get(section.path);
|
|
1535
|
+
if (existing) existing.push(section.diff);
|
|
1536
|
+
else byPath.set(section.path, [section.diff]);
|
|
1537
|
+
}
|
|
1538
|
+
return Array.from(byPath, ([path, diffs]) => ({ path, diff: diffs.join("\n") }));
|
|
1539
|
+
}
|
|
@@ -197,12 +197,11 @@ export class CustomEditor extends Editor {
|
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// Intercept configured exit shortcut
|
|
200
|
+
// Intercept configured exit shortcut. Always consume the shortcut so it
|
|
201
|
+
// never reaches the parent handler; firing onExit is the controller's
|
|
202
|
+
// chance to snapshot the current text as a draft before shutting down.
|
|
201
203
|
if (this.#matchesAction(data, "app.exit")) {
|
|
202
|
-
|
|
203
|
-
this.onExit();
|
|
204
|
-
}
|
|
205
|
-
// Always consume exit shortcut (don't pass to parent)
|
|
204
|
+
this.onExit?.();
|
|
206
205
|
return;
|
|
207
206
|
}
|
|
208
207
|
|
|
@@ -403,7 +403,9 @@ export class InputController {
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
handleCtrlD(): void {
|
|
406
|
-
//
|
|
406
|
+
// Editor text (if any) is snapshotted at the start of shutdown() and
|
|
407
|
+
// persisted as a draft for the next resume. Empty text is also fine —
|
|
408
|
+
// shutdown clears any stale sidecar in that case.
|
|
407
409
|
void this.ctx.shutdown();
|
|
408
410
|
}
|
|
409
411
|
|
|
@@ -445,6 +445,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
445
445
|
// Restore mode from session (e.g. plan mode on resume)
|
|
446
446
|
await this.#restoreModeFromSession();
|
|
447
447
|
|
|
448
|
+
// Restore unsent editor draft from previous session shutdown (Ctrl+D).
|
|
449
|
+
// One-shot: consumeDraft removes the sidecar after read so the next
|
|
450
|
+
// resume does not re-restore the same text.
|
|
451
|
+
try {
|
|
452
|
+
const draft = await this.sessionManager.consumeDraft();
|
|
453
|
+
if (draft && !this.editor.getText()) {
|
|
454
|
+
this.editor.setText(draft);
|
|
455
|
+
this.updateEditorBorderColor();
|
|
456
|
+
this.ui.requestRender();
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
logger.warn("Failed to restore session draft", { error: String(err) });
|
|
460
|
+
}
|
|
461
|
+
|
|
448
462
|
// Subscribe to agent events
|
|
449
463
|
this.#subscribeToAgent();
|
|
450
464
|
|
|
@@ -1189,8 +1203,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1189
1203
|
if (this.#isShuttingDown) return;
|
|
1190
1204
|
this.#isShuttingDown = true;
|
|
1191
1205
|
|
|
1206
|
+
// Snapshot the editor before any teardown empties it. Persisting the draft
|
|
1207
|
+
// here covers Ctrl+D shutdown with non-empty text; for /exit the editor is
|
|
1208
|
+
// already cleared so saveDraft("") just removes any stale sidecar.
|
|
1209
|
+
const draftText = this.editor.getText();
|
|
1210
|
+
|
|
1192
1211
|
// Flush pending session writes before shutdown
|
|
1193
1212
|
await this.sessionManager.flush();
|
|
1213
|
+
try {
|
|
1214
|
+
await this.sessionManager.saveDraft(draftText);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
logger.warn("Failed to save session draft", { error: String(err) });
|
|
1217
|
+
}
|
|
1194
1218
|
this.#btwController.dispose();
|
|
1195
1219
|
|
|
1196
1220
|
// Emit shutdown event to hooks
|
|
@@ -8,8 +8,8 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
8
8
|
|
|
9
9
|
<ops>
|
|
10
10
|
@PATH header: subsequent ops apply to PATH
|
|
11
|
-
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
11
|
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
|
+
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
13
13
|
- A..B delete the line range (inclusive); `- A` for one line
|
|
14
14
|
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
|
|
15
15
|
</ops>
|
|
@@ -20,6 +20,7 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
20
20
|
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
21
|
- `= A..B` replaces the inclusive range with the following payload lines. `= A` (or `= A..B`) with no payload blanks the range to a single empty line.
|
|
22
22
|
- `- A..B` deletes the inclusive range; omit `..B` for one line.
|
|
23
|
+
- Pick the smallest op for the change: pure addition → `+`/`<`; pure deletion → `-`; `= A..B` ONLY when content inside `A..B` is actually being modified or removed.
|
|
23
24
|
</rules>
|
|
24
25
|
|
|
25
26
|
<case file="a.ts">
|
|
@@ -39,11 +40,9 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
39
40
|
|
|
40
41
|
# Replace a contiguous range with multiple lines
|
|
41
42
|
@a.ts
|
|
42
|
-
= {{hrefr
|
|
43
|
-
{{hsep}}export function label(name: string): string {
|
|
43
|
+
= {{hrefr 4}}..{{hrefr 5}}
|
|
44
44
|
{{hsep}} const clean = (name || DEF).trim();
|
|
45
45
|
{{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
46
|
-
{{hsep}}}
|
|
47
46
|
|
|
48
47
|
# Insert BEFORE a line
|
|
49
48
|
@a.ts
|
|
@@ -73,11 +72,30 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
73
72
|
= {{hrefr 2}}
|
|
74
73
|
</examples>
|
|
75
74
|
|
|
75
|
+
<anti-pattern>
|
|
76
|
+
# WRONG — replaces 6 lines just to add one. Use `+` at the boundary instead.
|
|
77
|
+
@a.ts
|
|
78
|
+
= {{hrefr 1}}..{{hrefr 6}}
|
|
79
|
+
{{hsep}}const DEF = "guest";
|
|
80
|
+
{{hsep}}const DEBUG = false;
|
|
81
|
+
{{hsep}}
|
|
82
|
+
{{hsep}}export function label(name) {
|
|
83
|
+
{{hsep}} const clean = name || DEF;
|
|
84
|
+
{{hsep}} return clean.trim();
|
|
85
|
+
{{hsep}}}
|
|
86
|
+
|
|
87
|
+
# RIGHT — same effect, one-line insert
|
|
88
|
+
@a.ts
|
|
89
|
+
+ {{hrefr 1}}
|
|
90
|
+
{{hsep}}const DEBUG = false;
|
|
91
|
+
|
|
92
|
+
If your replacement payload would render with even one unchanged line in the diff, you have the wrong op or the wrong range. Stop and rewrite as `+`/`<`/`-` plus a narrower `=`.
|
|
93
|
+
</anti-pattern>
|
|
94
|
+
|
|
76
95
|
<critical>
|
|
77
96
|
- Always copy anchors exactly from tool output, but **NEVER** include line content after the `{{hsep}}` separator in the op line.
|
|
78
|
-
- Only emit changed lines. Do not restate unchanged context as payload.
|
|
79
97
|
- Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
|
|
80
98
|
- Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
|
|
81
|
-
- To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
|
|
82
99
|
- `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
|
|
100
|
+
- Multiple ops in one patch are cheap. Prefer two narrow ops over one wide `=`.
|
|
83
101
|
</critical>
|
|
@@ -2182,6 +2182,63 @@ export class SessionManager {
|
|
|
2182
2182
|
return manager.getPath(id);
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* Path to the unsent-input draft sidecar for the current session. Lives inside
|
|
2187
|
+
* the artifacts directory so it is removed together with the session on
|
|
2188
|
+
* `dropSession`. Returns null when the session has no on-disk identity.
|
|
2189
|
+
*/
|
|
2190
|
+
#getDraftPath(): string | null {
|
|
2191
|
+
const dir = this.getArtifactsDir();
|
|
2192
|
+
return dir ? path.join(dir, "draft.txt") : null;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Persist (or clear) the current editor draft so the next resume of this
|
|
2197
|
+
* session can restore it. Empty text deletes any stale draft. No-op when the
|
|
2198
|
+
* session is not persisted.
|
|
2199
|
+
*/
|
|
2200
|
+
async saveDraft(text: string): Promise<void> {
|
|
2201
|
+
const draftPath = this.#getDraftPath();
|
|
2202
|
+
if (!draftPath || !this.persist) return;
|
|
2203
|
+
if (text.length === 0) {
|
|
2204
|
+
try {
|
|
2205
|
+
await this.storage.unlink(draftPath);
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
if (!isEnoent(err)) throw err;
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
// Force the session header onto disk so resume can find the file we are
|
|
2212
|
+
// attaching this draft to. Without this, a session whose first message
|
|
2213
|
+
// never produced an assistant reply would persist a draft next to a
|
|
2214
|
+
// session file that does not exist on disk.
|
|
2215
|
+
await this.ensureOnDisk();
|
|
2216
|
+
await this.storage.writeText(draftPath, text);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
* Read and remove the saved draft. Returns the previously-saved text, or
|
|
2221
|
+
* null when no draft is pending. Single-shot: a successful read removes the
|
|
2222
|
+
* sidecar so a subsequent resume does not re-restore the same text.
|
|
2223
|
+
*/
|
|
2224
|
+
async consumeDraft(): Promise<string | null> {
|
|
2225
|
+
const draftPath = this.#getDraftPath();
|
|
2226
|
+
if (!draftPath) return null;
|
|
2227
|
+
let text: string;
|
|
2228
|
+
try {
|
|
2229
|
+
text = await this.storage.readText(draftPath);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
if (isEnoent(err)) return null;
|
|
2232
|
+
throw err;
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
await this.storage.unlink(draftPath);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
if (!isEnoent(err)) throw err;
|
|
2238
|
+
}
|
|
2239
|
+
return text;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2185
2242
|
/** The source that set the session name: "user" (manual /rename or RPC) or "auto" (generated title). */
|
|
2186
2243
|
get titleSource(): "auto" | "user" | undefined {
|
|
2187
2244
|
return this.#titleSource;
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -1125,7 +1125,9 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1125
1125
|
headers: {
|
|
1126
1126
|
"Content-Type": "application/json",
|
|
1127
1127
|
Authorization: `Bearer ${apiKey.apiKey}`,
|
|
1128
|
-
"
|
|
1128
|
+
"HTTP-Referer": "https://github.com/can1357/oh-my-pi",
|
|
1129
|
+
"X-OpenRouter-Title": "Oh-My-Pi",
|
|
1130
|
+
"X-OpenRouter-Categories": "cli-agent",
|
|
1129
1131
|
},
|
|
1130
1132
|
body: JSON.stringify(requestBody),
|
|
1131
1133
|
signal: requestSignal,
|