diff --git a/src-tauri/src/commands/agent.rs b/src-tauri/src/commands/agent.rs index 0b73853a..e8568c10 100644 --- a/src-tauri/src/commands/agent.rs +++ b/src-tauri/src/commands/agent.rs @@ -145,6 +145,7 @@ pub async fn thread_execute_approved_plan( let action = match action.as_str() { "apply_plan" => PlanApprovalAction::ApplyPlan, "apply_plan_with_context_reset" => PlanApprovalAction::ApplyPlanWithContextReset, + "apply_plan_with_goal" => PlanApprovalAction::ApplyPlanWithGoal, other => { return Err(AppError::recoverable( crate::model::errors::ErrorSource::Thread, diff --git a/src-tauri/src/core/agent_run_manager.rs b/src-tauri/src/core/agent_run_manager.rs index e6c8a6ad..6c3ab5c0 100644 --- a/src-tauri/src/core/agent_run_manager.rs +++ b/src-tauri/src/core/agent_run_manager.rs @@ -439,11 +439,23 @@ impl AgentRunManager { plan_metadata.approval_state = IMPLEMENTATION_PLAN_APPROVED_STATE.to_string(); - let implementation_prompt = - build_implementation_handoff_prompt(thread_id, &plan_metadata, action.clone()); + let implementation_prompt = match action { + PlanApprovalAction::ApplyPlanWithGoal => { + // Build the goal objective text using the plan file path. + let plan_path = crate::core::plan_checkpoint::plan_file_path(thread_id) + .map(|p| p.display().to_string()) + .unwrap_or_default(); + format!( + "Please follow the implementation plan {} to complete all implementation, and conduct reviews at each stage following the plan, ensuring the implementation follows the design in the plan and meets quality standards.", + plan_path + ) + } + _ => build_implementation_handoff_prompt(thread_id, &plan_metadata, action.clone()), + }; let (history_override, context_seed_messages) = match action { PlanApprovalAction::ApplyPlan => (None, None), - PlanApprovalAction::ApplyPlanWithContextReset => { + PlanApprovalAction::ApplyPlanWithContextReset + | PlanApprovalAction::ApplyPlanWithGoal => { let message_bundle = self .build_context_reset_message_bundle(thread_id, &plan_metadata) .await?; @@ -454,6 +466,19 @@ impl AgentRunManager { } }; + // For ApplyPlanWithGoal: create a persistent goal before starting the + // implementation run so the goal continuation loop can drive execution. + if let PlanApprovalAction::ApplyPlanWithGoal = action { + let goal_manager = crate::core::goal_manager::GoalManager::new( + self.pool.clone(), + thread_id.to_string(), + Arc::clone(&self.goal_runtime_state), + ); + goal_manager + .create_goal(&implementation_prompt, None) + .await?; + } + let result = self .start_run_with_options( thread_id, diff --git a/src-tauri/src/core/agent_run_summary.rs b/src-tauri/src/core/agent_run_summary.rs index e51b9120..d8452458 100644 --- a/src-tauri/src/core/agent_run_summary.rs +++ b/src-tauri/src/core/agent_run_summary.rs @@ -82,6 +82,9 @@ pub(crate) fn build_implementation_handoff_prompt( PlanApprovalAction::ApplyPlanWithContextReset => { "The user approved this plan after clearing the planning conversation from the implementation context." } + PlanApprovalAction::ApplyPlanWithGoal => { + "The user approved this plan for goal-driven implementation with context reset." + } }; let plan_file_note = crate::core::plan_checkpoint::plan_file_path(thread_id) .filter(|path| path.exists()) @@ -99,12 +102,14 @@ pub(crate) fn build_implementation_handoff_prompt( &plan_markdown, ) } - PlanApprovalAction::ApplyPlanWithContextReset => render_handoff_template_no_plan( - include_str!("prompt/templates/handoff/without_plan.tpl.md"), - action_note, - &metadata.artifact.plan_revision.to_string(), - &plan_file_note, - ), + PlanApprovalAction::ApplyPlanWithContextReset | PlanApprovalAction::ApplyPlanWithGoal => { + render_handoff_template_no_plan( + include_str!("prompt/templates/handoff/without_plan.tpl.md"), + action_note, + &metadata.artifact.plan_revision.to_string(), + &plan_file_note, + ) + } } } diff --git a/src-tauri/src/core/agent_session_execution.rs b/src-tauri/src/core/agent_session_execution.rs index fa98d07e..fb858070 100644 --- a/src-tauri/src/core/agent_session_execution.rs +++ b/src-tauri/src/core/agent_session_execution.rs @@ -1039,8 +1039,13 @@ impl AgentSession { let approval_message_id = uuid::Uuid::now_v7().to_string(); let plan_metadata = build_plan_message_metadata(artifact.clone(), &self.spec.run_id, &self.spec.run_mode); - let approval_metadata = - build_approval_prompt_metadata(artifact.plan_revision, &plan_message_id); + let approval_metadata = build_approval_prompt_metadata( + &self.pool, + &self.spec.thread_id, + artifact.plan_revision, + &plan_message_id, + ) + .await; let plan_message = MessageRecord { id: plan_message_id.clone(), diff --git a/src-tauri/src/core/plan_checkpoint.rs b/src-tauri/src/core/plan_checkpoint.rs index a9898967..e3ab8249 100644 --- a/src-tauri/src/core/plan_checkpoint.rs +++ b/src-tauri/src/core/plan_checkpoint.rs @@ -11,13 +11,15 @@ pub const IMPLEMENTATION_PLAN_SUPERSEDED_STATE: &str = "superseded"; pub enum PlanApprovalAction { ApplyPlan, ApplyPlanWithContextReset, + ApplyPlanWithGoal, } impl PlanApprovalAction { pub fn label(&self) -> &'static str { match self { Self::ApplyPlan => "按计划实施", - Self::ApplyPlanWithContextReset => "清理上下文后按计划实施", + Self::ApplyPlanWithContextReset => "清理上下文后实施", + Self::ApplyPlanWithGoal => "按计划设置Goal后实施", } } } @@ -103,27 +105,44 @@ pub fn build_plan_message_metadata( } } -pub fn build_approval_prompt_metadata( +pub async fn build_approval_prompt_metadata( + pool: &sqlx::SqlitePool, + thread_id: &str, plan_revision: u32, plan_message_id: &str, ) -> ApprovalPromptMetadata { + let mut options = vec![ + PlanApprovalOption { + action: PlanApprovalAction::ApplyPlan, + label: PlanApprovalAction::ApplyPlan.label().to_string(), + }, + PlanApprovalOption { + action: PlanApprovalAction::ApplyPlanWithContextReset, + label: PlanApprovalAction::ApplyPlanWithContextReset + .label() + .to_string(), + }, + ]; + + // Only offer the goal-driven option when the thread has no existing goal + // record (any status: active, paused, budget_limited, or complete). + if crate::persistence::repo::goal_repo::find_by_thread_id(pool, thread_id) + .await + .map(|opt| opt.is_none()) + .unwrap_or(false) + { + options.push(PlanApprovalOption { + action: PlanApprovalAction::ApplyPlanWithGoal, + label: PlanApprovalAction::ApplyPlanWithGoal.label().to_string(), + }); + } + ApprovalPromptMetadata { kind: IMPLEMENTATION_PLAN_APPROVAL_KIND.to_string(), plan_revision, plan_message_id: plan_message_id.to_string(), state: IMPLEMENTATION_PLAN_PENDING_STATE.to_string(), - options: vec![ - PlanApprovalOption { - action: PlanApprovalAction::ApplyPlan, - label: PlanApprovalAction::ApplyPlan.label().to_string(), - }, - PlanApprovalOption { - action: PlanApprovalAction::ApplyPlanWithContextReset, - label: PlanApprovalAction::ApplyPlanWithContextReset - .label() - .to_string(), - }, - ], + options, expires_on_new_user_message: true, approved_action: None, } @@ -424,6 +443,24 @@ mod tests { parse_approval_prompt_metadata, parse_plan_message_metadata, plan_design_markdown, plan_markdown, write_plan_file_to, PlanApprovalAction, }; + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; + use std::str::FromStr; + + async fn setup_test_pool() -> sqlx::SqlitePool { + let options = SqliteConnectOptions::from_str("sqlite::memory:") + .expect("invalid sqlite options") + .foreign_keys(true); + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await + .expect("pool"); + sqlx::migrate!("./migrations") + .run(&pool) + .await + .expect("migrations"); + pool + } #[test] fn plan_artifact_builder_accepts_string_and_object_steps() { @@ -470,8 +507,9 @@ mod tests { ); } - #[test] - fn metadata_round_trip_is_stable() { + #[tokio::test] + async fn metadata_round_trip_is_stable() { + let pool = setup_test_pool().await; let artifact = build_plan_artifact_from_tool_input( &serde_json::json!({ "title": "Implement checkpoint", @@ -484,11 +522,18 @@ mod tests { parse_plan_message_metadata(&serde_json::to_value(&metadata).unwrap()).unwrap(); assert_eq!(parsed.artifact, artifact); - let approval = build_approval_prompt_metadata(artifact.plan_revision, "msg-plan"); + let approval = build_approval_prompt_metadata( + &pool, + "thread-no-goal", + artifact.plan_revision, + "msg-plan", + ) + .await; let parsed_approval = parse_approval_prompt_metadata(&serde_json::to_value(&approval).unwrap()).unwrap(); assert_eq!(parsed_approval.plan_revision, 1); - assert_eq!(parsed_approval.options.len(), 2); + // With no existing goal, the third option (ApplyPlanWithGoal) is included. + assert_eq!(parsed_approval.options.len(), 3); assert_eq!( parsed_approval.options[0].action, PlanApprovalAction::ApplyPlan @@ -496,6 +541,89 @@ mod tests { assert!(approval_prompt_markdown(&artifact).contains("Implement checkpoint")); } + #[tokio::test] + async fn build_approval_prompt_metadata_suppresses_goal_option_when_goal_exists() { + use crate::model::goal::{GoalRecord, GoalStatus}; + use crate::persistence::repo::goal_repo; + use chrono::Utc; + + let pool = setup_test_pool().await; + + // Seed workspace + thread to satisfy the goals → threads FK. + let now = Utc::now().to_rfc3339(); + sqlx::query( + "INSERT INTO workspaces (id, name, path, canonical_path, display_path, + is_default, is_git, auto_work_tree, status, created_at, updated_at) + VALUES ('ws-goal', 'Test', '/tmp/test', '/tmp/test', '/tmp/test', + 0, 0, 0, 'ready', ?, ?)", + ) + .bind(&now) + .bind(&now) + .execute(&pool) + .await + .expect("seed workspace"); + + sqlx::query( + "INSERT INTO threads (id, workspace_id, title, status, last_active_at, created_at, updated_at) + VALUES ('thread-with-goal', 'ws-goal', 'Test', 'idle', ?, ?, ?)", + ) + .bind(&now) + .bind(&now) + .bind(&now) + .execute(&pool) + .await + .expect("seed thread"); + + // Insert an active goal for this thread. + let goal = GoalRecord { + id: "goal-1".to_string(), + thread_id: "thread-with-goal".to_string(), + objective: "Build feature X".to_string(), + status: GoalStatus::Active, + token_budget: None, + tokens_used: 0, + turns_used: 0, + max_turns: 50, + pause_reason: None, + pause_detail: None, + evidence: None, + last_evaluated_run_id: None, + judge_passed: false, + judge_completeness: None, + judge_findings: None, + judge_summary: None, + judge_evaluated_run_id: None, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + goal_repo::insert(&pool, &goal).await.expect("insert goal"); + + let artifact = build_plan_artifact_from_tool_input( + &serde_json::json!({"title": "Test", "summary": "Test plan."}), + 1, + ); + let approval = build_approval_prompt_metadata( + &pool, + "thread-with-goal", + artifact.plan_revision, + "msg-plan", + ) + .await; + + // When a goal already exists, only 2 options should be returned + // (ApplyPlan and ApplyPlanWithContextReset), excluding ApplyPlanWithGoal. + assert_eq!(approval.options.len(), 2); + assert_eq!(approval.options[0].action, PlanApprovalAction::ApplyPlan); + assert_eq!( + approval.options[1].action, + PlanApprovalAction::ApplyPlanWithContextReset + ); + assert!(!approval + .options + .iter() + .any(|o| o.action == PlanApprovalAction::ApplyPlanWithGoal)); + } + #[test] fn plan_markdown_renders_new_structured_sections() { let artifact = build_plan_artifact_from_tool_input( diff --git a/src-tauri/tests/agent_run.rs b/src-tauri/tests/agent_run.rs index f613da26..49f53b55 100644 --- a/src-tauri/tests/agent_run.rs +++ b/src-tauri/tests/agent_run.rs @@ -1121,3 +1121,199 @@ async fn test_render_validation_failure_persists_failed_tool_call() { .unwrap() .contains("valid 'spec' object")); } + +// ========================================================================= +// ApplyPlanWithGoal: goal-creation integration test +// ========================================================================= + +#[tokio::test] +async fn test_execute_approved_plan_with_goal_creates_goal_record() { + use std::sync::{Arc, Mutex as StdMutex}; + use tiycode_lib::core::agent_run_manager::AgentRunManager; + use tiycode_lib::core::app_event_emitter::NoopAppEventEmitter; + use tiycode_lib::core::app_state::GoalRuntimeState; + use tiycode_lib::core::built_in_agent_runtime::BuiltInAgentRuntime; + use tiycode_lib::core::plan_checkpoint::{ + build_plan_artifact_from_tool_input, build_plan_message_metadata, ApprovalPromptMetadata, + PlanApprovalAction, PlanApprovalOption, IMPLEMENTATION_PLAN_APPROVAL_KIND, + IMPLEMENTATION_PLAN_PENDING_STATE, + }; + use tiycode_lib::core::sleep_manager::SleepManager; + use tiycode_lib::core::terminal_manager::TerminalManager; + use tiycode_lib::core::tool_gateway::ToolGateway; + use tiycode_lib::model::thread::MessageRecord; + use tiycode_lib::persistence::repo::{goal_repo, message_repo, run_repo}; + + let pool = test_helpers::setup_test_pool().await; + test_helpers::seed_workspace(&pool, "ws-goal-plan", "/tmp/goal-plan").await; + test_helpers::seed_thread(&pool, "t-goal-plan", "ws-goal-plan", None).await; + + // Insert a provider so build_session_spec can resolve the model plan. + sqlx::query( + "INSERT INTO providers ( + id, provider_kind, provider_key, name, protocol_type, base_url, + api_key_encrypted, enabled, mapping_locked + ) VALUES ('prov-goal', 'builtin', 'openai', 'OpenAI', 'openai', + 'https://api.openai.com/v1', 'sk-test', 1, 1)", + ) + .execute(&pool) + .await + .unwrap(); + + // Model plan JSON matching the provider we just seeded. + let model_plan = serde_json::json!({ + "primary": { + "providerId": "prov-goal", + "modelRecordId": "model-goal", + "providerType": "openai", + "providerName": "OpenAI", + "model": "gpt-4.1", + "modelId": "gpt-4.1", + "modelDisplayName": "GPT-4.1", + "baseUrl": "https://api.openai.com/v1", + "contextWindow": "128000", + "maxOutputTokens": "16384" + } + }); + + // Create the planning run with the model plan. + run_repo::insert( + &pool, + &run_repo::RunInsert { + id: "r-planning".to_string(), + thread_id: "t-goal-plan".to_string(), + profile_id: None, + run_mode: "plan".to_string(), + provider_id: None, + model_id: None, + effective_model_plan_json: Some(model_plan.to_string()), + status: "waiting_approval".to_string(), + }, + ) + .await + .unwrap(); + + // Create the plan message with PlanMessageMetadata. + let artifact = build_plan_artifact_from_tool_input( + &serde_json::json!({ + "title": "Goal-driven plan test", + "summary": "Verify ApplyPlanWithGoal creates a goal." + }), + 1, + ); + let plan_metadata = build_plan_message_metadata(artifact.clone(), "r-planning", "plan"); + let plan_message = MessageRecord { + id: "m-plan".to_string(), + thread_id: "t-goal-plan".to_string(), + run_id: Some("r-planning".to_string()), + role: "assistant".to_string(), + content_markdown: "# Goal-driven plan test".to_string(), + parts_json: None, + message_type: "plan".to_string(), + status: "completed".to_string(), + metadata_json: serde_json::to_string(&plan_metadata).ok(), + attachments_json: None, + created_at: String::new(), + }; + message_repo::insert(&pool, &plan_message).await.unwrap(); + + // Create the approval_prompt message with ApprovalPromptMetadata (pending). + let approval_metadata = ApprovalPromptMetadata { + kind: IMPLEMENTATION_PLAN_APPROVAL_KIND.to_string(), + plan_revision: 1, + plan_message_id: "m-plan".to_string(), + state: IMPLEMENTATION_PLAN_PENDING_STATE.to_string(), + options: vec![ + PlanApprovalOption { + action: PlanApprovalAction::ApplyPlan, + label: PlanApprovalAction::ApplyPlan.label().to_string(), + }, + PlanApprovalOption { + action: PlanApprovalAction::ApplyPlanWithContextReset, + label: PlanApprovalAction::ApplyPlanWithContextReset + .label() + .to_string(), + }, + PlanApprovalOption { + action: PlanApprovalAction::ApplyPlanWithGoal, + label: PlanApprovalAction::ApplyPlanWithGoal.label().to_string(), + }, + ], + expires_on_new_user_message: true, + approved_action: None, + }; + let approval_message = MessageRecord { + id: "m-approval".to_string(), + thread_id: "t-goal-plan".to_string(), + run_id: Some("r-planning".to_string()), + role: "assistant".to_string(), + content_markdown: "Plan approval prompt".to_string(), + parts_json: None, + message_type: "approval_prompt".to_string(), + status: "completed".to_string(), + metadata_json: serde_json::to_string(&approval_metadata).ok(), + attachments_json: None, + created_at: String::new(), + }; + message_repo::insert(&pool, &approval_message) + .await + .unwrap(); + + // Build AgentRunManager with real dependencies. + let goal_runtime_state = Arc::new(StdMutex::new(GoalRuntimeState::default())); + let terminal_manager = Arc::new(TerminalManager::new(pool.clone())); + let tool_gateway = Arc::new(ToolGateway::new( + pool.clone(), + Arc::clone(&terminal_manager), + )); + let runtime = Arc::new(BuiltInAgentRuntime::new( + pool.clone(), + Arc::clone(&tool_gateway), + Arc::clone(&goal_runtime_state), + )); + let sleep_manager = Arc::new(SleepManager::new()); + let app_events: Arc = + Arc::new(NoopAppEventEmitter); + let manager = Arc::new(AgentRunManager::new( + pool.clone(), + app_events, + runtime, + sleep_manager, + goal_runtime_state, + )); + + // Execute the approved plan with ApplyPlanWithGoal. + let result = manager + .execute_approved_plan( + "t-goal-plan", + "m-approval", + PlanApprovalAction::ApplyPlanWithGoal, + ) + .await; + + // The run should start successfully (the LLM call happens asynchronously). + // Even if it fails, the goal record should already be persisted. + if result.is_ok() { + // Clean up: cancel the spawned run task to avoid background panics. + let _ = manager.cancel_run("t-goal-plan").await; + } + + // Verify that a goal record was created in the database. + let goal = goal_repo::find_by_thread_id(&pool, "t-goal-plan") + .await + .expect("query goals"); + assert!( + goal.is_some(), + "ApplyPlanWithGoal should create a goal record; execute_approved_plan returned: {:?}", + result.as_ref().err() + ); + + let goal = goal.unwrap(); + assert_eq!(goal.thread_id, "t-goal-plan"); + assert_eq!(goal.status, tiycode_lib::model::goal::GoalStatus::Active); + assert!( + goal.objective.contains("implementation plan"), + "goal objective should reference the plan; got: {}", + goal.objective + ); +} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index f3fd2940..dc201839 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -138,8 +138,10 @@ const en: Record = { // ── Runtime Thread Surface (Plan Approval) ─────────────── "plan.implementAsPlan": "Implement as planned", "plan.clearAndImplement": "Clear context and implement", + "plan.goalImplement": "Set goal and implement", "plan.approvedClearAndImplement": "Approved: Clear context and implement", "plan.approvedImplement": "Approved: Implement as planned", + "plan.approvedGoalImplement": "Approved: Set goal and implement", "plan.approvedToImplement": "Approved to implement", "plan.superseded": "This plan has been superseded by a newer version.", "plan.awaitingApproval": "Awaiting implementation approval", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 3f5c5164..34dc0636 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -136,9 +136,11 @@ const zhCN = { // ── Runtime Thread Surface (Plan Approval) ─────────────── "plan.implementAsPlan": "按计划实施", - "plan.clearAndImplement": "清理上下文后按计划实施", - "plan.approvedClearAndImplement": "已批准:清理上下文后按计划实施", + "plan.clearAndImplement": "清理上下文后实施", + "plan.goalImplement": "按计划设置Goal后实施", + "plan.approvedClearAndImplement": "已批准:清理上下文后实施", "plan.approvedImplement": "已批准:按计划实施", + "plan.approvedGoalImplement": "已批准:按计划设置Goal后实施", "plan.approvedToImplement": "已批准进入实施", "plan.superseded": "该计划已被新的规划版本替代。", "plan.awaitingApproval": "等待实施审批", diff --git a/src/modules/workbench-shell/ui/runtime-thread-surface-metadata.ts b/src/modules/workbench-shell/ui/runtime-thread-surface-metadata.ts index 41886126..f906cdd1 100644 --- a/src/modules/workbench-shell/ui/runtime-thread-surface-metadata.ts +++ b/src/modules/workbench-shell/ui/runtime-thread-surface-metadata.ts @@ -2,7 +2,7 @@ import type { TranslationKey } from "@/i18n"; -export type PlanApprovalAction = "apply_plan" | "apply_plan_with_context_reset"; +export type PlanApprovalAction = "apply_plan" | "apply_plan_with_context_reset" | "apply_plan_with_goal"; export type PlanStepMetadata = { description?: string; @@ -152,7 +152,7 @@ export function parseApprovalPromptMetadata(value: unknown, t: (key: Translation const action = readStringField(optionRecord, "action"); const label = readStringField(optionRecord, "label"); if ( - (action !== "apply_plan" && action !== "apply_plan_with_context_reset") + (action !== "apply_plan" && action !== "apply_plan_with_context_reset" && action !== "apply_plan_with_goal") || !label ) { return null; @@ -167,6 +167,7 @@ export function parseApprovalPromptMetadata(value: unknown, t: (key: Translation approvedAction: readStringField(record, "approvedAction") === "apply_plan" || readStringField(record, "approvedAction") === "apply_plan_with_context_reset" + || readStringField(record, "approvedAction") === "apply_plan_with_goal" ? (readStringField(record, "approvedAction") as PlanApprovalAction) : null, options: options.length > 0 diff --git a/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts b/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts index dc8a284b..5d679201 100644 --- a/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts +++ b/src/modules/workbench-shell/ui/runtime-thread-surface-state.ts @@ -393,11 +393,15 @@ export function mapRecordedUserMessage(event: RecordedUserMessageEvent): Surface export function formatApprovalPromptState(state: string, approvedAction: PlanApprovalAction | null, t: (key: TranslationKey) => string) { switch (state) { case "approved": - return approvedAction === "apply_plan_with_context_reset" - ? t("plan.approvedClearAndImplement") - : approvedAction === "apply_plan" - ? t("plan.approvedImplement") - : t("plan.approvedToImplement") + if (approvedAction === "apply_plan_with_context_reset") { + return t("plan.approvedClearAndImplement"); + } + if (approvedAction === "apply_plan_with_goal") { + return t("plan.approvedGoalImplement"); + } + return approvedAction === "apply_plan" + ? t("plan.approvedImplement") + : t("plan.approvedToImplement"); case "superseded": return t("plan.superseded"); default: diff --git a/src/modules/workbench-shell/ui/runtime-thread-surface.tsx b/src/modules/workbench-shell/ui/runtime-thread-surface.tsx index b53b4eb9..192d7bd4 100644 --- a/src/modules/workbench-shell/ui/runtime-thread-surface.tsx +++ b/src/modules/workbench-shell/ui/runtime-thread-surface.tsx @@ -2238,7 +2238,7 @@ export function RuntimeThreadSurface({ } preserveContextUsageOnNextEmptySnapshotRef.current = action === "apply_plan"; - if (action === "apply_plan_with_context_reset") { + if (action === "apply_plan_with_context_reset" || action === "apply_plan_with_goal") { threadStore.setState({ runtimeContextUsage: null }); } setApprovingPlanMessageId(messageId); diff --git a/src/services/bridge/agent-commands.ts b/src/services/bridge/agent-commands.ts index 9159bc83..9485629b 100644 --- a/src/services/bridge/agent-commands.ts +++ b/src/services/bridge/agent-commands.ts @@ -606,7 +606,7 @@ export async function threadSubscribeRun( export async function threadExecuteApprovedPlan( threadId: string, approvalMessageId: string, - action: "apply_plan" | "apply_plan_with_context_reset", + action: "apply_plan" | "apply_plan_with_context_reset" | "apply_plan_with_goal", onEvent: (event: ThreadStreamEvent) => void, ): Promise { requireTauri("thread_execute_approved_plan"); diff --git a/src/services/thread-stream/thread-stream.ts b/src/services/thread-stream/thread-stream.ts index 96ae4833..9a717478 100644 --- a/src/services/thread-stream/thread-stream.ts +++ b/src/services/thread-stream/thread-stream.ts @@ -357,7 +357,7 @@ export class ThreadStream { async executeApprovedPlan( threadId: string, approvalMessageId: string, - action: "apply_plan" | "apply_plan_with_context_reset", + action: "apply_plan" | "apply_plan_with_context_reset" | "apply_plan_with_goal", ): Promise { try { const runId = await threadExecuteApprovedPlan(