feat: add multi-project support (#110)
* feat: Implement project management store with project path validation and synchronization - Added `useProjectsStore` for managing projects, including adding, removing, renaming, and validating project paths. - Implemented persistence for projects and active project ID using safe storage. - Introduced synchronization from desktop settings to keep project data consistent. - Enhanced session store to manage sessions by directory and added new methods for session management. - Updated todo store to fetch session todos based on the directory context. - Refactored server code to validate and resolve project directories for various API endpoints. - Added project entry validation and sanitization to ensure data integrity. * feat(settings): migrate legacy project settings and update settings loading logic * feat: enhance project management with directory-aware settings and improved agent/command source handling * feat: enhance session and project management with directory-aware settings and improved configuration refresh logic * feat: enhance project management with worktree manager integration and project directory resolution * feat: enhance agent groups store with project directory resolution and loading logic * feat: add heartbeat management and global wrapping for SSE blocks in agent and chat providers * feat: refactor command and project handling in useCommandsStore - Replaced useDirectoryStore with useProjectsStore to manage project paths. - Introduced getRequestDirectory function to determine the active project directory. - Updated command fetching to respect project-level scoping. - Enhanced error handling and logging for command configuration fetching. - Improved command configuration saving and updating to utilize project directory context. feat: enhance project path normalization in useProjectsStore - Added resolveTildePath function to expand paths starting with ~. - Updated normalizeProjectPath to utilize home directory for path expansion. fix: update permission handling in useSessionStore - Changed Permission type to PermissionRequest for clarity. - Updated respondToPermission method to use requestId instead of permissionId. refactor: improve permission utilities - Introduced types for PermissionAction and PermissionRule. - Enhanced getAgentDefinition and resolveConfigStore functions for better type safety. - Added resolvePermissionAction to streamline permission resolution logic. feat: add agent configuration retrieval endpoint - Implemented new API endpoint to fetch agent configuration based on project directory. - Enhanced getAgentPermissionSource to prioritize project-level permissions. chore: update SDK version in package.json files - Bumped @opencode-ai/sdk version to ^1.1.1 across all relevant package.json files. refactor: streamline bridge message handling - Updated handleBridgeMessage to accept directory parameter for agent and command requests. - Improved local API request handling to extract directory from query parameters and headers. feat: enhance project configuration management - Added functions to retrieve and merge project configuration paths. - Improved handling of existing project configuration files for agents and commands. * feat: enhance VSCode integration and session management - Added support for a sticky sidebar header background in light and dark themes. - Introduced functions to read VSCode workspace directory and check if running in VSCode. - Implemented detailed logging for session loading and creation processes. - Enhanced session filtering based on directory structure and canonical paths. - Added a new method to reorder projects and prevent modifications in VSCode workspace. - Improved error handling and logging for app initialization and markdown file parsing. - Updated API checks and health checks to ensure readiness before proceeding. - Refactored code for better readability and maintainability across various modules. * feat: improve agent and branch selection logic, enhance session management, and update multi-run creation response * feat: add worktree management actions in agent group detail and sidebar, including delete and keep only options * fix(ui): share IME guard and cover multi-run * fix(session): reduce maximum visible sessions in group from 7 to 5
This commit is contained in:
committed by
GitHub
parent
8aa379e313
commit
18c5b4c7b5
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
use std::{collections::HashSet, path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures_util::TryStreamExt;
|
||||
@@ -11,6 +11,7 @@ use tauri_plugin_notification::NotificationExt;
|
||||
use tokio::{io::AsyncBufReadExt, sync::Mutex};
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
use crate::path_utils::expand_tilde_path;
|
||||
use crate::DesktopRuntime;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -21,6 +22,14 @@ struct EventEnvelope {
|
||||
properties: Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MultiplexedEventEnvelope {
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
directory: Option<String>,
|
||||
payload: EventEnvelope,
|
||||
}
|
||||
|
||||
pub fn spawn_assistant_notifications(
|
||||
app: AppHandle,
|
||||
runtime: DesktopRuntime,
|
||||
@@ -71,41 +80,8 @@ async fn run_once(
|
||||
};
|
||||
|
||||
let prefix = opencode.api_prefix();
|
||||
let mut url = format!("http://127.0.0.1:{port}{}/event", prefix);
|
||||
|
||||
if let Some(dir) = opencode
|
||||
.get_working_directory()
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
{
|
||||
let mut parsed = reqwest::Url::parse(&url)?;
|
||||
parsed.query_pairs_mut().append_pair("directory", &dir);
|
||||
url = parsed.to_string();
|
||||
}
|
||||
|
||||
debug!("[desktop:notify] Connecting SSE for notifications: {url}");
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("accept", "text/event-stream")
|
||||
.header("accept-encoding", "identity")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
"[desktop:notify] SSE response status={} headers={:?}",
|
||||
response.status(),
|
||||
response.headers()
|
||||
);
|
||||
|
||||
if !response.status().is_success() {
|
||||
warn!(
|
||||
"[desktop:notify] SSE connect failed with status {}",
|
||||
response.status()
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
return Ok(());
|
||||
}
|
||||
let base = format!("http://127.0.0.1:{port}{prefix}");
|
||||
let response = connect_notifications_sse(runtime, client, &base).await?;
|
||||
|
||||
let stream = response
|
||||
.bytes_stream()
|
||||
@@ -142,7 +118,7 @@ async fn run_once(
|
||||
let raw = data_lines.join("\n");
|
||||
data_lines.clear();
|
||||
|
||||
match serde_json::from_str::<EventEnvelope>(&raw) {
|
||||
match parse_event_envelope(&raw) {
|
||||
Ok(event) => handle_event(app, event, notified_messages).await,
|
||||
Err(err) => {
|
||||
warn!("[desktop:notify] Failed to parse SSE data: {err}; raw={raw}");
|
||||
@@ -159,6 +135,111 @@ async fn run_once(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_event_envelope(raw: &str) -> Result<EventEnvelope> {
|
||||
if let Ok(event) = serde_json::from_str::<EventEnvelope>(raw) {
|
||||
return Ok(event);
|
||||
}
|
||||
|
||||
let multiplexed = serde_json::from_str::<MultiplexedEventEnvelope>(raw)?;
|
||||
Ok(multiplexed.payload)
|
||||
}
|
||||
|
||||
async fn resolve_project_directory_from_settings(runtime: &DesktopRuntime) -> Option<PathBuf> {
|
||||
let settings = runtime.settings().load().await.ok()?;
|
||||
|
||||
if let Some(active_id) = settings.get("activeProjectId").and_then(Value::as_str) {
|
||||
if let Some(projects) = settings.get("projects").and_then(Value::as_array) {
|
||||
if let Some(path) = projects.iter().find_map(|entry| {
|
||||
let id = entry.get("id").and_then(Value::as_str)?;
|
||||
if id != active_id {
|
||||
return None;
|
||||
}
|
||||
entry.get("path").and_then(Value::as_str)
|
||||
}) {
|
||||
return Some(expand_tilde_path(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings
|
||||
.get("lastDirectory")
|
||||
.and_then(Value::as_str)
|
||||
.map(expand_tilde_path)
|
||||
}
|
||||
|
||||
async fn connect_notifications_sse(
|
||||
runtime: &DesktopRuntime,
|
||||
client: &Client,
|
||||
base: &str,
|
||||
) -> Result<reqwest::Response> {
|
||||
let global_url = format!("{base}/global/event");
|
||||
match try_connect_sse(client, &global_url, "[desktop:notify]").await {
|
||||
Ok(response) => {
|
||||
debug!("[desktop:notify] Using SSE endpoint: {global_url}");
|
||||
return Ok(response);
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"[desktop:notify] SSE endpoint unavailable: {global_url} ({err:?}); falling back"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let event_url = format!("{base}/event");
|
||||
match try_connect_sse(client, &event_url, "[desktop:notify]").await {
|
||||
Ok(response) => {
|
||||
debug!("[desktop:notify] Using SSE endpoint: {event_url}");
|
||||
return Ok(response);
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"[desktop:notify] SSE endpoint unavailable: {event_url} ({err:?}); falling back"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(working_dir) = resolve_project_directory_from_settings(runtime).await else {
|
||||
anyhow::bail!("No project directory available for SSE fallback");
|
||||
};
|
||||
let directory = working_dir.to_string_lossy().to_string();
|
||||
let mut parsed = reqwest::Url::parse(&event_url)?;
|
||||
parsed
|
||||
.query_pairs_mut()
|
||||
.append_pair("directory", &directory);
|
||||
let directory_url = parsed.to_string();
|
||||
|
||||
let response = try_connect_sse(client, &directory_url, "[desktop:notify]").await?;
|
||||
debug!("[desktop:notify] Using directory-scoped SSE endpoint: {directory_url}");
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn try_connect_sse(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
log_prefix: &str,
|
||||
) -> Result<reqwest::Response> {
|
||||
debug!("{log_prefix} Connecting SSE: {url}");
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.header("accept", "text/event-stream")
|
||||
.header("accept-encoding", "identity")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
"{log_prefix} SSE response status={} headers={:?}",
|
||||
response.status(),
|
||||
response.headers()
|
||||
);
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("SSE connect failed with status {}", response.status());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_event(
|
||||
app: &AppHandle,
|
||||
event: EventEnvelope,
|
||||
|
||||
Reference in New Issue
Block a user