From 5619cc44661497e9d479e5378f292bc5c4f40003 Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 26 Feb 2026 17:25:43 -0700 Subject: [PATCH 1/2] fix(mark): stabilize remote project chat sessions - route remote project sessions through remote ACP workspace context - disable MCP tool injection for remote project sessions to avoid startup hangs - harden ACP proxy parsing for record-separator framed output and add startup timeout - scope project-page agent selection by project id so changing one project does not affect others - update project chat UI gating for remote workspace readiness --- apps/mark/src-tauri/src/session_commands.rs | 120 ++++++++++++++---- .../lib/features/agents/AgentSelector.svelte | 20 ++- .../features/projects/ProjectSection.svelte | 29 ++++- .../features/settings/preferences.svelte.ts | 35 +++++ crates/acp-client/src/driver.rs | 78 +++++++++--- 5 files changed, 229 insertions(+), 53 deletions(-) diff --git a/apps/mark/src-tauri/src/session_commands.rs b/apps/mark/src-tauri/src/session_commands.rs index af6c9c99..f342f7f4 100644 --- a/apps/mark/src-tauri/src/session_commands.rs +++ b/apps/mark/src-tauri/src/session_commands.rs @@ -55,6 +55,38 @@ fn resolve_branch_repo_slug( project.primary_repo().map(|s| s.to_string()) } +/// Resolve the workspace name to use for a project-level session. +/// +/// Local projects return `None` (use local ACP binaries). +/// Remote projects must have at least one branch with a workspace name so we +/// can route through `blox acp` instead of local clients. +fn resolve_project_session_workspace_name( + store: &Arc, + project: &store::Project, +) -> Result, String> { + if project.location != store::ProjectLocation::Remote { + return Ok(None); + } + + let branches = store + .list_branches_for_project(&project.id) + .map_err(|e| e.to_string())?; + + let workspace_name = branches + .iter() + .find(|b| b.workspace_status == Some(store::WorkspaceStatus::Running)) + .and_then(|b| b.workspace_name.clone()) + .or_else(|| branches.iter().find_map(|b| b.workspace_name.clone())); + + if workspace_name.is_none() { + return Err( + "Remote project has no workspace yet. Add a repository and start its workspace before starting a project session.".to_string(), + ); + } + + Ok(workspace_name) +} + async fn run_blox_blocking(op: F) -> Result where T: Send + 'static, @@ -319,8 +351,9 @@ pub struct ProjectSessionResponse { /// Start a project-level session. /// /// Project sessions operate at the project level rather than a specific branch. -/// The agent receives project context (all repos, existing project notes), -/// and an MCP server with tools to start repo subagent sessions and add repos. +/// The agent receives project context (all repos, existing project notes). +/// Local sessions also receive an MCP server with tools to start repo subagent +/// sessions and add repos. /// Always creates a ProjectNote stub that is populated when the session completes. #[tauri::command(rename_all = "camelCase")] #[allow(clippy::too_many_arguments)] @@ -341,11 +374,26 @@ pub async fn start_project_session( .map_err(|e| e.to_string())? .ok_or_else(|| format!("Project not found: {project_id}"))?; + let workspace_name = resolve_project_session_workspace_name(&store, &project)?; + let is_remote_project_session = workspace_name.is_some(); + // Build project context for the prompt - let project_context = build_project_session_context(&store, &project); + let project_context = + build_project_session_context(&store, &project, workspace_name.as_deref()); // Build the full prompt - let action_instructions = "The user is requesting work at the project level. Investigate and \ + let action_instructions = if is_remote_project_session { + "The user is requesting work at the project level. Investigate and \ + fulfill the request below, then produce a project note summarizing what you found and any \ + actions taken.\n\n\ + This is a remote workspace session. The project MCP tools (`start_repo_session`, \ + `add_project_repo`) are not available in this mode. Work directly with the repository \ + available in the remote workspace and the provided project context.\n\n\ + To return the note, include a horizontal rule (---) followed by the note content. \ + Begin the note with a markdown H1 heading as the title. \n\n\ + " + } else { + "The user is requesting work at the project level. Investigate and \ fulfill the request below, then produce a project note summarizing what you found and any \ actions taken.\n\n\ You have access to the following tools:\n\n\ @@ -361,7 +409,8 @@ pub async fn start_project_session( GitHub organizations. Only add repos from organizations the user already belongs to.\n\n\ To return the note, include a horizontal rule (---) followed by the note content. \ Begin the note with a markdown H1 heading as the title. \n\n\ - "; + " + }; let full_prompt = format!( "\n{action_instructions}\n\nProject information:\n{project_context}\n\n\n{prompt}" @@ -397,11 +446,23 @@ pub async fn start_project_session( agent_session_id: None, pre_head_sha: None, provider, - workspace_name: None, + workspace_name, extra_env: vec![], - mcp_project_id: Some(project_id.clone()), - action_executor: Some(Arc::clone(&action_executor)), - action_registry: Some(Arc::clone(&action_registry)), + mcp_project_id: if is_remote_project_session { + None + } else { + Some(project_id.clone()) + }, + action_executor: if is_remote_project_session { + None + } else { + Some(Arc::clone(&action_executor)) + }, + action_registry: if is_remote_project_session { + None + } else { + Some(Arc::clone(&action_registry)) + }, remote_working_dir: None, }, store, @@ -856,7 +917,11 @@ pub(crate) fn build_project_context( /// /// Includes: project name, all attached repos (with reasons and per-repo /// branch timelines), and existing project notes. -fn build_project_session_context(store: &Arc, project: &store::Project) -> String { +fn build_project_session_context( + store: &Arc, + project: &store::Project, + workspace_name: Option<&str>, +) -> String { let project_name = project.name.trim(); let project_name = if project_name.is_empty() { "Unnamed Project" @@ -912,7 +977,8 @@ fn build_project_session_context(store: &Arc, project: &store::Project) - lines.push(String::new()); lines.push(format!("### Branch: {}", branch.branch_name)); - let timeline = build_branch_timeline_summary(store, branch); + let timeline = + build_branch_timeline_summary(store, branch, branch.workspace_name.as_deref()); if timeline.is_empty() { lines.push("No activity on this branch yet.".to_string()); } else { @@ -933,7 +999,8 @@ fn build_project_session_context(store: &Arc, project: &store::Project) - lines.push(String::new()); lines.push(format!("### Branch: {}", branch.branch_name)); - let timeline = build_branch_timeline_summary(store, branch); + let timeline = + build_branch_timeline_summary(store, branch, branch.workspace_name.as_deref()); if timeline.is_empty() { lines.push("No activity on this branch yet.".to_string()); } else { @@ -949,18 +1016,12 @@ fn build_project_session_context(store: &Arc, project: &store::Project) - lines.push(String::new()); lines.push("## Existing Project Notes".to_string()); for note in &non_empty_notes { - let note_path = std::env::temp_dir().join(format!("mark-note-{}.md", note.id)); - let formatted = match std::fs::write(¬e_path, ¬e.content) { - Ok(()) => format!( - "### Project Note: {}\n\nSee: `{}`", - note.title, - note_path.display() - ), - Err(e) => { - log::warn!("Failed to write project note to temp file, inlining: {e}"); - format!("### Project Note: {}\n\n{}", note.title, note.content) - } - }; + let formatted = format_project_note_for_context( + ¬e.id, + ¬e.title, + ¬e.content, + workspace_name, + ); lines.push(formatted); } } @@ -974,7 +1035,11 @@ fn build_project_session_context(store: &Arc, project: &store::Project) - /// Includes commit log (when a local worktree is available), notes, and /// reviews — but omits project-level notes (those are rendered separately /// at the project level to avoid duplication). -fn build_branch_timeline_summary(store: &Arc, branch: &store::Branch) -> String { +fn build_branch_timeline_summary( + store: &Arc, + branch: &store::Branch, + workspace_name: Option<&str>, +) -> String { let mut timeline: Vec = Vec::new(); let mut commit_error = None; @@ -998,8 +1063,9 @@ fn build_branch_timeline_summary(store: &Arc, branch: &store::Branch) -> } } - // Notes written to local temp files — project sessions run locally - timeline.extend(note_timeline_entries(store, &branch.id, None)); + // Notes are written to temp files in the matching execution environment: + // remote workspace when available, otherwise local temp files. + timeline.extend(note_timeline_entries(store, &branch.id, workspace_name)); timeline.extend(review_timeline_entries(store, &branch.id)); if timeline.is_empty() { diff --git a/apps/mark/src/lib/features/agents/AgentSelector.svelte b/apps/mark/src/lib/features/agents/AgentSelector.svelte index 29b41e5a..2e745321 100644 --- a/apps/mark/src/lib/features/agents/AgentSelector.svelte +++ b/apps/mark/src/lib/features/agents/AgentSelector.svelte @@ -14,20 +14,28 @@ import { onMount, onDestroy } from 'svelte'; import { ChevronDown, Check, Bot } from 'lucide-svelte'; import { agentState, REMOTE_AGENTS } from './agent.svelte'; - import { setAiAgent, getPreferredAgent } from '../settings/preferences.svelte'; + import { + setAiAgent, + getPreferredAgent, + setProjectAiAgent, + getPreferredAgentForProject, + } from '../settings/preferences.svelte'; interface Props { disabled?: boolean; remote?: boolean; + projectId?: string | null; } - let { disabled = false, remote = false }: Props = $props(); + let { disabled = false, remote = false, projectId = null }: Props = $props(); let showDropdown = $state(false); let agents = $derived(remote ? REMOTE_AGENTS : agentState.providers); - let preferredId = $derived(getPreferredAgent(agents)); + let preferredId = $derived( + projectId ? getPreferredAgentForProject(projectId, agents) : getPreferredAgent(agents) + ); let currentLabel = $derived(agents.find((p) => p.id === preferredId)?.label ?? 'Agent'); @@ -47,7 +55,11 @@ } function select(id: string) { - setAiAgent(id); + if (projectId) { + setProjectAiAgent(projectId, id); + } else { + setAiAgent(id); + } showDropdown = false; } diff --git a/apps/mark/src/lib/features/projects/ProjectSection.svelte b/apps/mark/src/lib/features/projects/ProjectSection.svelte index 92d66cb1..68b8c39b 100644 --- a/apps/mark/src/lib/features/projects/ProjectSection.svelte +++ b/apps/mark/src/lib/features/projects/ProjectSection.svelte @@ -32,7 +32,7 @@ import SessionModal from '../sessions/SessionModal.svelte'; import AgentSelector from '../agents/AgentSelector.svelte'; import { agentState, REMOTE_AGENTS } from '../agents/agent.svelte'; - import { getPreferredAgent } from '../settings/preferences.svelte'; + import { getPreferredAgentForProject } from '../settings/preferences.svelte'; interface Props { project: Project; @@ -163,8 +163,23 @@ let availableAgents = $derived( project.location === 'remote' ? REMOTE_AGENTS : agentState.providers ); - let preferredProvider = $derived(getPreferredAgent(availableAgents) ?? undefined); - let canSubmitPrompt = $derived(!!promptText.trim() && !!preferredProvider); + let preferredProvider = $derived( + getPreferredAgentForProject(project.id, availableAgents) ?? undefined + ); + let remoteWorkspaceReady = $derived( + project.location !== 'remote' || + branches.some((b) => b.workspaceStatus === 'running' && !!b.workspaceName) + ); + let canSubmitPrompt = $derived( + !!promptText.trim() && !!preferredProvider && remoteWorkspaceReady + ); + let sendButtonTitle = $derived( + !preferredProvider + ? 'No AI agent available' + : !remoteWorkspaceReady + ? 'Start the remote workspace before starting a project session' + : 'Start project session' + ); /** Session IDs for running project sessions (all produce notes). */ let activeSessionIds = $state>(new Set()); @@ -374,7 +389,9 @@
- + diff --git a/apps/mark/src/lib/features/settings/preferences.svelte.ts b/apps/mark/src/lib/features/settings/preferences.svelte.ts index 00d68d8e..5a727937 100644 --- a/apps/mark/src/lib/features/settings/preferences.svelte.ts +++ b/apps/mark/src/lib/features/settings/preferences.svelte.ts @@ -34,6 +34,7 @@ const SIZE_DEFAULT = 13; const SIZE_STORE_KEY = 'size-base'; const SYNTAX_THEME_STORE_KEY = 'syntax-theme'; const RECENT_AGENTS_STORE_KEY = 'recent-agents'; +const PROJECT_AGENTS_STORE_KEY = 'project-agents'; /** Maximum number of recent agents to remember. */ const RECENT_AGENTS_MAX = 10; @@ -65,6 +66,11 @@ export const preferences = $state({ * Used to pick the best available agent for a given context (local vs remote). */ recentAgents: [] as string[], + /** + * Per-project preferred AI agent (projectId -> agentId). + * Used by project-level chat so changing one project does not affect others. + */ + projectAgents: {} as Record, /** Whether all preferences have been loaded from storage */ loaded: false, }); @@ -132,6 +138,12 @@ export async function initPreferences(): Promise { } } + // Load per-project agent preferences + const savedProjectAgents = await getStoreValue>(PROJECT_AGENTS_STORE_KEY); + if (savedProjectAgents && typeof savedProjectAgents === 'object') { + preferences.projectAgents = savedProjectAgents; + } + preferences.loaded = true; } @@ -223,3 +235,26 @@ export function getPreferredAgent(available: { id: string }[]): string | null { } return available[0]?.id ?? null; } + +/** + * Set preferred agent for a specific project. + */ +export function setProjectAiAgent(projectId: string, agentId: string): void { + if (!projectId) return; + preferences.projectAgents = { ...preferences.projectAgents, [projectId]: agentId }; + setStoreValue(PROJECT_AGENTS_STORE_KEY, preferences.projectAgents); +} + +/** + * Return the preferred agent for a project, falling back to global preference. + */ +export function getPreferredAgentForProject( + projectId: string, + available: { id: string }[] +): string | null { + const scoped = preferences.projectAgents[projectId]; + if (scoped && available.some((a) => a.id === scoped)) { + return scoped; + } + return getPreferredAgent(available); +} diff --git a/crates/acp-client/src/driver.rs b/crates/acp-client/src/driver.rs index 33f4a9a3..fc8c09de 100644 --- a/crates/acp-client/src/driver.rs +++ b/crates/acp-client/src/driver.rs @@ -11,7 +11,7 @@ use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use agent_client_protocol::{ Agent, ClientSideConnection, ContentBlock as AcpContentBlock, Implementation, @@ -109,6 +109,7 @@ pub struct AcpDriver { } const REMOTE_ACP_MAX_PENDING_LINE_BYTES: usize = 256 * 1024; +const ACP_PROTOCOL_START_TIMEOUT: Duration = Duration::from_secs(90); impl AcpDriver { /// Create a driver for the given provider ID (e.g. "goose", "claude"). @@ -296,10 +297,21 @@ impl AgentDriver for AcpDriver { writer.finalize().await; return Ok(()); } - result = run_acp_protocol( - &connection, &acp_working_dir, prompt, store, - session_id, agent_session_id, &handler, &self.mcp_servers, - ) => result, + result = tokio::time::timeout( + ACP_PROTOCOL_START_TIMEOUT, + run_acp_protocol( + &connection, &acp_working_dir, prompt, store, + session_id, agent_session_id, &handler, &self.mcp_servers, + ) + ) => { + match result { + Ok(protocol_result) => protocol_result, + Err(_) => Err(format!( + "Timed out waiting for ACP protocol startup after {}s", + ACP_PROTOCOL_START_TIMEOUT.as_secs() + )), + } + }, }; writer.finalize().await; @@ -406,6 +418,16 @@ fn consume_remote_acp_line(pending: &mut String, raw_line: &str) -> RemoteLineOu } } +fn remote_acp_segments(decoded_line: &str) -> impl Iterator { + // `sq blox acp` can emit JSON Text Sequences where records are delimited by + // U+001E (record separator). Keep line-based handling for normal JSON-RPC + // output, but split RS-delimited frames so concatenated messages are not + // treated as malformed JSON. + decoded_line + .split('\u{1e}') + .filter(|segment| !segment.trim().is_empty()) +} + async fn normalize_remote_acp_stdout( stdout: R, mut writer: tokio::io::DuplexStream, @@ -426,15 +448,17 @@ async fn normalize_remote_acp_stdout( log::warn!("Dropped invalid UTF-8 bytes from remote ACP stdout"); } - match consume_remote_acp_line(&mut pending, &decoded_line) { - RemoteLineOutcome::Emit(line) => { - writer.write_all(line.as_bytes()).await?; - writer.write_all(b"\n").await?; - } - RemoteLineOutcome::Pending => {} - RemoteLineOutcome::Dropped => { - if !decoded_line.trim().is_empty() { - log::warn!("Dropped malformed ACP proxy output line"); + for segment in remote_acp_segments(&decoded_line) { + match consume_remote_acp_line(&mut pending, segment) { + RemoteLineOutcome::Emit(line) => { + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + } + RemoteLineOutcome::Pending => {} + RemoteLineOutcome::Dropped => { + if !segment.trim().is_empty() { + log::warn!("Dropped malformed ACP proxy output line"); + } } } } @@ -713,8 +737,8 @@ impl MessageWriter for BasicMessageWriter { #[cfg(test)] mod tests { use super::{ - consume_remote_acp_line, decode_remote_acp_line, resolve_spawn_working_dir, - sanitize_remote_acp_chunk, RemoteLineOutcome, + consume_remote_acp_line, decode_remote_acp_line, remote_acp_segments, + resolve_spawn_working_dir, sanitize_remote_acp_chunk, RemoteLineOutcome, }; use std::time::{SystemTime, UNIX_EPOCH}; @@ -761,6 +785,28 @@ mod tests { ); } + #[test] + fn splits_record_separator_delimited_messages_in_one_stdout_line() { + let mut pending = String::new(); + let line = "\u{1e}{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}\u{1e}{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":null}\n"; + + let outcomes: Vec = remote_acp_segments(line) + .map(|segment| consume_remote_acp_line(&mut pending, segment)) + .collect(); + + assert_eq!( + outcomes, + vec![ + RemoteLineOutcome::Emit( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}".to_string() + ), + RemoteLineOutcome::Emit( + "{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":null}".to_string() + ), + ] + ); + } + #[test] fn remote_utf8_decoder_strips_invalid_bytes() { let (decoded, had_invalid_utf8) = From 7e908e7ae47cf8d95e8faa953cc99dbe850c99b7 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 27 Feb 2026 11:27:41 -0700 Subject: [PATCH 2/2] fix(mark): restore project MCP orchestration for remote projects - run project sessions locally with MCP for both local and remote projects - clarify coordinator vs repo-subagent responsibilities in project prompts - enforce MCP transport capability checks during ACP setup - scope ACP startup timeout to setup phase only (not full prompt run) - keep remote ACP proxy framing robustness for record-separator output --- apps/mark/src-tauri/src/session_commands.rs | 91 +++++++------------ .../features/projects/ProjectSection.svelte | 26 ++---- crates/acp-client/src/driver.rs | 66 +++++++++----- 3 files changed, 79 insertions(+), 104 deletions(-) diff --git a/apps/mark/src-tauri/src/session_commands.rs b/apps/mark/src-tauri/src/session_commands.rs index f342f7f4..d70020be 100644 --- a/apps/mark/src-tauri/src/session_commands.rs +++ b/apps/mark/src-tauri/src/session_commands.rs @@ -55,38 +55,6 @@ fn resolve_branch_repo_slug( project.primary_repo().map(|s| s.to_string()) } -/// Resolve the workspace name to use for a project-level session. -/// -/// Local projects return `None` (use local ACP binaries). -/// Remote projects must have at least one branch with a workspace name so we -/// can route through `blox acp` instead of local clients. -fn resolve_project_session_workspace_name( - store: &Arc, - project: &store::Project, -) -> Result, String> { - if project.location != store::ProjectLocation::Remote { - return Ok(None); - } - - let branches = store - .list_branches_for_project(&project.id) - .map_err(|e| e.to_string())?; - - let workspace_name = branches - .iter() - .find(|b| b.workspace_status == Some(store::WorkspaceStatus::Running)) - .and_then(|b| b.workspace_name.clone()) - .or_else(|| branches.iter().find_map(|b| b.workspace_name.clone())); - - if workspace_name.is_none() { - return Err( - "Remote project has no workspace yet. Add a repository and start its workspace before starting a project session.".to_string(), - ); - } - - Ok(workspace_name) -} - async fn run_blox_blocking(op: F) -> Result where T: Send + 'static, @@ -352,8 +320,8 @@ pub struct ProjectSessionResponse { /// /// Project sessions operate at the project level rather than a specific branch. /// The agent receives project context (all repos, existing project notes). -/// Local sessions also receive an MCP server with tools to start repo subagent -/// sessions and add repos. +/// Sessions receive an MCP server with tools to start repo subagent sessions +/// and add repos. /// Always creates a ProjectNote stub that is populated when the session completes. #[tauri::command(rename_all = "camelCase")] #[allow(clippy::too_many_arguments)] @@ -374,21 +342,31 @@ pub async fn start_project_session( .map_err(|e| e.to_string())? .ok_or_else(|| format!("Project not found: {project_id}"))?; - let workspace_name = resolve_project_session_workspace_name(&store, &project)?; - let is_remote_project_session = workspace_name.is_some(); - // Build project context for the prompt - let project_context = - build_project_session_context(&store, &project, workspace_name.as_deref()); + let project_context = build_project_session_context(&store, &project, None); // Build the full prompt - let action_instructions = if is_remote_project_session { + let action_instructions = if project.location == store::ProjectLocation::Remote { "The user is requesting work at the project level. Investigate and \ fulfill the request below, then produce a project note summarizing what you found and any \ actions taken.\n\n\ - This is a remote workspace session. The project MCP tools (`start_repo_session`, \ - `add_project_repo`) are not available in this mode. Work directly with the repository \ - available in the remote workspace and the provided project context.\n\n\ + This top-level project session runs locally and acts as a coordinator. \ + For repository-specific execution, use MCP subagent tools.\n\n\ + This is a remote-workspace project. Use the project MCP tools to orchestrate work:\n\n\ + - start_repo_session: Use this to make changes or run tasks in one of the project's \ + repositories. Use `expected_outcome=\"note_in_repo\"` for repo notes and \ + `expected_outcome=\"commit\"` for code changes/commits. For remote branches this subagent \ + runs on the remote workspace, where file access, notes, and commits must happen.\n\n\ + - add_project_repo: Use this when the task requires a repository that isn't yet in the \ + project. Pass the GitHub repo slug to add it.\n\n\ + IMPORTANT: `add_project_repo` and `start_repo_session` are MCP tools, not shell commands. \ + Do not run `which`/`type` for these names and do not ask the user to add repos manually \ + unless the MCP tool call itself returns an error. If the tool call fails, report the exact \ + error and the next action needed.\n\n\ + Keep this project session focused on coordination and synthesis. Do not perform \ + repository edits directly here; use `start_repo_session` for implementation work.\n\n\ + To discover repositories that might be relevant, use `gh` to explore repos in the user's \ + GitHub organizations. Only add repos from organizations the user already belongs to.\n\n\ To return the note, include a horizontal rule (---) followed by the note content. \ Begin the note with a markdown H1 heading as the title. \n\n\ " @@ -400,11 +378,16 @@ pub async fn start_project_session( - start_repo_session: Use this to make changes or run tasks within one of the project's \ repositories. Pass the repo slug (e.g. \"org/repo\") and clear instructions for what to \ do there. This tool starts a subagent session and waits for it to complete before \ - returning the outcome. Do not ask for both a note and a commit in a single start_repo_session \ + returning the outcome. Use `expected_outcome=\"note_in_repo\"` for repo notes and \ + `expected_outcome=\"commit\"` for code changes/commits. Do not ask for both a note and a commit in a single start_repo_session \ request — choose one outcome per call. All reasoning specific to a repo should be done within \ a repo session rather in this project wide context.\n\n\ - add_project_repo: Use this when the task requires a repository that isn't yet in the \ project. Pass the GitHub repo slug to add it.\n\n\ + IMPORTANT: `add_project_repo` and `start_repo_session` are MCP tools, not shell commands. \ + Do not run `which`/`type` for these names and do not ask the user to add repos manually \ + unless the MCP tool call itself returns an error. If the tool call fails, report the exact \ + error and the next action needed.\n\n\ To discover repositories that might be relevant, use `gh` to explore repos in the user's \ GitHub organizations. Only add repos from organizations the user already belongs to.\n\n\ To return the note, include a horizontal rule (---) followed by the note content. \ @@ -446,23 +429,11 @@ pub async fn start_project_session( agent_session_id: None, pre_head_sha: None, provider, - workspace_name, + workspace_name: None, extra_env: vec![], - mcp_project_id: if is_remote_project_session { - None - } else { - Some(project_id.clone()) - }, - action_executor: if is_remote_project_session { - None - } else { - Some(Arc::clone(&action_executor)) - }, - action_registry: if is_remote_project_session { - None - } else { - Some(Arc::clone(&action_registry)) - }, + mcp_project_id: Some(project_id.clone()), + action_executor: Some(Arc::clone(&action_executor)), + action_registry: Some(Arc::clone(&action_registry)), remote_working_dir: None, }, store, diff --git a/apps/mark/src/lib/features/projects/ProjectSection.svelte b/apps/mark/src/lib/features/projects/ProjectSection.svelte index 68b8c39b..c068b175 100644 --- a/apps/mark/src/lib/features/projects/ProjectSection.svelte +++ b/apps/mark/src/lib/features/projects/ProjectSection.svelte @@ -31,7 +31,7 @@ import NoteModal from '../notes/NoteModal.svelte'; import SessionModal from '../sessions/SessionModal.svelte'; import AgentSelector from '../agents/AgentSelector.svelte'; - import { agentState, REMOTE_AGENTS } from '../agents/agent.svelte'; + import { agentState } from '../agents/agent.svelte'; import { getPreferredAgentForProject } from '../settings/preferences.svelte'; interface Props { @@ -160,25 +160,13 @@ // ── Project session input ────────────────────────────────────────────── let promptText = $state(''); let promptTextarea = $state(null); - let availableAgents = $derived( - project.location === 'remote' ? REMOTE_AGENTS : agentState.providers - ); + let availableAgents = $derived(agentState.providers); let preferredProvider = $derived( getPreferredAgentForProject(project.id, availableAgents) ?? undefined ); - let remoteWorkspaceReady = $derived( - project.location !== 'remote' || - branches.some((b) => b.workspaceStatus === 'running' && !!b.workspaceName) - ); - let canSubmitPrompt = $derived( - !!promptText.trim() && !!preferredProvider && remoteWorkspaceReady - ); + let canSubmitPrompt = $derived(!!promptText.trim() && !!preferredProvider); let sendButtonTitle = $derived( - !preferredProvider - ? 'No AI agent available' - : !remoteWorkspaceReady - ? 'Start the remote workspace before starting a project session' - : 'Start project session' + preferredProvider ? 'Start project session' : 'No AI agent available' ); /** Session IDs for running project sessions (all produce notes). */ let activeSessionIds = $state>(new Set()); @@ -389,9 +377,7 @@
- +