@oh-my-pi/pi-coding-agent 14.6.4 → 14.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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",
4
+ "version": "14.6.5",
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.4",
50
- "@oh-my-pi/pi-agent-core": "14.6.4",
51
- "@oh-my-pi/pi-ai": "14.6.4",
52
- "@oh-my-pi/pi-natives": "14.6.4",
53
- "@oh-my-pi/pi-tui": "14.6.4",
54
- "@oh-my-pi/pi-utils": "14.6.4",
49
+ "@oh-my-pi/omp-stats": "14.6.5",
50
+ "@oh-my-pi/pi-agent-core": "14.6.5",
51
+ "@oh-my-pi/pi-ai": "14.6.5",
52
+ "@oh-my-pi/pi-natives": "14.6.5",
53
+ "@oh-my-pi/pi-tui": "14.6.5",
54
+ "@oh-my-pi/pi-utils": "14.6.5",
55
55
  "@puppeteer/browsers": "^2.13.0",
56
56
  "@sinclair/typebox": "^0.34.49",
57
57
  "@xterm/headless": "^6.0.0",
@@ -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.locallySubmittedUserSignatures.add(`${text}\u0000${images?.length ?? 0}`);
348
- await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
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;
@@ -440,7 +443,9 @@ export class InputController {
440
443
  if (this.ctx.session.isStreaming) {
441
444
  this.ctx.editor.addToHistory(text);
442
445
  this.ctx.editor.setText("");
443
- await this.ctx.session.prompt(text, { streamingBehavior: "followUp" });
446
+ await this.ctx.withLocalSubmission(text, () =>
447
+ this.ctx.session.prompt(text, { streamingBehavior: "followUp" }),
448
+ );
444
449
  this.ctx.updatePendingMessagesDisplay();
445
450
  this.ctx.ui.requestRender();
446
451
  return;
@@ -449,7 +454,7 @@ export class InputController {
449
454
  // Not streaming — just submit normally
450
455
  this.ctx.editor.addToHistory(text);
451
456
  this.ctx.editor.setText("");
452
- await this.ctx.session.prompt(text);
457
+ await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text));
453
458
  }
454
459
 
455
460
  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;
@@ -567,6 +568,30 @@ export class InteractiveMode implements InteractiveModeContext {
567
568
  );
568
569
  }
569
570
 
571
+ recordLocalSubmission(text: string, imageCount = 0): () => void {
572
+ if (this.isKnownSlashCommand(text)) {
573
+ return () => {};
574
+ }
575
+ const signature = `${text}\u0000${imageCount}`;
576
+ this.locallySubmittedUserSignatures.add(signature);
577
+ let disposed = false;
578
+ return () => {
579
+ if (disposed) return;
580
+ disposed = true;
581
+ this.locallySubmittedUserSignatures.delete(signature);
582
+ };
583
+ }
584
+
585
+ async withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T> {
586
+ const dispose = this.recordLocalSubmission(text, options?.imageCount ?? 0);
587
+ try {
588
+ return await fn();
589
+ } catch (err) {
590
+ dispose();
591
+ throw err;
592
+ }
593
+ }
594
+
570
595
  startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
571
596
  const submission: SubmittedUserInput = {
572
597
  text: input.text,
@@ -575,8 +600,9 @@ export class InteractiveMode implements InteractiveModeContext {
575
600
  started: false,
576
601
  };
577
602
  this.#pendingSubmittedInput = submission;
578
- this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
579
- this.locallySubmittedUserSignatures.add(this.optimisticUserMessageSignature);
603
+ const imageCount = submission.images?.length ?? 0;
604
+ this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
605
+ this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
580
606
  this.addMessageToChat({
581
607
  role: "user",
582
608
  content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
@@ -598,7 +624,8 @@ export class InteractiveMode implements InteractiveModeContext {
598
624
  submission.cancelled = true;
599
625
  this.#pendingSubmittedInput = undefined;
600
626
  this.optimisticUserMessageSignature = undefined;
601
- this.locallySubmittedUserSignatures.delete(`${submission.text}\u0000${submission.images?.length ?? 0}`);
627
+ this.#pendingSubmissionDispose?.();
628
+ this.#pendingSubmissionDispose = undefined;
602
629
  this.#pendingWorkingMessage = undefined;
603
630
  if (this.loadingAnimation) {
604
631
  this.loadingAnimation.stop();
@@ -624,6 +651,7 @@ export class InteractiveMode implements InteractiveModeContext {
624
651
  finishPendingSubmission(input: SubmittedUserInput): void {
625
652
  if (this.#pendingSubmittedInput === input) {
626
653
  this.#pendingSubmittedInput = undefined;
654
+ this.#pendingSubmissionDispose = undefined;
627
655
  }
628
656
  }
629
657
 
@@ -1223,6 +1251,8 @@ export class InteractiveMode implements InteractiveModeContext {
1223
1251
  showError(message: string): void {
1224
1252
  this.#pendingSubmittedInput = undefined;
1225
1253
  this.optimisticUserMessageSignature = undefined;
1254
+ this.#pendingSubmissionDispose?.();
1255
+ this.#pendingSubmissionDispose = undefined;
1226
1256
  this.#pendingWorkingMessage = undefined;
1227
1257
  if (this.loadingAnimation) {
1228
1258
  this.loadingAnimation.stop();
@@ -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
- if (this.ctx.isKnownSlashCommand(message.text)) {
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
- await this.ctx.session.prompt(message.text);
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
- if (this.ctx.isKnownSlashCommand(message.text)) {
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;