@oh-my-pi/pi-coding-agent 14.6.4 → 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 +12 -5
- package/src/modes/interactive-mode.ts +57 -3
- package/src/modes/types.ts +13 -0
- package/src/modes/utils/ui-helpers.ts +22 -15
- 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
|
|
|
@@ -344,8 +344,11 @@ export class InputController {
|
|
|
344
344
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
345
345
|
// typed since queuing intact. Same protection as #783, applied to
|
|
346
346
|
// the streaming/queue path.
|
|
347
|
-
this.ctx.
|
|
348
|
-
|
|
347
|
+
await this.ctx.withLocalSubmission(
|
|
348
|
+
text,
|
|
349
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
|
|
350
|
+
{ imageCount: images?.length ?? 0 },
|
|
351
|
+
);
|
|
349
352
|
this.ctx.updatePendingMessagesDisplay();
|
|
350
353
|
this.ctx.ui.requestRender();
|
|
351
354
|
return;
|
|
@@ -400,7 +403,9 @@ export class InputController {
|
|
|
400
403
|
}
|
|
401
404
|
|
|
402
405
|
handleCtrlD(): void {
|
|
403
|
-
//
|
|
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.
|
|
404
409
|
void this.ctx.shutdown();
|
|
405
410
|
}
|
|
406
411
|
|
|
@@ -440,7 +445,9 @@ export class InputController {
|
|
|
440
445
|
if (this.ctx.session.isStreaming) {
|
|
441
446
|
this.ctx.editor.addToHistory(text);
|
|
442
447
|
this.ctx.editor.setText("");
|
|
443
|
-
await this.ctx.
|
|
448
|
+
await this.ctx.withLocalSubmission(text, () =>
|
|
449
|
+
this.ctx.session.prompt(text, { streamingBehavior: "followUp" }),
|
|
450
|
+
);
|
|
444
451
|
this.ctx.updatePendingMessagesDisplay();
|
|
445
452
|
this.ctx.ui.requestRender();
|
|
446
453
|
return;
|
|
@@ -449,7 +456,7 @@ export class InputController {
|
|
|
449
456
|
// Not streaming — just submit normally
|
|
450
457
|
this.ctx.editor.addToHistory(text);
|
|
451
458
|
this.ctx.editor.setText("");
|
|
452
|
-
await this.ctx.session.prompt(text);
|
|
459
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text));
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
@@ -183,6 +183,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
183
183
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
184
184
|
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
185
185
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
186
|
+
#pendingSubmissionDispose: (() => void) | undefined;
|
|
186
187
|
lastSigintTime = 0;
|
|
187
188
|
lastEscapeTime = 0;
|
|
188
189
|
shutdownRequested = false;
|
|
@@ -444,6 +445,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
444
445
|
// Restore mode from session (e.g. plan mode on resume)
|
|
445
446
|
await this.#restoreModeFromSession();
|
|
446
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
|
+
|
|
447
462
|
// Subscribe to agent events
|
|
448
463
|
this.#subscribeToAgent();
|
|
449
464
|
|
|
@@ -567,6 +582,30 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
567
582
|
);
|
|
568
583
|
}
|
|
569
584
|
|
|
585
|
+
recordLocalSubmission(text: string, imageCount = 0): () => void {
|
|
586
|
+
if (this.isKnownSlashCommand(text)) {
|
|
587
|
+
return () => {};
|
|
588
|
+
}
|
|
589
|
+
const signature = `${text}\u0000${imageCount}`;
|
|
590
|
+
this.locallySubmittedUserSignatures.add(signature);
|
|
591
|
+
let disposed = false;
|
|
592
|
+
return () => {
|
|
593
|
+
if (disposed) return;
|
|
594
|
+
disposed = true;
|
|
595
|
+
this.locallySubmittedUserSignatures.delete(signature);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T> {
|
|
600
|
+
const dispose = this.recordLocalSubmission(text, options?.imageCount ?? 0);
|
|
601
|
+
try {
|
|
602
|
+
return await fn();
|
|
603
|
+
} catch (err) {
|
|
604
|
+
dispose();
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
570
609
|
startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
|
|
571
610
|
const submission: SubmittedUserInput = {
|
|
572
611
|
text: input.text,
|
|
@@ -575,8 +614,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
575
614
|
started: false,
|
|
576
615
|
};
|
|
577
616
|
this.#pendingSubmittedInput = submission;
|
|
578
|
-
|
|
579
|
-
this.
|
|
617
|
+
const imageCount = submission.images?.length ?? 0;
|
|
618
|
+
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
619
|
+
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
580
620
|
this.addMessageToChat({
|
|
581
621
|
role: "user",
|
|
582
622
|
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
@@ -598,7 +638,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
598
638
|
submission.cancelled = true;
|
|
599
639
|
this.#pendingSubmittedInput = undefined;
|
|
600
640
|
this.optimisticUserMessageSignature = undefined;
|
|
601
|
-
this
|
|
641
|
+
this.#pendingSubmissionDispose?.();
|
|
642
|
+
this.#pendingSubmissionDispose = undefined;
|
|
602
643
|
this.#pendingWorkingMessage = undefined;
|
|
603
644
|
if (this.loadingAnimation) {
|
|
604
645
|
this.loadingAnimation.stop();
|
|
@@ -624,6 +665,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
624
665
|
finishPendingSubmission(input: SubmittedUserInput): void {
|
|
625
666
|
if (this.#pendingSubmittedInput === input) {
|
|
626
667
|
this.#pendingSubmittedInput = undefined;
|
|
668
|
+
this.#pendingSubmissionDispose = undefined;
|
|
627
669
|
}
|
|
628
670
|
}
|
|
629
671
|
|
|
@@ -1161,8 +1203,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1161
1203
|
if (this.#isShuttingDown) return;
|
|
1162
1204
|
this.#isShuttingDown = true;
|
|
1163
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
|
+
|
|
1164
1211
|
// Flush pending session writes before shutdown
|
|
1165
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
|
+
}
|
|
1166
1218
|
this.#btwController.dispose();
|
|
1167
1219
|
|
|
1168
1220
|
// Emit shutdown event to hooks
|
|
@@ -1223,6 +1275,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1223
1275
|
showError(message: string): void {
|
|
1224
1276
|
this.#pendingSubmittedInput = undefined;
|
|
1225
1277
|
this.optimisticUserMessageSignature = undefined;
|
|
1278
|
+
this.#pendingSubmissionDispose?.();
|
|
1279
|
+
this.#pendingSubmissionDispose = undefined;
|
|
1226
1280
|
this.#pendingWorkingMessage = undefined;
|
|
1227
1281
|
if (this.loadingAnimation) {
|
|
1228
1282
|
this.loadingAnimation.stop();
|
package/src/modes/types.ts
CHANGED
|
@@ -151,6 +151,19 @@ export interface InteractiveModeContext {
|
|
|
151
151
|
cancelPendingSubmission(): boolean;
|
|
152
152
|
markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
|
|
153
153
|
finishPendingSubmission(input: SubmittedUserInput): void;
|
|
154
|
+
/**
|
|
155
|
+
* Marks a locally-initiated user submission so the eventual `message_start`
|
|
156
|
+
* event for that user message does not clobber the editor draft (see #783).
|
|
157
|
+
* Returns a dispose function that removes the signature; call it on
|
|
158
|
+
* delivery failure so a retry can be re-marked cleanly.
|
|
159
|
+
*/
|
|
160
|
+
recordLocalSubmission(text: string, imageCount?: number): () => void;
|
|
161
|
+
/**
|
|
162
|
+
* Wraps `fn` in a `recordLocalSubmission` marker that is automatically
|
|
163
|
+
* removed if `fn` rejects. Use this for the common case where a thrown
|
|
164
|
+
* delivery error should leave the signature set untouched.
|
|
165
|
+
*/
|
|
166
|
+
withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
|
|
154
167
|
isKnownSlashCommand(text: string): boolean;
|
|
155
168
|
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
|
|
156
169
|
renderSessionContext(
|
|
@@ -534,6 +534,16 @@ export class UiHelpers {
|
|
|
534
534
|
this.ctx.showStatus("Queued message for after compaction");
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
|
|
538
|
+
if (this.ctx.isKnownSlashCommand(message.text)) {
|
|
539
|
+
await this.ctx.session.prompt(message.text);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await this.ctx.withLocalSubmission(message.text, () =>
|
|
543
|
+
message.mode === "followUp" ? this.ctx.session.followUp(message.text) : this.ctx.session.steer(message.text),
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
537
547
|
isKnownSlashCommand(text: string): boolean {
|
|
538
548
|
if (!text.startsWith("/")) return false;
|
|
539
549
|
const spaceIndex = text.indexOf(" ");
|
|
@@ -576,13 +586,7 @@ export class UiHelpers {
|
|
|
576
586
|
try {
|
|
577
587
|
if (options?.willRetry) {
|
|
578
588
|
for (const message of queuedMessages) {
|
|
579
|
-
|
|
580
|
-
await this.ctx.session.prompt(message.text);
|
|
581
|
-
} else if (message.mode === "followUp") {
|
|
582
|
-
await this.ctx.session.followUp(message.text);
|
|
583
|
-
} else {
|
|
584
|
-
await this.ctx.session.steer(message.text);
|
|
585
|
-
}
|
|
589
|
+
await this.#deliverQueuedMessage(message);
|
|
586
590
|
}
|
|
587
591
|
this.ctx.updatePendingMessagesDisplay();
|
|
588
592
|
return;
|
|
@@ -607,7 +611,10 @@ export class UiHelpers {
|
|
|
607
611
|
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
|
608
612
|
|
|
609
613
|
for (const message of preCommands) {
|
|
610
|
-
|
|
614
|
+
// preCommands are all slash commands; #deliverQueuedMessage handles
|
|
615
|
+
// that branch (no local-submission marking needed since slash
|
|
616
|
+
// commands don't generate a matching user message_start).
|
|
617
|
+
await this.#deliverQueuedMessage(message);
|
|
611
618
|
}
|
|
612
619
|
|
|
613
620
|
// Pass streamingBehavior so that if the session is still streaming when
|
|
@@ -619,22 +626,22 @@ export class UiHelpers {
|
|
|
619
626
|
// deferred, the message lands in the same queue every other consumer
|
|
620
627
|
// (Alt+Up dequeue, post-stream drain) already drains, instead of being
|
|
621
628
|
// stranded in compactionQueuedMessages with no drainer.
|
|
629
|
+
//
|
|
630
|
+
// firstPrompt is fire-and-forget — its rejection is funneled through
|
|
631
|
+
// `restoreQueue` rather than rethrown, so we use the primitive
|
|
632
|
+
// recordLocalSubmission and dispose manually in the catch.
|
|
633
|
+
const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text);
|
|
622
634
|
const promptPromise = this.ctx.session
|
|
623
635
|
.prompt(firstPrompt.text, {
|
|
624
636
|
streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
|
|
625
637
|
})
|
|
626
638
|
.catch((error: unknown) => {
|
|
639
|
+
disposeFirstPrompt();
|
|
627
640
|
restoreQueue(error);
|
|
628
641
|
});
|
|
629
642
|
|
|
630
643
|
for (const message of rest) {
|
|
631
|
-
|
|
632
|
-
await this.ctx.session.prompt(message.text);
|
|
633
|
-
} else if (message.mode === "followUp") {
|
|
634
|
-
await this.ctx.session.followUp(message.text);
|
|
635
|
-
} else {
|
|
636
|
-
await this.ctx.session.steer(message.text);
|
|
637
|
-
}
|
|
644
|
+
await this.#deliverQueuedMessage(message);
|
|
638
645
|
}
|
|
639
646
|
this.ctx.updatePendingMessagesDisplay();
|
|
640
647
|
void promptPromise;
|
|
@@ -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,
|