feat(v3): implement Mission Control MVP (Phases 1-5)
Phase 1: Data model - groups.rs (Rust structs + load/save groups.json), groups.ts (TypeScript interfaces), groups-bridge.ts (IPC adapter), workspace.svelte.ts (replaces layout store), SQLite migrations (agent_messages, project_agent_state tables, project_id column), --group CLI argument. Phase 2: Project shell layout - GlobalTabBar, ProjectGrid, ProjectBox, ProjectHeader, CommandPalette, DocsTab, ContextTab, SettingsTab, App.svelte full rewrite (no sidebar/TilingGrid). Phase 3: ClaudeSession.svelte wrapping AgentPane per-project. Phase 4: TerminalTabs.svelte with shell/SSH/agent tab types. Phase 5: TeamAgentsPanel + AgentCard for compact subagent view. Also fixes AgentPane Svelte 5 event modifier (on:click -> onclick).
This commit is contained in:
parent
293bed6dc5
commit
ab79dac4b3
20 changed files with 2296 additions and 65 deletions
|
|
@ -116,6 +116,47 @@ impl SessionDb {
|
|||
.map_err(|e| format!("Migration (group_name) failed: {e}"))?;
|
||||
}
|
||||
|
||||
// v3 migration: project_id column on sessions
|
||||
let has_project_id: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('sessions') WHERE name='project_id'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
).unwrap_or(0);
|
||||
if has_project_id == 0 {
|
||||
conn.execute("ALTER TABLE sessions ADD COLUMN project_id TEXT DEFAULT ''", [])
|
||||
.map_err(|e| format!("Migration (project_id) failed: {e}"))?;
|
||||
}
|
||||
|
||||
// v3: agent message history for session continuity
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS agent_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
message_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_messages_session
|
||||
ON agent_messages(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_messages_project
|
||||
ON agent_messages(project_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_agent_state (
|
||||
project_id TEXT PRIMARY KEY,
|
||||
last_session_id TEXT NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
cost_usd REAL DEFAULT 0,
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
last_prompt TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);"
|
||||
).map_err(|e| format!("Migration (v3 tables) failed: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +381,142 @@ impl SessionDb {
|
|||
).map_err(|e| format!("SSH update failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- v3: Agent message persistence ---
|
||||
|
||||
pub fn save_agent_messages(
|
||||
&self,
|
||||
session_id: &str,
|
||||
project_id: &str,
|
||||
sdk_session_id: Option<&str>,
|
||||
messages: &[AgentMessageRecord],
|
||||
) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
// Clear previous messages for this session
|
||||
conn.execute(
|
||||
"DELETE FROM agent_messages WHERE session_id = ?1",
|
||||
params![session_id],
|
||||
).map_err(|e| format!("Delete old messages failed: {e}"))?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT INTO agent_messages (session_id, project_id, sdk_session_id, message_type, content, parent_id, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
||||
).map_err(|e| format!("Prepare insert failed: {e}"))?;
|
||||
|
||||
for msg in messages {
|
||||
stmt.execute(params![
|
||||
session_id,
|
||||
project_id,
|
||||
sdk_session_id,
|
||||
msg.message_type,
|
||||
msg.content,
|
||||
msg.parent_id,
|
||||
msg.created_at,
|
||||
]).map_err(|e| format!("Insert message failed: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_agent_messages(&self, project_id: &str) -> Result<Vec<AgentMessageRecord>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
// Load messages from the most recent session for this project
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, session_id, project_id, sdk_session_id, message_type, content, parent_id, created_at
|
||||
FROM agent_messages
|
||||
WHERE project_id = ?1
|
||||
ORDER BY created_at ASC"
|
||||
).map_err(|e| format!("Query prepare failed: {e}"))?;
|
||||
|
||||
let messages = stmt.query_map(params![project_id], |row| {
|
||||
Ok(AgentMessageRecord {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
project_id: row.get(2)?,
|
||||
sdk_session_id: row.get(3)?,
|
||||
message_type: row.get(4)?,
|
||||
content: row.get(5)?,
|
||||
parent_id: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
}).map_err(|e| format!("Query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row read failed: {e}"))?;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
// --- v3: Project agent state ---
|
||||
|
||||
pub fn save_project_agent_state(&self, state: &ProjectAgentState) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO project_agent_state (project_id, last_session_id, sdk_session_id, status, cost_usd, input_tokens, output_tokens, last_prompt, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
state.project_id,
|
||||
state.last_session_id,
|
||||
state.sdk_session_id,
|
||||
state.status,
|
||||
state.cost_usd,
|
||||
state.input_tokens,
|
||||
state.output_tokens,
|
||||
state.last_prompt,
|
||||
state.updated_at,
|
||||
],
|
||||
).map_err(|e| format!("Save project agent state failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_project_agent_state(&self, project_id: &str) -> Result<Option<ProjectAgentState>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT project_id, last_session_id, sdk_session_id, status, cost_usd, input_tokens, output_tokens, last_prompt, updated_at FROM project_agent_state WHERE project_id = ?1"
|
||||
).map_err(|e| format!("Query prepare failed: {e}"))?;
|
||||
|
||||
let result = stmt.query_row(params![project_id], |row| {
|
||||
Ok(ProjectAgentState {
|
||||
project_id: row.get(0)?,
|
||||
last_session_id: row.get(1)?,
|
||||
sdk_session_id: row.get(2)?,
|
||||
status: row.get(3)?,
|
||||
cost_usd: row.get(4)?,
|
||||
input_tokens: row.get(5)?,
|
||||
output_tokens: row.get(6)?,
|
||||
last_prompt: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(state) => Ok(Some(state)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(format!("Load project agent state failed: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentMessageRecord {
|
||||
#[serde(default)]
|
||||
pub id: i64,
|
||||
pub session_id: String,
|
||||
pub project_id: String,
|
||||
pub sdk_session_id: Option<String>,
|
||||
pub message_type: String,
|
||||
pub content: String,
|
||||
pub parent_id: Option<String>,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectAgentState {
|
||||
pub project_id: String,
|
||||
pub last_session_id: String,
|
||||
pub sdk_session_id: Option<String>,
|
||||
pub status: String,
|
||||
pub cost_usd: f64,
|
||||
pub input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
pub last_prompt: Option<String>,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue