@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
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 +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- 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/args.ts +2 -2
- 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 +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- 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 +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -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/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- 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/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 +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- 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/read-tool-group.ts +29 -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 +55 -4
- 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 +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- 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 +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- 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/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- 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/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- 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-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- 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/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- 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/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- 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/gemini.ts +35 -95
- 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,18 +37,26 @@ 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" };
|
|
45
|
+
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
46
|
+
type: "text",
|
|
47
|
+
};
|
|
44
48
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
49
|
+
import type { CompactionOutcome } from "../session/compaction";
|
|
45
50
|
import { HistoryStorage } from "../session/history-storage";
|
|
46
51
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
47
52
|
import { getRecentSessions } from "../session/session-manager";
|
|
53
|
+
import { formatDuration } from "../slash-commands/helpers/format";
|
|
48
54
|
import { STTController, type SttState } from "../stt";
|
|
49
|
-
import type {
|
|
55
|
+
import type { LspStartupServerInfo } from "../tools";
|
|
50
56
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
57
|
+
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
51
58
|
import { formatPhaseDisplayName } from "../tools/todo-write";
|
|
59
|
+
import { ToolError } from "../tools/tool-errors";
|
|
52
60
|
import type { EventBus } from "../utils/event-bus";
|
|
53
61
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
54
62
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
@@ -123,6 +131,22 @@ function formatHudNoteMarker(count: number): string {
|
|
|
123
131
|
return theme.fg("dim", chalk.italic(` \u207a${sub}`));
|
|
124
132
|
}
|
|
125
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
|
+
|
|
126
150
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
127
151
|
export interface InteractiveModeOptions {
|
|
128
152
|
/** Providers that were migrated during startup */
|
|
@@ -164,6 +188,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
164
188
|
todoExpanded = false;
|
|
165
189
|
planModeEnabled = false;
|
|
166
190
|
planModePaused = false;
|
|
191
|
+
goalModeEnabled = false;
|
|
192
|
+
goalModePaused = false;
|
|
167
193
|
planModePlanFilePath: string | undefined = undefined;
|
|
168
194
|
loopModeEnabled = false;
|
|
169
195
|
loopPrompt: string | undefined = undefined;
|
|
@@ -212,6 +238,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
212
238
|
readonly #version: string;
|
|
213
239
|
readonly #changelogMarkdown: string | undefined;
|
|
214
240
|
#planModePreviousTools: string[] | undefined;
|
|
241
|
+
#goalModePreviousTools: string[] | undefined;
|
|
242
|
+
#goalContinuationTimer: NodeJS.Timeout | undefined;
|
|
243
|
+
#goalTurnHadToolCalls = false;
|
|
244
|
+
#goalContinuationTurnInFlight = false;
|
|
245
|
+
#goalSuppressNextContinuation = false;
|
|
215
246
|
#planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
216
247
|
#pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
217
248
|
#planModeHasEntered = false;
|
|
@@ -473,6 +504,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
473
504
|
// Subscribe to agent events
|
|
474
505
|
this.#subscribeToAgent();
|
|
475
506
|
|
|
507
|
+
this.#eventBusUnsubscribers.push(
|
|
508
|
+
this.session.subscribe(event => {
|
|
509
|
+
void this.#handleGoalSessionEvent(event);
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
476
512
|
// Set up theme file watcher
|
|
477
513
|
onThemeChange(() => {
|
|
478
514
|
clearRenderCache();
|
|
@@ -516,12 +552,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
516
552
|
}
|
|
517
553
|
|
|
518
554
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
555
|
+
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
556
|
+
await this.#exitGoalMode({ reason: "completed", silent: true });
|
|
557
|
+
}
|
|
519
558
|
const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
|
|
520
559
|
this.onInputCallback = input => {
|
|
521
560
|
this.onInputCallback = undefined;
|
|
522
561
|
resolve(input);
|
|
523
562
|
};
|
|
524
563
|
this.#scheduleLoopAutoSubmit();
|
|
564
|
+
this.#scheduleGoalContinuation();
|
|
525
565
|
return promise;
|
|
526
566
|
}
|
|
527
567
|
|
|
@@ -545,6 +585,48 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
545
585
|
}
|
|
546
586
|
}
|
|
547
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
|
+
|
|
548
630
|
async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
|
|
549
631
|
if (!consumeLoopLimitIteration(this.loopLimit)) {
|
|
550
632
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
@@ -635,23 +717,36 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
635
717
|
}
|
|
636
718
|
}
|
|
637
719
|
|
|
638
|
-
startPendingSubmission(input: {
|
|
720
|
+
startPendingSubmission(input: {
|
|
721
|
+
text: string;
|
|
722
|
+
images?: ImageContent[];
|
|
723
|
+
customType?: string;
|
|
724
|
+
display?: boolean;
|
|
725
|
+
}): SubmittedUserInput {
|
|
639
726
|
const submission: SubmittedUserInput = {
|
|
640
727
|
text: input.text,
|
|
641
728
|
images: input.images,
|
|
729
|
+
customType: input.customType,
|
|
730
|
+
display: input.display,
|
|
642
731
|
cancelled: false,
|
|
643
732
|
started: false,
|
|
644
733
|
};
|
|
645
734
|
this.#pendingSubmittedInput = submission;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|
|
655
750
|
this.editor.setText("");
|
|
656
751
|
this.ensureLoadingAnimation();
|
|
657
752
|
this.ui.requestRender();
|
|
@@ -670,14 +765,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
670
765
|
this.#pendingSubmissionDispose?.();
|
|
671
766
|
this.#pendingSubmissionDispose = undefined;
|
|
672
767
|
this.#pendingWorkingMessage = undefined;
|
|
768
|
+
if (submission.customType === "goal-continuation") {
|
|
769
|
+
this.#goalContinuationTurnInFlight = false;
|
|
770
|
+
}
|
|
673
771
|
if (this.loadingAnimation) {
|
|
674
772
|
this.loadingAnimation.stop();
|
|
675
773
|
this.loadingAnimation = undefined;
|
|
676
774
|
this.statusContainer.clear();
|
|
677
775
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
776
|
+
if (!submission.customType) {
|
|
777
|
+
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
778
|
+
this.rebuildChatFromMessages();
|
|
779
|
+
this.editor.setText(submission.text);
|
|
780
|
+
}
|
|
681
781
|
this.updateEditorBorderColor();
|
|
682
782
|
this.ui.requestRender();
|
|
683
783
|
return true;
|
|
@@ -698,6 +798,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
698
798
|
this.#pendingSubmittedInput = undefined;
|
|
699
799
|
this.#pendingSubmissionDispose = undefined;
|
|
700
800
|
}
|
|
801
|
+
if (input.customType === "goal-continuation") {
|
|
802
|
+
this.#goalContinuationTurnInFlight = false;
|
|
803
|
+
}
|
|
701
804
|
|
|
702
805
|
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
703
806
|
this.optimisticUserMessageSignature = undefined;
|
|
@@ -853,6 +956,95 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
853
956
|
this.ui.requestRender();
|
|
854
957
|
}
|
|
855
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
|
+
|
|
856
1048
|
async #applyPlanModeModel(): Promise<void> {
|
|
857
1049
|
const resolved = this.session.resolveRoleModelWithThinking("plan");
|
|
858
1050
|
if (!resolved.model) return;
|
|
@@ -899,6 +1091,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
899
1091
|
/** Restore mode state from session entries on resume (e.g. plan mode). */
|
|
900
1092
|
async #restoreModeFromSession(): Promise<void> {
|
|
901
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
|
+
}
|
|
902
1116
|
if (!this.session.settings.get("plan.enabled")) {
|
|
903
1117
|
// Clear stale plan/plan_paused mode so re-enabling the setting
|
|
904
1118
|
// later doesn't unexpectedly restore an old plan session.
|
|
@@ -921,13 +1135,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
921
1135
|
if (this.planModeEnabled) {
|
|
922
1136
|
return;
|
|
923
1137
|
}
|
|
1138
|
+
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1139
|
+
this.showWarning("Exit goal mode first.");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
924
1142
|
|
|
925
1143
|
this.planModePaused = false;
|
|
926
1144
|
|
|
927
1145
|
const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
|
|
928
1146
|
const previousTools = this.session.getActiveToolNames();
|
|
929
|
-
const
|
|
930
|
-
const planTools =
|
|
1147
|
+
const hasResolveTool = this.session.getToolByName("resolve") !== undefined;
|
|
1148
|
+
const planTools = hasResolveTool ? [...previousTools, "resolve"] : previousTools;
|
|
931
1149
|
const uniquePlanTools = [...new Set(planTools)];
|
|
932
1150
|
|
|
933
1151
|
this.#planModePreviousTools = previousTools;
|
|
@@ -941,6 +1159,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
941
1159
|
workflow: options?.workflow ?? "parallel",
|
|
942
1160
|
reentry: this.#planModeHasEntered,
|
|
943
1161
|
});
|
|
1162
|
+
this.session.setStandingResolveHandler?.(input => this.#runPlanApprovalResolve(input));
|
|
944
1163
|
if (this.session.isStreaming) {
|
|
945
1164
|
await this.session.sendPlanModeContext({ deliverAs: "steer" });
|
|
946
1165
|
}
|
|
@@ -951,6 +1170,47 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
951
1170
|
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
|
|
952
1171
|
}
|
|
953
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
|
+
|
|
954
1214
|
async #exitPlanMode(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
|
|
955
1215
|
if (!this.planModeEnabled) {
|
|
956
1216
|
return;
|
|
@@ -986,6 +1246,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
986
1246
|
}
|
|
987
1247
|
}
|
|
988
1248
|
}
|
|
1249
|
+
this.session.setStandingResolveHandler?.(null);
|
|
989
1250
|
this.session.setPlanModeState(undefined);
|
|
990
1251
|
this.planModeEnabled = false;
|
|
991
1252
|
this.planModePaused = options?.paused ?? false;
|
|
@@ -1000,6 +1261,73 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1000
1261
|
}
|
|
1001
1262
|
}
|
|
1002
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
|
+
|
|
1003
1331
|
async #readPlanFile(planFilePath: string): Promise<string | null> {
|
|
1004
1332
|
const resolvedPath = this.#resolvePlanFilePath(planFilePath);
|
|
1005
1333
|
try {
|
|
@@ -1109,7 +1437,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1109
1437
|
|
|
1110
1438
|
async #approvePlan(
|
|
1111
1439
|
planContent: string,
|
|
1112
|
-
options: {
|
|
1440
|
+
options: {
|
|
1441
|
+
planFilePath: string;
|
|
1442
|
+
finalPlanFilePath: string;
|
|
1443
|
+
preserveContext?: boolean;
|
|
1444
|
+
compactBeforeExecute?: boolean;
|
|
1445
|
+
},
|
|
1113
1446
|
): Promise<void> {
|
|
1114
1447
|
await renameApprovedPlanFile({
|
|
1115
1448
|
planFilePath: options.planFilePath,
|
|
@@ -1118,21 +1451,80 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1118
1451
|
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1119
1452
|
});
|
|
1120
1453
|
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
+
}
|
|
1464
|
+
let compactOutcome: CompactionOutcome | undefined;
|
|
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();
|
|
1131
1503
|
}
|
|
1504
|
+
|
|
1505
|
+
// Tool restoration runs on every path — the plan mode tools must be
|
|
1506
|
+
// retired regardless of whether the synthetic prompt fires.
|
|
1132
1507
|
if (previousTools.length > 0) {
|
|
1133
1508
|
await this.session.setActiveToolsByName(previousTools);
|
|
1134
1509
|
}
|
|
1135
1510
|
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
1511
|
+
|
|
1512
|
+
if (compactOutcome === "cancelled") {
|
|
1513
|
+
// Explicit abort: honor it. `executeCompaction` already surfaced
|
|
1514
|
+
// `showError("Compaction cancelled")` to the operator; we add the
|
|
1515
|
+
// deferred-dispatch warning and exit. `markPlanReferenceSent` is
|
|
1516
|
+
// intentionally skipped here: `#planReferenceSent` stays false, so
|
|
1517
|
+
// `AgentSession.#buildPlanReferenceMessage` will inject the plan
|
|
1518
|
+
// reference on the operator's next `prompt()` call. If we marked it
|
|
1519
|
+
// sent here, the executor's first turn would have no plan context.
|
|
1520
|
+
this.showWarning(
|
|
1521
|
+
"Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
|
|
1522
|
+
);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// markPlanReferenceSent fires only on the dispatch path so the synthetic
|
|
1527
|
+
// plan-approved prompt is the source of the reference injection.
|
|
1136
1528
|
this.session.markPlanReferenceSent();
|
|
1137
1529
|
const planModePrompt = prompt.render(planModeApprovedPrompt, {
|
|
1138
1530
|
planContent,
|
|
@@ -1143,6 +1535,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1143
1535
|
}
|
|
1144
1536
|
|
|
1145
1537
|
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
|
|
1538
|
+
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1539
|
+
this.showWarning("Exit goal mode first.");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1146
1542
|
if (this.planModeEnabled) {
|
|
1147
1543
|
const confirmed = await this.showHookConfirm(
|
|
1148
1544
|
"Exit plan mode?",
|
|
@@ -1162,16 +1558,241 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1162
1558
|
}
|
|
1163
1559
|
}
|
|
1164
1560
|
|
|
1165
|
-
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> {
|
|
1166
1787
|
if (!this.planModeEnabled) {
|
|
1167
1788
|
this.showWarning("Plan mode is not active.");
|
|
1168
1789
|
return;
|
|
1169
1790
|
}
|
|
1170
1791
|
|
|
1171
|
-
// Abort the agent to prevent it from continuing (e.g.,
|
|
1172
|
-
//
|
|
1173
|
-
// (agent's #emit is fire-and-forget), so without this the model sees
|
|
1174
|
-
// 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.
|
|
1175
1796
|
await this.session.abort();
|
|
1176
1797
|
|
|
1177
1798
|
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
@@ -1185,14 +1806,24 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1185
1806
|
this.#renderPlanPreview(planContent, { append: true });
|
|
1186
1807
|
const choice = await this.showHookSelector(
|
|
1187
1808
|
"Plan mode - next step",
|
|
1188
|
-
[
|
|
1809
|
+
[
|
|
1810
|
+
"Approve and execute",
|
|
1811
|
+
"Approve and compact context",
|
|
1812
|
+
"Approve and keep context",
|
|
1813
|
+
"Refine plan",
|
|
1814
|
+
"Stay in plan mode",
|
|
1815
|
+
],
|
|
1189
1816
|
{
|
|
1190
1817
|
helpText: this.#getPlanReviewHelpText(),
|
|
1191
1818
|
onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
|
|
1192
1819
|
},
|
|
1193
1820
|
);
|
|
1194
1821
|
|
|
1195
|
-
if (
|
|
1822
|
+
if (
|
|
1823
|
+
choice === "Approve and execute" ||
|
|
1824
|
+
choice === "Approve and compact context" ||
|
|
1825
|
+
choice === "Approve and keep context"
|
|
1826
|
+
) {
|
|
1196
1827
|
const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
|
|
1197
1828
|
try {
|
|
1198
1829
|
const latestPlanContent = await this.#readPlanFile(planFilePath);
|
|
@@ -1203,7 +1834,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1203
1834
|
await this.#approvePlan(latestPlanContent, {
|
|
1204
1835
|
planFilePath,
|
|
1205
1836
|
finalPlanFilePath,
|
|
1206
|
-
preserveContext: choice
|
|
1837
|
+
preserveContext: choice !== "Approve and execute",
|
|
1838
|
+
compactBeforeExecute: choice === "Approve and compact context",
|
|
1207
1839
|
});
|
|
1208
1840
|
} catch (error) {
|
|
1209
1841
|
this.showError(
|
|
@@ -1230,6 +1862,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1230
1862
|
this.loadingAnimation = undefined;
|
|
1231
1863
|
}
|
|
1232
1864
|
this.#cleanupMicAnimation();
|
|
1865
|
+
this.#cancelGoalContinuation();
|
|
1233
1866
|
if (this.#sttController) {
|
|
1234
1867
|
this.#sttController.dispose();
|
|
1235
1868
|
this.#sttController = undefined;
|
|
@@ -1727,7 +2360,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1727
2360
|
await controller.handle(text);
|
|
1728
2361
|
}
|
|
1729
2362
|
|
|
1730
|
-
handleCompactCommand(customInstructions?: string): Promise<
|
|
2363
|
+
handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
|
|
1731
2364
|
return this.#commandController.handleCompactCommand(customInstructions);
|
|
1732
2365
|
}
|
|
1733
2366
|
|
|
@@ -1735,7 +2368,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1735
2368
|
return this.#commandController.handleHandoffCommand(customInstructions);
|
|
1736
2369
|
}
|
|
1737
2370
|
|
|
1738
|
-
executeCompaction(
|
|
2371
|
+
executeCompaction(
|
|
2372
|
+
customInstructionsOrOptions?: string | CompactOptions,
|
|
2373
|
+
isAuto?: boolean,
|
|
2374
|
+
): Promise<CompactionOutcome> {
|
|
1739
2375
|
return this.#commandController.executeCompaction(customInstructionsOrOptions, isAuto);
|
|
1740
2376
|
}
|
|
1741
2377
|
|