Skip to content

Commit d110862

Browse files
feat: conductor polish, semantic indexing, web app provider
Phase 5: Conductor Polish - Enforce agent concurrency limits (Rust: count_running_agents vs preference) - Live agent detection on kanban board (pulsing green badge) - Per-issue workspace isolation (workspace_id FK on agent_tasks) - Multi-turn agent sessions (--resume session_id in Claude Code adapter) - Symphony-style orchestrator (plan→code→review pipeline with retry) Phase 6: Semantic Indexing - Regex-based symbol extraction (TS/JS/Python/Rust/Go) - Jaccard similarity duplicate detection - analyzeDiffForDuplicates() findings for review-core Phase 7: Web App - DataProvider abstraction (TauriProvider + HttpProvider) - Auto-detect environment (Tauri vs browser) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e85794e commit d110862

12 files changed

Lines changed: 728 additions & 22 deletions

File tree

apps/desktop/src-tauri/src/adapters/claude_code.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ impl AgentAdapter for ClaudeCodeAdapter {
4646
project_path: PathBuf,
4747
role: Option<String>,
4848
task: Option<String>,
49+
resume_session_id: Option<String>,
4950
) -> Result<AgentHandle, String> {
5051
let cli_path =
5152
Self::detect_cli().ok_or("Claude Code CLI not found. Install it first.")?;
@@ -58,6 +59,12 @@ impl AgentAdapter for ClaudeCodeAdapter {
5859
});
5960

6061
let mut cmd = Command::new(&cli_path);
62+
63+
if let Some(ref session_id) = resume_session_id {
64+
// Resume a previous session with a new prompt
65+
cmd.arg("--resume").arg(session_id);
66+
}
67+
6168
cmd.arg("-p")
6269
.arg(&task_prompt)
6370
.arg("--output-format")

