@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
|
+
import { type Agent, type AgentMessage, type AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import {
|
|
9
9
|
type AssistantMessage,
|
|
10
10
|
type ImageContent,
|
|
@@ -37,9 +37,10 @@ import type {
|
|
|
37
37
|
} from "../extensibility/extensions";
|
|
38
38
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
39
39
|
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
40
|
+
import type { Goal, GoalModeState } from "../goals/state";
|
|
40
41
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
41
42
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
42
|
-
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
|
43
|
+
import { normalizePlanTitle, type PlanApprovalDetails, renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
|
43
44
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
44
45
|
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
45
46
|
type: "text",
|
|
@@ -49,10 +50,13 @@ import type { CompactionOutcome } from "../session/compaction";
|
|
|
49
50
|
import { HistoryStorage } from "../session/history-storage";
|
|
50
51
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
51
52
|
import { getRecentSessions } from "../session/session-manager";
|
|
53
|
+
import { formatDuration } from "../slash-commands/helpers/format";
|
|
52
54
|
import { STTController, type SttState } from "../stt";
|
|
53
|
-
import type {
|
|
55
|
+
import type { LspStartupServerInfo } from "../tools";
|
|
54
56
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
57
|
+
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
55
58
|
import { formatPhaseDisplayName } from "../tools/todo-write";
|
|
59
|
+
import { ToolError } from "../tools/tool-errors";
|
|
56
60
|
import type { EventBus } from "../utils/event-bus";
|
|
57
61
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
58
62
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
@@ -127,6 +131,22 @@ function formatHudNoteMarker(count: number): string {
|
|
|
127
131
|
return theme.fg("dim", chalk.italic(` \u207a${sub}`));
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop" | "budget";
|
|
135
|
+
|
|
136
|
+
const GOAL_SUBCOMMANDS = new Set<GoalSubcommand>(["set", "show", "pause", "resume", "drop", "budget"]);
|
|
137
|
+
|
|
138
|
+
function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; rest: string } {
|
|
139
|
+
const trimmed = args.trim();
|
|
140
|
+
if (!trimmed) return { sub: undefined, rest: "" };
|
|
141
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
142
|
+
if (!match) return { sub: undefined, rest: trimmed };
|
|
143
|
+
const first = match[1].toLowerCase();
|
|
144
|
+
if (GOAL_SUBCOMMANDS.has(first as GoalSubcommand)) {
|
|
145
|
+
return { sub: first as GoalSubcommand, rest: match[2]?.trim() ?? "" };
|
|
146
|
+
}
|
|
147
|
+
return { sub: undefined, rest: trimmed };
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
131
151
|
export interface InteractiveModeOptions {
|
|
132
152
|
/** Providers that were migrated during startup */
|
|
@@ -168,6 +188,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
168
188
|
todoExpanded = false;
|
|
169
189
|
planModeEnabled = false;
|
|
170
190
|
planModePaused = false;
|
|
191
|
+
goalModeEnabled = false;
|
|
192
|
+
goalModePaused = false;
|
|
171
193
|
planModePlanFilePath: string | undefined = undefined;
|
|
172
194
|
loopModeEnabled = false;
|
|
173
195
|
loopPrompt: string | undefined = undefined;
|
|
@@ -216,6 +238,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
216
238
|
readonly #version: string;
|
|
217
239
|
readonly #changelogMarkdown: string | undefined;
|
|
218
240
|
#planModePreviousTools: string[] | undefined;
|
|
241
|
+
#goalModePreviousTools: string[] | undefined;
|
|
242
|
+
#goalContinuationTimer: NodeJS.Timeout | undefined;
|
|
243
|
+
#goalTurnHadToolCalls = false;
|
|
244
|
+
#goalContinuationTurnInFlight = false;
|
|
245
|
+
#goalSuppressNextContinuation = false;
|
|
219
246
|
#planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
220
247
|
#pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
221
248
|
#planModeHasEntered = false;
|
|
@@ -477,6 +504,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
477
504
|
// Subscribe to agent events
|
|
478
505
|
this.#subscribeToAgent();
|
|
479
506
|
|
|
507
|
+
this.#eventBusUnsubscribers.push(
|
|
508
|
+
this.session.subscribe(event => {
|
|
509
|
+
void this.#handleGoalSessionEvent(event);
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
480
512
|
// Set up theme file watcher
|
|
481
513
|
onThemeChange(() => {
|
|
482
514
|
clearRenderCache();
|
|
@@ -520,12 +552,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
520
552
|
}
|
|
521
553
|
|
|
522
554
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
555
|
+
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
556
|
+
await this.#exitGoalMode({ reason: "completed", silent: true });
|
|
557
|
+
}
|
|
523
558
|
const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
|
|
524
559
|
this.onInputCallback = input => {
|
|
525
560
|
this.onInputCallback = undefined;
|
|
526
561
|
resolve(input);
|
|
527
562
|
};
|
|
528
563
|
this.#scheduleLoopAutoSubmit();
|
|
564
|
+
this.#scheduleGoalContinuation();
|
|
529
565
|
return promise;
|
|
530
566
|
}
|
|
531
567
|
|
|
@@ -549,6 +585,48 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
549
585
|
}
|
|
550
586
|
}
|
|
551
587
|
|
|
588
|
+
#scheduleGoalContinuation(): void {
|
|
589
|
+
this.#cancelGoalContinuation();
|
|
590
|
+
if (this.loopModeEnabled) return;
|
|
591
|
+
if (!this.onInputCallback) return;
|
|
592
|
+
if (!this.session.settings.get("goal.continuationModes").includes("interactive")) return;
|
|
593
|
+
if (this.planModeEnabled || this.planModePaused) return;
|
|
594
|
+
if (!this.goalModeEnabled || this.goalModePaused) return;
|
|
595
|
+
if (this.#goalSuppressNextContinuation) return;
|
|
596
|
+
if (this.#pendingSubmittedInput) return;
|
|
597
|
+
if (this.editor.getText().trim().length > 0) return;
|
|
598
|
+
if ((this.pendingImages?.length ?? 0) > 0) return;
|
|
599
|
+
const state = this.session.getGoalModeState();
|
|
600
|
+
if (!state?.enabled || state.goal.status !== "active") return;
|
|
601
|
+
const prompt = this.session.goalRuntime.buildContinuationPrompt();
|
|
602
|
+
if (!prompt) return;
|
|
603
|
+
this.#goalContinuationTimer = setTimeout(() => {
|
|
604
|
+
this.#goalContinuationTimer = undefined;
|
|
605
|
+
if (!this.onInputCallback) return;
|
|
606
|
+
if (!this.goalModeEnabled || this.goalModePaused) return;
|
|
607
|
+
if (this.#pendingSubmittedInput) return;
|
|
608
|
+
if (this.editor.getText().trim().length > 0) return;
|
|
609
|
+
if ((this.pendingImages?.length ?? 0) > 0) return;
|
|
610
|
+
const latestState = this.session.getGoalModeState();
|
|
611
|
+
if (!latestState?.enabled || latestState.goal.status !== "active") return;
|
|
612
|
+
this.#goalContinuationTurnInFlight = true;
|
|
613
|
+
this.onInputCallback(
|
|
614
|
+
this.startPendingSubmission({
|
|
615
|
+
text: prompt,
|
|
616
|
+
customType: "goal-continuation",
|
|
617
|
+
display: false,
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
}, 800);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#cancelGoalContinuation(): void {
|
|
624
|
+
if (this.#goalContinuationTimer) {
|
|
625
|
+
clearTimeout(this.#goalContinuationTimer);
|
|
626
|
+
this.#goalContinuationTimer = undefined;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
552
630
|
async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
|
|
553
631
|
if (!consumeLoopLimitIteration(this.loopLimit)) {
|
|
554
632
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
@@ -639,23 +717,36 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
639
717
|
}
|
|
640
718
|
}
|
|
641
719
|
|
|
642
|
-
startPendingSubmission(input: {
|
|
720
|
+
startPendingSubmission(input: {
|
|
721
|
+
text: string;
|
|
722
|
+
images?: ImageContent[];
|
|
723
|
+
customType?: string;
|
|
724
|
+
display?: boolean;
|
|
725
|
+
}): SubmittedUserInput {
|
|
643
726
|
const submission: SubmittedUserInput = {
|
|
644
727
|
text: input.text,
|
|
645
728
|
images: input.images,
|
|
729
|
+
customType: input.customType,
|
|
730
|
+
display: input.display,
|
|
646
731
|
cancelled: false,
|
|
647
732
|
started: false,
|
|
648
733
|
};
|
|
649
734
|
this.#pendingSubmittedInput = submission;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
735
|
+
if (!submission.customType) {
|
|
736
|
+
this.#resetGoalContinuationSuppression();
|
|
737
|
+
const imageCount = submission.images?.length ?? 0;
|
|
738
|
+
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
739
|
+
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
740
|
+
this.addMessageToChat({
|
|
741
|
+
role: "user",
|
|
742
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
743
|
+
attribution: "user",
|
|
744
|
+
timestamp: Date.now(),
|
|
745
|
+
});
|
|
746
|
+
} else {
|
|
747
|
+
this.optimisticUserMessageSignature = undefined;
|
|
748
|
+
this.#pendingSubmissionDispose = undefined;
|
|
749
|
+
}
|
|
659
750
|
this.editor.setText("");
|
|
660
751
|
this.ensureLoadingAnimation();
|
|
661
752
|
this.ui.requestRender();
|
|
@@ -674,14 +765,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
674
765
|
this.#pendingSubmissionDispose?.();
|
|
675
766
|
this.#pendingSubmissionDispose = undefined;
|
|
676
767
|
this.#pendingWorkingMessage = undefined;
|
|
768
|
+
if (submission.customType === "goal-continuation") {
|
|
769
|
+
this.#goalContinuationTurnInFlight = false;
|
|
770
|
+
}
|
|
677
771
|
if (this.loadingAnimation) {
|
|
678
772
|
this.loadingAnimation.stop();
|
|
679
773
|
this.loadingAnimation = undefined;
|
|
680
774
|
this.statusContainer.clear();
|
|
681
775
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
776
|
+
if (!submission.customType) {
|
|
777
|
+
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
778
|
+
this.rebuildChatFromMessages();
|
|
779
|
+
this.editor.setText(submission.text);
|
|
780
|
+
}
|
|
685
781
|
this.updateEditorBorderColor();
|
|
686
782
|
this.ui.requestRender();
|
|
687
783
|
return true;
|
|
@@ -702,6 +798,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
702
798
|
this.#pendingSubmittedInput = undefined;
|
|
703
799
|
this.#pendingSubmissionDispose = undefined;
|
|
704
800
|
}
|
|
801
|
+
if (input.customType === "goal-continuation") {
|
|
802
|
+
this.#goalContinuationTurnInFlight = false;
|
|
803
|
+
}
|
|
705
804
|
|
|
706
805
|
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
707
806
|
this.optimisticUserMessageSignature = undefined;
|
|
@@ -857,6 +956,95 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
857
956
|
this.ui.requestRender();
|
|
858
957
|
}
|
|
859
958
|
|
|
959
|
+
#updateGoalModeStatus(): void {
|
|
960
|
+
const status =
|
|
961
|
+
this.goalModeEnabled || this.goalModePaused
|
|
962
|
+
? { enabled: this.goalModeEnabled, paused: this.goalModePaused }
|
|
963
|
+
: undefined;
|
|
964
|
+
this.statusLine.setGoalModeStatus(status);
|
|
965
|
+
this.updateEditorTopBorder();
|
|
966
|
+
this.ui.requestRender();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
#resetGoalContinuationSuppression(): void {
|
|
970
|
+
this.#goalSuppressNextContinuation = false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
#getPausedGoalState(): GoalModeState | undefined {
|
|
974
|
+
const state = this.session.getGoalModeState();
|
|
975
|
+
if (!state?.goal || state.enabled || state.goal.status !== "paused") {
|
|
976
|
+
return undefined;
|
|
977
|
+
}
|
|
978
|
+
return state;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
#goalFromModeData(modeData: SessionContext["modeData"]): Goal | undefined {
|
|
982
|
+
const goal = modeData?.goal;
|
|
983
|
+
if (!goal || typeof goal !== "object") return undefined;
|
|
984
|
+
const value = goal as Record<string, unknown>;
|
|
985
|
+
if (
|
|
986
|
+
typeof value.id !== "string" ||
|
|
987
|
+
typeof value.objective !== "string" ||
|
|
988
|
+
typeof value.status !== "string" ||
|
|
989
|
+
typeof value.tokensUsed !== "number" ||
|
|
990
|
+
typeof value.timeUsedSeconds !== "number" ||
|
|
991
|
+
typeof value.createdAt !== "number" ||
|
|
992
|
+
typeof value.updatedAt !== "number"
|
|
993
|
+
) {
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
id: value.id,
|
|
998
|
+
objective: value.objective,
|
|
999
|
+
status: value.status as Goal["status"],
|
|
1000
|
+
tokenBudget: typeof value.tokenBudget === "number" ? value.tokenBudget : undefined,
|
|
1001
|
+
tokensUsed: value.tokensUsed,
|
|
1002
|
+
timeUsedSeconds: value.timeUsedSeconds,
|
|
1003
|
+
createdAt: value.createdAt,
|
|
1004
|
+
updatedAt: value.updatedAt,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async #handleGoalSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
1009
|
+
if (event.type === "agent_start") {
|
|
1010
|
+
this.#goalTurnHadToolCalls = false;
|
|
1011
|
+
this.#cancelGoalContinuation();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (event.type === "tool_execution_start") {
|
|
1015
|
+
this.#goalTurnHadToolCalls = true;
|
|
1016
|
+
if (!this.#goalContinuationTurnInFlight) {
|
|
1017
|
+
this.#resetGoalContinuationSuppression();
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (event.type === "message_start" && event.message.role === "user" && !event.message.synthetic) {
|
|
1022
|
+
this.#resetGoalContinuationSuppression();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (event.type === "goal_updated") {
|
|
1026
|
+
this.goalModeEnabled = event.state?.enabled === true;
|
|
1027
|
+
this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
|
|
1028
|
+
if (!event.state?.enabled) {
|
|
1029
|
+
this.#cancelGoalContinuation();
|
|
1030
|
+
}
|
|
1031
|
+
this.#updateGoalModeStatus();
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (event.type !== "agent_end") {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (this.#goalContinuationTurnInFlight) {
|
|
1038
|
+
this.#goalSuppressNextContinuation = !this.#goalTurnHadToolCalls;
|
|
1039
|
+
this.#goalContinuationTurnInFlight = false;
|
|
1040
|
+
}
|
|
1041
|
+
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
1042
|
+
await this.#exitGoalMode({ reason: "completed", silent: true });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
this.#scheduleGoalContinuation();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
860
1048
|
async #applyPlanModeModel(): Promise<void> {
|
|
861
1049
|
const resolved = this.session.resolveRoleModelWithThinking("plan");
|
|
862
1050
|
if (!resolved.model) return;
|
|
@@ -903,6 +1091,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
903
1091
|
/** Restore mode state from session entries on resume (e.g. plan mode). */
|
|
904
1092
|
async #restoreModeFromSession(): Promise<void> {
|
|
905
1093
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1094
|
+
const goalEnabled = this.session.settings.get("goal.enabled");
|
|
1095
|
+
if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
|
|
1096
|
+
this.sessionManager.appendModeChange("none");
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused") {
|
|
1100
|
+
const goal = this.#goalFromModeData(sessionContext.modeData);
|
|
1101
|
+
if (!goal) {
|
|
1102
|
+
this.sessionManager.appendModeChange("none");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
this.session.setGoalModeState({
|
|
1106
|
+
enabled: sessionContext.mode === "goal",
|
|
1107
|
+
mode: "active",
|
|
1108
|
+
goal,
|
|
1109
|
+
});
|
|
1110
|
+
const restored = await this.session.goalRuntime.onThreadResumed();
|
|
1111
|
+
this.goalModeEnabled = restored?.enabled === true;
|
|
1112
|
+
this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
|
|
1113
|
+
this.#updateGoalModeStatus();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
906
1116
|
if (!this.session.settings.get("plan.enabled")) {
|
|
907
1117
|
// Clear stale plan/plan_paused mode so re-enabling the setting
|
|
908
1118
|
// later doesn't unexpectedly restore an old plan session.
|
|
@@ -925,13 +1135,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
925
1135
|
if (this.planModeEnabled) {
|
|
926
1136
|
return;
|
|
927
1137
|
}
|
|
1138
|
+
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1139
|
+
this.showWarning("Exit goal mode first.");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
928
1142
|
|
|
929
1143
|
this.planModePaused = false;
|
|
930
1144
|
|
|
931
1145
|
const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
|
|
932
1146
|
const previousTools = this.session.getActiveToolNames();
|
|
933
|
-
const
|
|
934
|
-
const planTools =
|
|
1147
|
+
const hasResolveTool = this.session.getToolByName("resolve") !== undefined;
|
|
1148
|
+
const planTools = hasResolveTool ? [...previousTools, "resolve"] : previousTools;
|
|
935
1149
|
const uniquePlanTools = [...new Set(planTools)];
|
|
936
1150
|
|
|
937
1151
|
this.#planModePreviousTools = previousTools;
|
|
@@ -945,6 +1159,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
945
1159
|
workflow: options?.workflow ?? "parallel",
|
|
946
1160
|
reentry: this.#planModeHasEntered,
|
|
947
1161
|
});
|
|
1162
|
+
this.session.setStandingResolveHandler?.(input => this.#runPlanApprovalResolve(input));
|
|
948
1163
|
if (this.session.isStreaming) {
|
|
949
1164
|
await this.session.sendPlanModeContext({ deliverAs: "steer" });
|
|
950
1165
|
}
|
|
@@ -955,6 +1170,47 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
955
1170
|
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
|
|
956
1171
|
}
|
|
957
1172
|
|
|
1173
|
+
/** Standing resolve dispatcher registered while plan mode is active. The agent
|
|
1174
|
+
* submits the finalized plan by calling `resolve { action: "apply", extra: { title } }`;
|
|
1175
|
+
* this handler validates the plan file exists, normalizes the title, and shapes the
|
|
1176
|
+
* payload that `event-controller` forwards to `handlePlanApproval`. */
|
|
1177
|
+
#runPlanApprovalResolve(input: unknown): Promise<AgentToolResult<ResolveToolDetails>> {
|
|
1178
|
+
return runResolveInvocation(input as Parameters<typeof runResolveInvocation>[0], {
|
|
1179
|
+
sourceToolName: "plan_approval",
|
|
1180
|
+
label: "Plan ready for approval",
|
|
1181
|
+
apply: async (_reason, extra) => {
|
|
1182
|
+
const state = this.session.getPlanModeState?.();
|
|
1183
|
+
if (!state?.enabled) {
|
|
1184
|
+
throw new ToolError("Plan mode is not active.");
|
|
1185
|
+
}
|
|
1186
|
+
const title = extra?.title;
|
|
1187
|
+
if (typeof title !== "string" || title.trim() === "") {
|
|
1188
|
+
throw new ToolError(
|
|
1189
|
+
'Plan approval requires `extra: { title: "<PLAN_TITLE>" }`. Provide a title with letters, numbers, underscores, or hyphens only.',
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const normalized = normalizePlanTitle(title);
|
|
1193
|
+
const planFilePath = state.planFilePath;
|
|
1194
|
+
const planContent = await this.#readPlanFile(planFilePath);
|
|
1195
|
+
if (planContent === null) {
|
|
1196
|
+
throw new ToolError(
|
|
1197
|
+
`Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const details: PlanApprovalDetails = {
|
|
1201
|
+
planFilePath,
|
|
1202
|
+
finalPlanFilePath: `local://${normalized.fileName}`,
|
|
1203
|
+
title: normalized.title,
|
|
1204
|
+
planExists: true,
|
|
1205
|
+
};
|
|
1206
|
+
return {
|
|
1207
|
+
content: [{ type: "text" as const, text: "Plan ready for approval." }],
|
|
1208
|
+
details,
|
|
1209
|
+
};
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
958
1214
|
async #exitPlanMode(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
|
|
959
1215
|
if (!this.planModeEnabled) {
|
|
960
1216
|
return;
|
|
@@ -990,6 +1246,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
990
1246
|
}
|
|
991
1247
|
}
|
|
992
1248
|
}
|
|
1249
|
+
this.session.setStandingResolveHandler?.(null);
|
|
993
1250
|
this.session.setPlanModeState(undefined);
|
|
994
1251
|
this.planModeEnabled = false;
|
|
995
1252
|
this.planModePaused = options?.paused ?? false;
|
|
@@ -1004,6 +1261,73 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1004
1261
|
}
|
|
1005
1262
|
}
|
|
1006
1263
|
|
|
1264
|
+
async #enterGoalMode(options: { objective?: string; resume?: boolean; silent?: boolean }): Promise<void> {
|
|
1265
|
+
if (this.goalModeEnabled) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (this.planModeEnabled || this.planModePaused) {
|
|
1269
|
+
this.showWarning("Exit plan mode first.");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
|
|
1273
|
+
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
1274
|
+
this.#goalModePreviousTools = previousTools;
|
|
1275
|
+
this.goalModePaused = false;
|
|
1276
|
+
const state = options.resume
|
|
1277
|
+
? await this.session.goalRuntime.resumeGoal()
|
|
1278
|
+
: await this.session.goalRuntime.createGoal({ objective: options.objective ?? "" });
|
|
1279
|
+
await this.session.setActiveToolsByName(goalTools);
|
|
1280
|
+
this.session.setGoalModeState(state);
|
|
1281
|
+
this.goalModeEnabled = true;
|
|
1282
|
+
this.#resetGoalContinuationSuppression();
|
|
1283
|
+
this.#updateGoalModeStatus();
|
|
1284
|
+
if (this.session.isStreaming) {
|
|
1285
|
+
await this.session.sendGoalModeContext({ deliverAs: "steer" });
|
|
1286
|
+
}
|
|
1287
|
+
if (!options.silent) {
|
|
1288
|
+
this.showStatus(options.resume ? "Goal mode resumed." : "Goal mode enabled.");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async #exitGoalMode(options?: {
|
|
1293
|
+
silent?: boolean;
|
|
1294
|
+
paused?: boolean;
|
|
1295
|
+
reason?: "completed" | "paused" | "dropped";
|
|
1296
|
+
}): Promise<void> {
|
|
1297
|
+
const previousTools = this.#goalModePreviousTools;
|
|
1298
|
+
if (this.goalModeEnabled && previousTools) {
|
|
1299
|
+
await this.session.setActiveToolsByName(previousTools);
|
|
1300
|
+
}
|
|
1301
|
+
const currentState = this.session.getGoalModeState();
|
|
1302
|
+
if (options?.reason === "completed") {
|
|
1303
|
+
this.session.setGoalModeState(undefined);
|
|
1304
|
+
this.sessionManager.appendModeChange("none");
|
|
1305
|
+
this.sessionManager.appendCustomEntry("goal-completed", {
|
|
1306
|
+
objective: currentState?.goal?.objective,
|
|
1307
|
+
tokensUsed: currentState?.goal?.tokensUsed,
|
|
1308
|
+
tokenBudget: currentState?.goal?.tokenBudget,
|
|
1309
|
+
timeUsedSeconds: currentState?.goal?.timeUsedSeconds,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
this.goalModeEnabled = false;
|
|
1313
|
+
this.goalModePaused = options?.paused ?? false;
|
|
1314
|
+
this.#goalModePreviousTools = undefined;
|
|
1315
|
+
this.#goalContinuationTurnInFlight = false;
|
|
1316
|
+
this.#cancelGoalContinuation();
|
|
1317
|
+
this.#updateGoalModeStatus();
|
|
1318
|
+
if (!options?.silent) {
|
|
1319
|
+
if (options?.reason === "completed") {
|
|
1320
|
+
this.showStatus("Goal mode completed.");
|
|
1321
|
+
} else if (options?.reason === "dropped") {
|
|
1322
|
+
this.showStatus("Goal dropped.");
|
|
1323
|
+
} else if (options?.paused) {
|
|
1324
|
+
this.showStatus("Goal mode paused.");
|
|
1325
|
+
} else {
|
|
1326
|
+
this.showStatus("Goal mode disabled.");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1007
1331
|
async #readPlanFile(planFilePath: string): Promise<string | null> {
|
|
1008
1332
|
const resolvedPath = this.#resolvePlanFilePath(planFilePath);
|
|
1009
1333
|
try {
|
|
@@ -1127,37 +1451,55 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1127
1451
|
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1128
1452
|
});
|
|
1129
1453
|
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
1130
|
-
await this.#exitPlanMode({ silent: true, paused: false });
|
|
1131
1454
|
|
|
1455
|
+
// Mark the pending abort caused by the plan-mode → compaction transition as
|
|
1456
|
+
// silent BEFORE #exitPlanMode raises it. The `finally` below clears the
|
|
1457
|
+
// flag on every terminal compaction outcome (ok / cancelled / failed /
|
|
1458
|
+
// throw) so a leaked flag cannot silence a later unrelated abort.
|
|
1459
|
+
// Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
|
|
1460
|
+
// is unconditional and idempotent.
|
|
1461
|
+
if (options.compactBeforeExecute) {
|
|
1462
|
+
this.session.markPlanCompactAbortPending();
|
|
1463
|
+
}
|
|
1132
1464
|
let compactOutcome: CompactionOutcome | undefined;
|
|
1133
|
-
|
|
1134
|
-
await this
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1465
|
+
try {
|
|
1466
|
+
await this.#exitPlanMode({ silent: true, paused: false });
|
|
1467
|
+
|
|
1468
|
+
if (!options.preserveContext) {
|
|
1469
|
+
await this.handleClearCommand();
|
|
1470
|
+
// The new session has a fresh local:// root — persist the approved plan there
|
|
1471
|
+
// so `local://<title>.md` resolves correctly in the execution session.
|
|
1472
|
+
const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
|
|
1473
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1474
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1475
|
+
});
|
|
1476
|
+
await Bun.write(newLocalPath, planContent);
|
|
1477
|
+
} else if (options.compactBeforeExecute) {
|
|
1478
|
+
// Distill the plan-mode transcript before the execution turn is queued so
|
|
1479
|
+
// the plan-approved synthetic prompt lands as a fresh cache anchor.
|
|
1480
|
+
// Outcome is consumed after tool-restoration and plan-reference-path
|
|
1481
|
+
// bookkeeping below; `markPlanReferenceSent` is intentionally deferred
|
|
1482
|
+
// past the cancel guard — see the comment at the cancel branch.
|
|
1483
|
+
// Cancellation skips the synthetic-prompt dispatch (operator's explicit
|
|
1484
|
+
// abort is honored); failure proceeds best-effort — approval intent stands.
|
|
1485
|
+
const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
|
|
1486
|
+
planFilePath: options.finalPlanFilePath,
|
|
1487
|
+
});
|
|
1488
|
+
// Pin the plan reference path BEFORE compaction so any user messages
|
|
1489
|
+
// queued during the compaction await (which `handleCompactCommand`
|
|
1490
|
+
// flushes via `flushCompactionQueue` before returning) see the
|
|
1491
|
+
// approved plan in `#buildPlanReferenceMessage`. Reassignment after
|
|
1492
|
+
// the try/finally is idempotent and kept for the !compactBeforeExecute
|
|
1493
|
+
// branch.
|
|
1494
|
+
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
1495
|
+
compactOutcome = await this.handleCompactCommand(compactionPrompt);
|
|
1496
|
+
}
|
|
1497
|
+
} finally {
|
|
1498
|
+
// Unconditional clear. Idempotent: a no-op when the flag was never set
|
|
1499
|
+
// (i.e., the !compactBeforeExecute branch), and a no-op when the flag
|
|
1500
|
+
// was already consumed by AgentSession.#handleAgentEvent's aborted
|
|
1501
|
+
// message_end stamping. Guarantees the flag is dead at every exit.
|
|
1502
|
+
this.session.clearPlanCompactAbortPending();
|
|
1161
1503
|
}
|
|
1162
1504
|
|
|
1163
1505
|
// Tool restoration runs on every path — the plan mode tools must be
|
|
@@ -1193,6 +1535,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1193
1535
|
}
|
|
1194
1536
|
|
|
1195
1537
|
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
|
|
1538
|
+
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1539
|
+
this.showWarning("Exit goal mode first.");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1196
1542
|
if (this.planModeEnabled) {
|
|
1197
1543
|
const confirmed = await this.showHookConfirm(
|
|
1198
1544
|
"Exit plan mode?",
|
|
@@ -1212,16 +1558,241 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1212
1558
|
}
|
|
1213
1559
|
}
|
|
1214
1560
|
|
|
1215
|
-
async
|
|
1561
|
+
async #handleGoalBudgetCommand(rawBudget: string): Promise<void> {
|
|
1562
|
+
const state = this.session.getGoalModeState();
|
|
1563
|
+
if (!this.goalModeEnabled || !state?.enabled) {
|
|
1564
|
+
this.showWarning("No active goal.");
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (state.goal.status === "complete") {
|
|
1568
|
+
this.showStatus("Goal is already complete.");
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const trimmed = rawBudget.trim().toLowerCase();
|
|
1572
|
+
let nextBudget: number | undefined;
|
|
1573
|
+
if (trimmed !== "off") {
|
|
1574
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1575
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1576
|
+
this.showError("Goal budget must be a positive integer or `off`.");
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
nextBudget = parsed;
|
|
1580
|
+
}
|
|
1581
|
+
await this.session.goalRuntime.onBudgetMutated(nextBudget);
|
|
1582
|
+
this.#resetGoalContinuationSuppression();
|
|
1583
|
+
this.#scheduleGoalContinuation();
|
|
1584
|
+
this.showStatus(nextBudget === undefined ? "Goal budget cleared." : `Goal budget set to ${nextBudget}.`);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async handleGoalModeCommand(rest?: string): Promise<void> {
|
|
1588
|
+
try {
|
|
1589
|
+
if (this.planModeEnabled || this.planModePaused) {
|
|
1590
|
+
this.showWarning("Exit plan mode first.");
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (!this.session.settings.get("goal.enabled")) {
|
|
1594
|
+
this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const { sub, rest: subRest } = parseGoalSubcommand(rest ?? "");
|
|
1598
|
+
if (sub) {
|
|
1599
|
+
await this.#dispatchGoalSubcommand(sub, subRest);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (this.goalModeEnabled) {
|
|
1603
|
+
if (subRest) {
|
|
1604
|
+
this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
await this.#openGoalMenu("active");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const pausedState = this.#getPausedGoalState();
|
|
1611
|
+
if (pausedState) {
|
|
1612
|
+
if (subRest) {
|
|
1613
|
+
this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
await this.#openGoalMenu("paused");
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (subRest) {
|
|
1620
|
+
await this.#startGoalFromObjective(subRest);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const objective = (
|
|
1624
|
+
await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true })
|
|
1625
|
+
)?.trim();
|
|
1626
|
+
if (!objective) return;
|
|
1627
|
+
await this.#startGoalFromObjective(objective);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
|
|
1634
|
+
switch (sub) {
|
|
1635
|
+
case "set":
|
|
1636
|
+
await this.#handleGoalSetSubcommand(rest);
|
|
1637
|
+
return;
|
|
1638
|
+
case "show":
|
|
1639
|
+
this.#showGoalDetails();
|
|
1640
|
+
return;
|
|
1641
|
+
case "pause":
|
|
1642
|
+
await this.#pauseGoalAction();
|
|
1643
|
+
return;
|
|
1644
|
+
case "resume":
|
|
1645
|
+
await this.#resumeGoalAction();
|
|
1646
|
+
return;
|
|
1647
|
+
case "drop":
|
|
1648
|
+
await this.#confirmAndDropGoal();
|
|
1649
|
+
return;
|
|
1650
|
+
case "budget":
|
|
1651
|
+
if (!this.goalModeEnabled) {
|
|
1652
|
+
this.showWarning(
|
|
1653
|
+
this.#getPausedGoalState() ? "Resume the goal before adjusting the budget." : "No active goal.",
|
|
1654
|
+
);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (!rest) {
|
|
1658
|
+
await this.#promptGoalBudgetEdit();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
await this.#handleGoalBudgetCommand(rest);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
async #openGoalMenu(state: "active" | "paused"): Promise<void> {
|
|
1667
|
+
const goal = this.session.getGoalModeState()?.goal;
|
|
1668
|
+
if (!goal) return;
|
|
1669
|
+
const summary = goal.objective.length > 48 ? `${goal.objective.slice(0, 47)}…` : goal.objective;
|
|
1670
|
+
const title = state === "active" ? `Goal: ${summary} (${goal.status})` : `Goal paused: ${summary}`;
|
|
1671
|
+
const items =
|
|
1672
|
+
state === "active"
|
|
1673
|
+
? ["Show details", "Adjust budget…", "Pause", "Drop"]
|
|
1674
|
+
: ["Resume", "Show details", "Adjust budget…", "Drop"];
|
|
1675
|
+
const choice = await this.showHookSelector(title, items);
|
|
1676
|
+
if (!choice) return;
|
|
1677
|
+
switch (choice) {
|
|
1678
|
+
case "Show details":
|
|
1679
|
+
this.#showGoalDetails();
|
|
1680
|
+
return;
|
|
1681
|
+
case "Adjust budget…":
|
|
1682
|
+
await this.#promptGoalBudgetEdit();
|
|
1683
|
+
return;
|
|
1684
|
+
case "Pause":
|
|
1685
|
+
await this.#pauseGoalAction();
|
|
1686
|
+
return;
|
|
1687
|
+
case "Resume":
|
|
1688
|
+
await this.#resumeGoalAction();
|
|
1689
|
+
return;
|
|
1690
|
+
case "Drop":
|
|
1691
|
+
await this.#confirmAndDropGoal();
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
#showGoalDetails(): void {
|
|
1697
|
+
const state = this.session.getGoalModeState();
|
|
1698
|
+
const goal = state?.goal;
|
|
1699
|
+
if (!goal) {
|
|
1700
|
+
this.showStatus("No goal set.");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const used = goal.tokensUsed.toLocaleString();
|
|
1704
|
+
const budgetLine =
|
|
1705
|
+
goal.tokenBudget !== undefined
|
|
1706
|
+
? `${used} / ${goal.tokenBudget.toLocaleString()} (${Math.max(0, goal.tokenBudget - goal.tokensUsed).toLocaleString()} left)`
|
|
1707
|
+
: `${used} (no budget)`;
|
|
1708
|
+
const lines = [
|
|
1709
|
+
`Objective: ${goal.objective}`,
|
|
1710
|
+
`Status: ${goal.status}${state?.enabled ? "" : " (paused)"}`,
|
|
1711
|
+
`Tokens: ${budgetLine}`,
|
|
1712
|
+
`Time spent: ${formatDuration(goal.timeUsedSeconds * 1000)}`,
|
|
1713
|
+
];
|
|
1714
|
+
this.showStatus(lines.join("\n"));
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async #promptGoalBudgetEdit(): Promise<void> {
|
|
1718
|
+
const goal = this.session.getGoalModeState()?.goal;
|
|
1719
|
+
const prefill = goal?.tokenBudget !== undefined ? String(goal.tokenBudget) : "";
|
|
1720
|
+
const input = (
|
|
1721
|
+
await this.showHookEditor("Goal budget (number, `off`, or empty to cancel)", prefill, undefined, {
|
|
1722
|
+
promptStyle: true,
|
|
1723
|
+
})
|
|
1724
|
+
)?.trim();
|
|
1725
|
+
if (!input) return;
|
|
1726
|
+
await this.#handleGoalBudgetCommand(input);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
async #pauseGoalAction(): Promise<void> {
|
|
1730
|
+
if (!this.goalModeEnabled) {
|
|
1731
|
+
this.showWarning("No active goal to pause.");
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
await this.session.goalRuntime.pauseGoal();
|
|
1735
|
+
await this.#exitGoalMode({ paused: true, reason: "paused" });
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async #resumeGoalAction(): Promise<void> {
|
|
1739
|
+
if (!this.#getPausedGoalState()) {
|
|
1740
|
+
this.showWarning("No paused goal to resume.");
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
await this.#enterGoalMode({ resume: true, silent: true });
|
|
1744
|
+
this.showStatus("Goal mode resumed.");
|
|
1745
|
+
this.#scheduleGoalContinuation();
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async #confirmAndDropGoal(): Promise<void> {
|
|
1749
|
+
if (!this.goalModeEnabled && !this.#getPausedGoalState()) {
|
|
1750
|
+
this.showWarning("No goal to drop.");
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const confirmed = await this.showHookConfirm(
|
|
1754
|
+
"Drop goal?",
|
|
1755
|
+
"This removes the goal record. Accumulated usage stays in the session log.",
|
|
1756
|
+
);
|
|
1757
|
+
if (!confirmed) return;
|
|
1758
|
+
await this.session.goalRuntime.dropGoal();
|
|
1759
|
+
await this.#exitGoalMode({ reason: "dropped" });
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
async #startGoalFromObjective(objective: string): Promise<void> {
|
|
1763
|
+
await this.#enterGoalMode({ objective, silent: true });
|
|
1764
|
+
this.#resetGoalContinuationSuppression();
|
|
1765
|
+
if (this.onInputCallback) {
|
|
1766
|
+
this.onInputCallback(this.startPendingSubmission({ text: objective }));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
async #handleGoalSetSubcommand(rest: string): Promise<void> {
|
|
1771
|
+
if (this.goalModeEnabled) {
|
|
1772
|
+
this.showStatus("Goal mode is already active. Use /goal drop to start over.");
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (this.#getPausedGoalState()) {
|
|
1776
|
+
this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const objective = rest.trim()
|
|
1780
|
+
? rest.trim()
|
|
1781
|
+
: (await this.showHookEditor("Goal objective", undefined, undefined, { promptStyle: true }))?.trim();
|
|
1782
|
+
if (!objective) return;
|
|
1783
|
+
await this.#startGoalFromObjective(objective);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
|
|
1216
1787
|
if (!this.planModeEnabled) {
|
|
1217
1788
|
this.showWarning("Plan mode is not active.");
|
|
1218
1789
|
return;
|
|
1219
1790
|
}
|
|
1220
1791
|
|
|
1221
|
-
// Abort the agent to prevent it from continuing (e.g.,
|
|
1222
|
-
//
|
|
1223
|
-
// (agent's #emit is fire-and-forget), so without this the model sees
|
|
1224
|
-
// ready for approval." and immediately
|
|
1792
|
+
// Abort the agent to prevent it from continuing (e.g., re-submitting the
|
|
1793
|
+
// plan) while the popup is showing. The event listener fires asynchronously
|
|
1794
|
+
// (agent's #emit is fire-and-forget), so without this the model sees
|
|
1795
|
+
// "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
|
|
1225
1796
|
await this.session.abort();
|
|
1226
1797
|
|
|
1227
1798
|
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
@@ -1291,6 +1862,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1291
1862
|
this.loadingAnimation = undefined;
|
|
1292
1863
|
}
|
|
1293
1864
|
this.#cleanupMicAnimation();
|
|
1865
|
+
this.#cancelGoalContinuation();
|
|
1294
1866
|
if (this.#sttController) {
|
|
1295
1867
|
this.#sttController.dispose();
|
|
1296
1868
|
this.#sttController = undefined;
|