@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.
- package/CHANGELOG.md +43 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +23 -1
- package/src/config/settings-schema.ts +23 -0
- package/src/edit/modes/atom.lark +7 -5
- package/src/edit/modes/atom.ts +462 -56
- package/src/edit/modes/hashline.ts +21 -1
- package/src/lsp/index.ts +2 -4
- package/src/lsp/render.ts +0 -3
- package/src/lsp/types.ts +1 -4
- package/src/lsp/utils.ts +18 -14
- package/src/modes/components/settings-defs.ts +10 -0
- package/src/modes/controllers/command-controller.ts +17 -0
- package/src/modes/controllers/event-controller.ts +14 -9
- package/src/modes/controllers/input-controller.ts +13 -1
- package/src/modes/interactive-mode.ts +44 -23
- package/src/modes/types.ts +5 -2
- package/src/modes/utils/context-usage.ts +294 -0
- package/src/prompts/tools/atom.md +99 -44
- package/src/prompts/tools/exit-plan-mode.md +5 -39
- package/src/prompts/tools/lsp.md +2 -3
- package/src/prompts/tools/recipe.md +16 -0
- package/src/prompts/tools/task.md +34 -147
- package/src/prompts/tools/todo-write.md +22 -64
- package/src/session/compaction/compaction.ts +35 -22
- package/src/session/session-dump-format.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +12 -5
- package/src/tools/bash.ts +149 -115
- package/src/tools/debug.ts +57 -70
- package/src/tools/index.ts +11 -0
- package/src/tools/recipe/index.ts +80 -0
- package/src/tools/recipe/render.ts +19 -0
- package/src/tools/recipe/runner.ts +219 -0
- package/src/tools/recipe/runners/cargo.ts +131 -0
- package/src/tools/recipe/runners/index.ts +8 -0
- package/src/tools/recipe/runners/just.ts +73 -0
- package/src/tools/recipe/runners/make.ts +101 -0
- package/src/tools/recipe/runners/pkg.ts +165 -0
- package/src/tools/recipe/runners/task.ts +72 -0
- 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 =
|
|
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,
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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 (!
|
|
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 (
|
|
629
|
+
if (occurrence > fallbackIndexes.length) {
|
|
626
630
|
throw new Error(
|
|
627
|
-
`Symbol "${symbol}" occurrence ${
|
|
631
|
+
`Symbol "${symbol}" occurrence ${occurrence} is out of bounds on line ${lineNumber} (found ${fallbackIndexes.length})`,
|
|
628
632
|
);
|
|
629
633
|
}
|
|
630
|
-
return fallbackIndexes[
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
503
|
+
void this.#runLoopIteration(loopAction, prompt);
|
|
505
504
|
}, 800);
|
|
506
505
|
}
|
|
507
506
|
|
|
508
|
-
|
|
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
|
|
532
|
+
if (wasEnabled) {
|
|
520
533
|
this.showStatus("Loop mode disabled.");
|
|
521
534
|
}
|
|
522
535
|
}
|
|
523
536
|
|
|
524
|
-
|
|
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 =
|
|
553
|
+
this.loopPrompt = undefined;
|
|
536
554
|
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
537
555
|
this.updateEditorTopBorder();
|
|
538
556
|
this.ui.requestRender();
|
|
539
|
-
this.showStatus(
|
|
540
|
-
|
|
541
|
-
|
|
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();
|
package/src/modes/types.ts
CHANGED
|
@@ -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(
|
|
239
|
-
disableLoopMode(
|
|
240
|
+
handleLoopCommand(): Promise<void>;
|
|
241
|
+
disableLoopMode(): void;
|
|
242
|
+
pauseLoop(): void;
|
|
240
243
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
241
244
|
|
|
242
245
|
// Hook UI methods
|