apps/desktop/src-tauri/src/adapters/codex.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ impl AgentAdapter for CodexAdapter {
4343
project_path: PathBuf,
4444
role: Option<String>,
4545
task: Option<String>,
46+
_resume_session_id: Option<String>,
4647
) -> Result<AgentHandle, String> {
4748
let cli_path =
4849
Self::detect_cli().ok_or("Codex CLI not found. Install it first.")?;

apps/desktop/src-tauri/src/adapters/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ pub trait AgentAdapter: Send + Sync {
2828

2929
/// Spawn an agent process in the given project directory, optionally
3030
/// scoped to a role and initial task prompt.
31+
/// If `resume_session_id` is provided, continue a previous session.
3132
async fn launch(
3233
&self,
3334
project_path: PathBuf,
3435
role: Option<String>,
3536
task: Option<String>,
37+
resume_session_id: Option<String>,
3638
) -> Result<AgentHandle, String>;
3739

3840
/// Gracefully stop a running agent by its ID / PID.

apps/desktop/src-tauri/src/commands/agents.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,34 @@ pub async fn launch_agent(
2626
role: Option<String>,
2727
task: Option<String>,
2828
review_id: Option<String>,
29+
resume_session_id: Option<String>,
2930
) -> Result<Value, String> {
31+
// ── Concurrency check ─────────────────────────────────────────────────
32+
{
33+
let conn = db.0.lock().map_err(|e| e.to_string())?;
34+
let running = queries::count_running_agents(&conn).unwrap_or(0);
35+
let max = queries::get_preference(&conn, "max_concurrent_agents")
36+
.ok()
37+
.flatten()
38+
.and_then(|v| v.parse::<i64>().ok())
39+
.unwrap_or(3);
40+
if running >= max {
41+
return Err(format!(
42+
"Concurrency limit reached ({running}/{max} agents running). \
43+
Wait for an agent to finish or increase the limit in Settings."
44+
));
45+
}
46+
}
47+
3048
let handle = match adapter.as_str() {
3149
"claude-code" => {
3250
let a = ClaudeCodeAdapter::new();
33-
a.launch(PathBuf::from(&project_path), role.clone(), task.clone())
51+
a.launch(PathBuf::from(&project_path), role.clone(), task.clone(), resume_session_id.clone())
3452
.await?
3553
}
3654
"codex" => {
3755
let a = CodexAdapter::new();
38-
a.launch(PathBuf::from(&project_path), role.clone(), task.clone())
56+
a.launch(PathBuf::from(&project_path), role.clone(), task.clone(), resume_session_id.clone())
3957
.await?
4058
}
4159
other => return Err(format!("Unknown adapter: {other}")),

apps/desktop/src-tauri/src/commands/linear.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ pub async fn import_linear_issues(
635635
description: Some(task_description),
636636
acceptance_criteria: Some(format!("Source: Linear {}", identifier)),
637637
project_path: None,
638+
workspace_id: None,
638639
status: Some("backlog".to_string()),
639640
};
640641

apps/desktop/src-tauri/src/commands/mission.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ pub async fn create_task(
1111
description: Option<String>,
1212
acceptance_criteria: Option<String>,
1313
project_path: Option<String>,
14+
workspace_id: Option<String>,
1415
) -> Result<Value, String> {
1516
let input = AgentTaskInput {
1617
title: title.clone(),
1718
description,
1819
acceptance_criteria,
1920
project_path: project_path.clone(),
21+
workspace_id: workspace_id.clone(),
2022
status: Some("backlog".to_string()),
2123
};
2224

apps/desktop/src-tauri/src/db/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub fn run_migrations(conn: &Connection) -> Result<(), rusqlite::Error> {
99
let _ = conn.execute("ALTER TABLE agent_tasks ADD COLUMN project_path TEXT", []);
1010
let _ = conn.execute("ALTER TABLE provider_accounts ADD COLUMN plan TEXT", []);
1111
let _ = conn.execute("ALTER TABLE provider_accounts ADD COLUMN weekly_limit REAL", []);
12+
let _ = conn.execute("ALTER TABLE agent_tasks ADD COLUMN workspace_id TEXT REFERENCES workspaces(id)", []);
1213

1314
Ok(())
1415
}

apps/desktop/src/components/kanban-board.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Task } from "@/lib/tauri-ipc";
1+
import type { Task, AgentProcess } from "@/lib/tauri-ipc";
22
import type { LoopState } from "@/lib/review-loop";
33
import { Card, CardContent } from "@/components/ui/card";
44
import { Badge } from "@/components/ui/badge";
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
77
interface KanbanBoardProps {
88
tasks: Task[];
99
loopStates?: Map<string, LoopState>;
10+
runningAgents?: AgentProcess[];
1011
onTaskClick?: (task: Task) => void;
1112
onAddTask?: (column: string) => void;
1213
onAssignAgent?: (task: Task) => void;
@@ -70,11 +71,13 @@ function ScoreTrend({ history }: { history: LoopState["reviewHistory"] }) {
7071
function TaskCard({
7172
task,
7273
loopState,
74+
isAgentRunning,
7375
onClick,
7476
onAssign,
7577
}: {
7678
task: Task;
7779
loopState?: LoopState;
80+
isAgentRunning?: boolean;
7881
onClick?: () => void;
7982
onAssign?: () => void;
8083
}) {
@@ -102,10 +105,15 @@ function TaskCard({
102105
<div className="mt-2 flex items-center gap-2">
103106
{task.assigned_agent ? (
104107
<div className="flex items-center gap-1">
105-
<span className="h-1.5 w-1.5 rounded-full bg-amber-400" />
106-
<span className="mono text-[10px] text-amber-400">
108+
<span className={`h-1.5 w-1.5 rounded-full ${isAgentRunning ? "bg-emerald-400 animate-pulse" : "bg-amber-400"}`} />
109+
<span className={`mono text-[10px] ${isAgentRunning ? "text-emerald-400" : "text-amber-400"}`}>
107110
{task.assigned_agent.slice(0, 8)}
108111
</span>
112+
{isAgentRunning && (
113+
<span className="text-[9px] font-semibold uppercase tracking-wider text-emerald-400/80">
114+
Live
115+
</span>
116+
)}
109117
</div>
110118
) : onAssign ? (
111119
<Button
@@ -128,7 +136,17 @@ function TaskCard({
128136
);
129137
}
130138

131-
export default function KanbanBoard({ tasks, loopStates, onTaskClick, onAddTask, onAssignAgent }: KanbanBoardProps) {
139+
/** Check if a task's assigned agent is currently running by matching project_path. */
140+
function isTaskAgentRunning(task: Task, runningAgents?: AgentProcess[]): boolean {
141+
if (!task.assigned_agent || !runningAgents) return false;
142+
return runningAgents.some(
143+
(a) =>
144+
a.status === "running" &&
145+
a.project_path === task.project_path
146+
);
147+
}
148+
149+
export default function KanbanBoard({ tasks, loopStates, runningAgents, onTaskClick, onAddTask, onAssignAgent }: KanbanBoardProps) {
132150
return (
133151
<div className="grid grid-cols-5 gap-3 min-w-[750px]">
134152
{columns.map((col) => {
@@ -176,6 +194,7 @@ export default function KanbanBoard({ tasks, loopStates, onTaskClick, onAddTask,
176194
key={task.id}
177195
task={task}
178196
loopState={loopStates?.get(task.id)}
197+
isAgentRunning={isTaskAgentRunning(task, runningAgents)}
179198
onClick={() => onTaskClick?.(task)}
180199
onAssign={onAssignAgent ? () => onAssignAgent(task) : undefined}
181200
/>

0 commit comments

Comments
 (0)