@oh-my-pi/pi-coding-agent 14.5.7 → 14.5.9

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +7 -7
  3. package/src/config/model-registry.ts +23 -1
  4. package/src/config/settings-schema.ts +23 -0
  5. package/src/edit/modes/atom.lark +7 -5
  6. package/src/edit/modes/atom.ts +462 -56
  7. package/src/edit/modes/hashline.ts +21 -1
  8. package/src/lsp/index.ts +2 -4
  9. package/src/lsp/render.ts +0 -3
  10. package/src/lsp/types.ts +1 -4
  11. package/src/lsp/utils.ts +18 -14
  12. package/src/modes/components/settings-defs.ts +10 -0
  13. package/src/modes/controllers/command-controller.ts +17 -0
  14. package/src/modes/controllers/event-controller.ts +14 -9
  15. package/src/modes/controllers/input-controller.ts +13 -1
  16. package/src/modes/interactive-mode.ts +44 -23
  17. package/src/modes/types.ts +5 -2
  18. package/src/modes/utils/context-usage.ts +294 -0
  19. package/src/prompts/tools/atom.md +99 -44
  20. package/src/prompts/tools/exit-plan-mode.md +5 -39
  21. package/src/prompts/tools/lsp.md +2 -3
  22. package/src/prompts/tools/recipe.md +16 -0
  23. package/src/prompts/tools/task.md +34 -147
  24. package/src/prompts/tools/todo-write.md +22 -64
  25. package/src/session/compaction/compaction.ts +35 -22
  26. package/src/session/session-dump-format.ts +1 -0
  27. package/src/slash-commands/builtin-registry.ts +12 -5
  28. package/src/tools/bash.ts +149 -115
  29. package/src/tools/debug.ts +57 -70
  30. package/src/tools/index.ts +11 -0
  31. package/src/tools/recipe/index.ts +80 -0
  32. package/src/tools/recipe/render.ts +19 -0
  33. package/src/tools/recipe/runner.ts +219 -0
  34. package/src/tools/recipe/runners/cargo.ts +131 -0
  35. package/src/tools/recipe/runners/index.ts +8 -0
  36. package/src/tools/recipe/runners/just.ts +73 -0
  37. package/src/tools/recipe/runners/make.ts +101 -0
  38. package/src/tools/recipe/runners/pkg.ts +165 -0
  39. package/src/tools/recipe/runners/task.ts +72 -0
  40. package/src/tools/renderers.ts +2 -0
@@ -605,6 +605,26 @@ export class HashlineMismatchError extends Error {
605
605
  `Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read (marked *).`,
606
606
  "The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
607
607
  );
608
+
609
+ // Content-based recovery hint: the two-letter hash is weak, so a
610
+ // unique match elsewhere is only a candidate. Keep this advisory; never
611
+ // silently retarget stale edits based on a whole-file hash-only match.
612
+ const hints: string[] = [];
613
+ for (const m of mismatches) {
614
+ const matches: number[] = [];
615
+ for (let line = 1; line <= fileLines.length; line++) {
616
+ if (computeLineHash(line, fileLines[line - 1]) === m.expected) matches.push(line);
617
+ if (matches.length > 1) break;
618
+ }
619
+ if (matches.length === 1 && matches[0] !== m.line) {
620
+ hints.push(` ${m.line}${m.expected} → ${matches[0]}${m.expected}`);
621
+ }
622
+ }
623
+ if (hints.length > 0) {
624
+ lines.push("Hash-only shifted candidate; verify content/context before using:");
625
+ lines.push(...hints);
626
+ }
627
+
608
628
  lines.push("");
609
629
 
610
630
  let prevLine = -1;
@@ -650,7 +670,7 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
650
670
  /**
651
671
  * Default search window for {@link tryRebaseAnchor} (lines on each side of the requested anchor).
652
672
  */
653
- export const ANCHOR_REBASE_WINDOW = 2;
673
+ export const ANCHOR_REBASE_WINDOW = 5;
654
674
 
655
675
  /**
656
676
  * Look for the requested hash within ±`window` lines of `anchor.line`.
package/src/lsp/index.ts CHANGED
@@ -1136,7 +1136,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1136
1136
  _onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
1137
1137
  _context?: AgentToolContext,
1138
1138
  ): Promise<AgentToolResult<LspToolDetails>> {
1139
- const { action, file, line, symbol, occurrence, query, new_name, apply, timeout } = params;
1139
+ const { action, file, line, symbol, query, new_name, apply, timeout } = params;
1140
1140
  const timeoutSec = clampTimeout("lsp", timeout);
1141
1141
  const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
1142
1142
  signal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
@@ -1449,9 +1449,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1449
1449
 
1450
1450
  const uri = targetFile ? fileToUri(targetFile) : "";
1451
1451
  const resolvedLine = line ?? 1;
1452
- const resolvedCharacter = targetFile
1453
- ? await resolveSymbolColumn(targetFile, resolvedLine, symbol, occurrence)
1454
- : 0;
1452
+ const resolvedCharacter = targetFile ? await resolveSymbolColumn(targetFile, resolvedLine, symbol) : 0;
1455
1453
  const position = { line: resolvedLine - 1, character: resolvedCharacter };
1456
1454
 
1457
1455
  let output: string;
package/src/lsp/render.ts CHANGED
@@ -131,9 +131,6 @@ export function renderResult(
131
131
  }
132
132
  if (request?.symbol) {
133
133
  requestLines.push(theme.fg("dim", `symbol: ${sanitizeInlineText(request.symbol)}`));
134
- if (request.occurrence !== undefined) {
135
- requestLines.push(theme.fg("dim", `occurrence: ${request.occurrence}`));
136
- }
137
134
  }
138
135
  if (request?.query) requestLines.push(theme.fg("dim", `query: ${request.query}`));
139
136
  if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
package/src/lsp/types.ts CHANGED
@@ -25,10 +25,7 @@ export const lspSchema = Type.Object({
25
25
  ),
26
26
  file: Type.Optional(Type.String({ description: "File path" })),
27
27
  line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
28
- symbol: Type.Optional(
29
- Type.String({ description: "Symbol/substring to locate on the line (used to compute column)" }),
30
- ),
31
- occurrence: Type.Optional(Type.Number({ description: "Symbol occurrence on line (1-indexed, default: 1)" })),
28
+ symbol: Type.Optional(Type.String({ description: "Symbol/substring to locate on the line" })),
32
29
  query: Type.Optional(Type.String({ description: "Search query or SSR pattern" })),
33
30
  new_name: Type.Optional(Type.String({ description: "New name for rename" })),
34
31
  apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true)" })),
package/src/lsp/utils.ts CHANGED
@@ -596,38 +596,42 @@ function findSymbolMatchIndexes(lineText: string, symbol: string, caseInsensitiv
596
596
  return indexes;
597
597
  }
598
598
 
599
- function normalizeOccurrence(occurrence?: number): number {
600
- if (occurrence === undefined || !Number.isFinite(occurrence)) return 1;
601
- return Math.max(1, Math.trunc(occurrence));
599
+ /**
600
+ * Parses a symbol spec of the form `name` or `name#N` where N is the 1-indexed
601
+ * occurrence on the target line. Returns `name` and `occurrence` (default 1).
602
+ *
603
+ * Greedy match on `.+` so `#name#2` parses as symbol=`#name` (TS private field)
604
+ * with occurrence 2. Specs without a trailing `#\d+` are treated as literal.
605
+ */
606
+ function parseSymbolSpec(spec: string): { symbol: string; occurrence: number } {
607
+ const match = spec.match(/^(.+)#(\d+)$/);
608
+ if (!match) return { symbol: spec, occurrence: 1 };
609
+ const occurrence = Math.max(1, Number.parseInt(match[2], 10));
610
+ return { symbol: match[1], occurrence };
602
611
  }
603
612
 
604
- export async function resolveSymbolColumn(
605
- filePath: string,
606
- line: number,
607
- symbol?: string,
608
- occurrence?: number,
609
- ): Promise<number> {
613
+ export async function resolveSymbolColumn(filePath: string, line: number, symbolSpec?: string): Promise<number> {
610
614
  const lineNumber = Math.max(1, line);
611
- const matchOccurrence = normalizeOccurrence(occurrence);
612
615
  try {
613
616
  const fileText = await Bun.file(filePath).text();
614
617
  const lines = fileText.split("\n");
615
618
  const targetLine = lines[lineNumber - 1] ?? "";
616
- if (!symbol) {
619
+ if (!symbolSpec) {
617
620
  return firstNonWhitespaceColumn(targetLine);
618
621
  }
619
622
 
623
+ const { symbol, occurrence } = parseSymbolSpec(symbolSpec);
620
624
  const exactIndexes = findSymbolMatchIndexes(targetLine, symbol);
621
625
  const fallbackIndexes = exactIndexes.length > 0 ? exactIndexes : findSymbolMatchIndexes(targetLine, symbol, true);
622
626
  if (fallbackIndexes.length === 0) {
623
627
  throw new Error(`Symbol "${symbol}" not found on line ${lineNumber}`);
624
628
  }
625
- if (matchOccurrence > fallbackIndexes.length) {
629
+ if (occurrence > fallbackIndexes.length) {
626
630
  throw new Error(
627
- `Symbol "${symbol}" occurrence ${matchOccurrence} is out of bounds on line ${lineNumber} (found ${fallbackIndexes.length})`,
631
+ `Symbol "${symbol}" occurrence ${occurrence} is out of bounds on line ${lineNumber} (found ${fallbackIndexes.length})`,
628
632
  );
629
633
  }
630
- return fallbackIndexes[matchOccurrence - 1];
634
+ return fallbackIndexes[occurrence - 1];
631
635
  } catch (error) {
632
636
  if (isEnoent(error)) {
633
637
  throw new Error(`File not found: ${filePath}`);
@@ -450,6 +450,16 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
450
450
  { value: "none", label: "None", description: "Space only" },
451
451
  { value: "ascii", label: "ASCII", description: "Greater-than signs" },
452
452
  ],
453
+ // Loop mode
454
+ "loop.mode": [
455
+ {
456
+ value: "prompt",
457
+ label: "Prompt",
458
+ description: "Re-submit the prompt as a follow-up message (current behavior)",
459
+ },
460
+ { value: "compact", label: "Compact", description: "Compact the session context, then re-submit the prompt" },
461
+ { value: "reset", label: "Reset", description: "Start a new session, then re-submit the prompt" },
462
+ ],
453
463
  };
454
464
 
455
465
  function createSubmenuSettingDef(base: Omit<SettingDef, "type" | "options">, provider: OptionProvider): SettingDef {
@@ -24,6 +24,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
24
24
  import { PythonExecutionComponent } from "../../modes/components/python-execution";
25
25
  import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
26
26
  import type { InteractiveModeContext } from "../../modes/types";
27
+ import { computeContextBreakdown, renderContextUsage } from "../../modes/utils/context-usage";
27
28
  import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
28
29
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
29
30
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
@@ -529,6 +530,22 @@ export class CommandController {
529
530
  showMarkdownPanel(this.ctx, "Available Tools", tools);
530
531
  }
531
532
 
533
+ handleContextCommand(): void {
534
+ const breakdown = computeContextBreakdown(this.ctx.session);
535
+ if (breakdown.contextWindow <= 0) {
536
+ this.ctx.showWarning("Context usage is unavailable: no model is selected for this session.");
537
+ return;
538
+ }
539
+ const output = renderContextUsage(breakdown, theme);
540
+ this.ctx.chatContainer.addChild(new Spacer(1));
541
+ this.ctx.chatContainer.addChild(new DynamicBorder());
542
+ this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Context Usage")), 1, 0));
543
+ this.ctx.chatContainer.addChild(new Spacer(1));
544
+ this.ctx.chatContainer.addChild(new Text(output, 1, 0));
545
+ this.ctx.chatContainer.addChild(new DynamicBorder());
546
+ this.ctx.ui.requestRender();
547
+ }
548
+
532
549
  async handleMemoryCommand(text: string): Promise<void> {
533
550
  const argumentText = text.slice(7).trim();
534
551
  const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
@@ -174,18 +174,23 @@ export class EventController {
174
174
 
175
175
  this.#resetReadGroup();
176
176
  const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
177
+ const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
177
178
  if (!wasOptimistic) {
178
179
  this.ctx.addMessageToChat(event.message);
179
180
  }
180
- this.ctx.optimisticUserMessageSignature = undefined;
181
-
182
- // Clear the editor only when the submission did not originate from this
183
- // session's optimistic flow (which already cleared the editor at submit
184
- // time). Clearing here on the optimistic path would race with the user
185
- // typing the next prompt while the previous large redraw lands and erase
186
- // their in-progress draft (#783).
187
- if (!event.message.synthetic && !wasOptimistic) {
188
- this.ctx.editor.setText("");
181
+ if (wasOptimistic) {
182
+ this.ctx.optimisticUserMessageSignature = undefined;
183
+ }
184
+
185
+ // Clear the editor only when the submission did not originate from a
186
+ // local submission (optimistic or queued-while-streaming). Both local
187
+ // paths already cleared the editor at submit time; clearing again here
188
+ // would race with the user typing the next prompt while the previous
189
+ // large redraw lands and erase their in-progress draft (#783).
190
+ if (!event.message.synthetic) {
191
+ if (!wasLocallySubmitted) {
192
+ this.ctx.editor.setText("");
193
+ }
189
194
  this.ctx.updatePendingMessagesDisplay();
190
195
  }
191
196
  this.ctx.ui.requestRender();
@@ -45,7 +45,7 @@ export class InputController {
45
45
  );
46
46
  this.ctx.editor.onEscape = () => {
47
47
  if (this.ctx.loopModeEnabled) {
48
- this.ctx.disableLoopMode();
48
+ this.ctx.pauseLoop();
49
49
  if (this.ctx.session.isStreaming) {
50
50
  void this.ctx.session.abort();
51
51
  } else {
@@ -317,6 +317,12 @@ export class InputController {
317
317
  }
318
318
  }
319
319
 
320
+ // While loop mode is on, every user-typed prompt becomes the new loop
321
+ // prompt that auto-resubmits after each yield.
322
+ if (this.ctx.loopModeEnabled) {
323
+ this.ctx.loopPrompt = text;
324
+ }
325
+
320
326
  // Queue input during compaction
321
327
  if (this.ctx.session.isCompacting) {
322
328
  if (this.ctx.pendingImages.length > 0) {
@@ -334,6 +340,11 @@ export class InputController {
334
340
  this.ctx.editor.setText("");
335
341
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
336
342
  this.ctx.pendingImages = [];
343
+ // Record the signature so the queued message's eventual delivery
344
+ // (a user-role `message_start` event) leaves any draft the user has
345
+ // typed since queuing intact. Same protection as #783, applied to
346
+ // the streaming/queue path.
347
+ this.ctx.locallySubmittedUserSignatures.add(`${text}\u0000${images?.length ?? 0}`);
337
348
  await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
338
349
  this.ctx.updatePendingMessagesDisplay();
339
350
  this.ctx.ui.requestRender();
@@ -443,6 +454,7 @@ export class InputController {
443
454
  }
444
455
 
445
456
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
457
+ this.ctx.locallySubmittedUserSignatures.clear();
446
458
  const { steering, followUp } = this.ctx.session.clearQueue();
447
459
  const allQueued = [...steering, ...followUp];
448
460
  if (allQueued.length === 0) {
@@ -170,6 +170,7 @@ export class InteractiveMode implements InteractiveModeContext {
170
170
  unsubscribe?: () => void;
171
171
  onInputCallback?: (input: SubmittedUserInput) => void;
172
172
  optimisticUserMessageSignature: string | undefined = undefined;
173
+ locallySubmittedUserSignatures: Set<string> = new Set();
173
174
  #pendingSubmittedInput: SubmittedUserInput | undefined;
174
175
  lastSigintTime = 0;
175
176
  lastEscapeTime = 0;
@@ -491,57 +492,71 @@ export class InteractiveMode implements InteractiveModeContext {
491
492
  }
492
493
 
493
494
  #scheduleLoopAutoSubmit(): void {
494
- if (this.#loopAutoSubmitTimer) {
495
- clearTimeout(this.#loopAutoSubmitTimer);
496
- this.#loopAutoSubmitTimer = undefined;
497
- }
495
+ this.#cancelLoopAutoSubmit();
498
496
  if (!this.loopModeEnabled || !this.loopPrompt) return;
499
497
  const prompt = this.loopPrompt;
498
+ const loopAction = settings.get("loop.mode");
500
499
  // Brief delay so the user has a chance to press Esc between iterations.
501
500
  this.#loopAutoSubmitTimer = setTimeout(() => {
502
501
  this.#loopAutoSubmitTimer = undefined;
503
502
  if (!this.loopModeEnabled || !this.onInputCallback) return;
504
- this.onInputCallback(this.startPendingSubmission({ text: prompt }));
503
+ void this.#runLoopIteration(loopAction, prompt);
505
504
  }, 800);
506
505
  }
507
506
 
508
- disableLoopMode(options?: { silent?: boolean }): void {
509
- const wasEnabled = this.loopModeEnabled;
510
- this.loopModeEnabled = false;
511
- this.loopPrompt = undefined;
507
+ #cancelLoopAutoSubmit(): void {
512
508
  if (this.#loopAutoSubmitTimer) {
513
509
  clearTimeout(this.#loopAutoSubmitTimer);
514
510
  this.#loopAutoSubmitTimer = undefined;
515
511
  }
512
+ }
513
+
514
+ async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
515
+ if (action === "compact") {
516
+ await this.handleCompactCommand();
517
+ } else if (action === "reset") {
518
+ await this.handleClearCommand();
519
+ }
520
+ if (!this.loopModeEnabled || !this.onInputCallback) return;
521
+ this.onInputCallback(this.startPendingSubmission({ text: prompt }));
522
+ }
523
+
524
+ disableLoopMode(): void {
525
+ const wasEnabled = this.loopModeEnabled;
526
+ this.loopModeEnabled = false;
527
+ this.loopPrompt = undefined;
528
+ this.#cancelLoopAutoSubmit();
516
529
  this.statusLine.setLoopModeStatus(undefined);
517
530
  this.updateEditorTopBorder();
518
531
  this.ui.requestRender();
519
- if (wasEnabled && !options?.silent) {
532
+ if (wasEnabled) {
520
533
  this.showStatus("Loop mode disabled.");
521
534
  }
522
535
  }
523
536
 
524
- async handleLoopCommand(prompt?: string): Promise<void> {
537
+ /**
538
+ * Pause the loop without exiting it: drops the captured prompt and any
539
+ * pending auto-resubmit. Loop mode stays enabled — the next prompt the
540
+ * user submits becomes the new loop prompt and resumes iteration.
541
+ */
542
+ pauseLoop(): void {
543
+ this.loopPrompt = undefined;
544
+ this.#cancelLoopAutoSubmit();
545
+ }
546
+
547
+ async handleLoopCommand(): Promise<void> {
525
548
  if (this.loopModeEnabled) {
526
549
  this.disableLoopMode();
527
550
  return;
528
551
  }
529
- const trimmed = prompt?.trim();
530
- if (!trimmed) {
531
- this.showError("Usage: /loop <prompt>");
532
- return;
533
- }
534
552
  this.loopModeEnabled = true;
535
- this.loopPrompt = trimmed;
553
+ this.loopPrompt = undefined;
536
554
  this.statusLine.setLoopModeStatus({ enabled: true });
537
555
  this.updateEditorTopBorder();
538
556
  this.ui.requestRender();
539
- this.showStatus("Loop mode enabled. Esc to stop.");
540
-
541
- // Submit the first iteration immediately so the loop kicks off.
542
- if (this.onInputCallback) {
543
- this.onInputCallback(this.startPendingSubmission({ text: trimmed }));
544
- }
557
+ this.showStatus(
558
+ "Loop mode enabled. Your next prompt will repeat after each turn. Esc cancels the current iteration; /loop again to disable.",
559
+ );
545
560
  }
546
561
 
547
562
  startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
@@ -553,6 +568,7 @@ export class InteractiveMode implements InteractiveModeContext {
553
568
  };
554
569
  this.#pendingSubmittedInput = submission;
555
570
  this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
571
+ this.locallySubmittedUserSignatures.add(this.optimisticUserMessageSignature);
556
572
  this.addMessageToChat({
557
573
  role: "user",
558
574
  content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
@@ -574,6 +590,7 @@ export class InteractiveMode implements InteractiveModeContext {
574
590
  submission.cancelled = true;
575
591
  this.#pendingSubmittedInput = undefined;
576
592
  this.optimisticUserMessageSignature = undefined;
593
+ this.locallySubmittedUserSignatures.delete(`${submission.text}\u0000${submission.images?.length ?? 0}`);
577
594
  this.#pendingWorkingMessage = undefined;
578
595
  if (this.loadingAnimation) {
579
596
  this.loadingAnimation.stop();
@@ -1382,6 +1399,10 @@ export class InteractiveMode implements InteractiveModeContext {
1382
1399
  this.#commandController.handleToolsCommand();
1383
1400
  }
1384
1401
 
1402
+ handleContextCommand(): void {
1403
+ this.#commandController.handleContextCommand();
1404
+ }
1405
+
1385
1406
  #prepareSessionSwitch(): void {
1386
1407
  this.#btwController.dispose();
1387
1408
  this.#extensionUiController.clearExtensionTerminalInputListeners();
@@ -108,6 +108,7 @@ export interface InteractiveModeContext {
108
108
  unsubscribe?: () => void;
109
109
  onInputCallback?: (input: SubmittedUserInput) => void;
110
110
  optimisticUserMessageSignature: string | undefined;
111
+ locallySubmittedUserSignatures: Set<string>;
111
112
  lastSigintTime: number;
112
113
  lastEscapeTime: number;
113
114
  shutdownRequested: boolean;
@@ -180,6 +181,7 @@ export interface InteractiveModeContext {
180
181
  handleChangelogCommand(showFull?: boolean): Promise<void>;
181
182
  handleHotkeysCommand(): void;
182
183
  handleToolsCommand(): void;
184
+ handleContextCommand(): void;
183
185
  handleDumpCommand(): void;
184
186
  handleDebugTranscriptCommand(): Promise<void>;
185
187
  handleClearCommand(): Promise<void>;
@@ -235,8 +237,9 @@ export interface InteractiveModeContext {
235
237
  openExternalEditor(): void;
236
238
  registerExtensionShortcuts(): void;
237
239
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
238
- handleLoopCommand(prompt?: string): Promise<void>;
239
- disableLoopMode(options?: { silent?: boolean }): void;
240
+ handleLoopCommand(): Promise<void>;
241
+ disableLoopMode(): void;
242
+ pauseLoop(): void;
240
243
  handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
241
244
 
242
245
  // Hook UI methods