summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx12
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts23
-rw-r--r--makima/frontend/src/hooks/useMultiTaskSubscription.ts5
-rw-r--r--makima/src/db/repository.rs17
4 files changed, 47 insertions, 10 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index f9e7eed..b73463d 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -54,6 +54,16 @@ export function DirectiveDetail({
const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status));
// Build task map from directive steps and orchestrator
+ // Derive a stable key from the actual task IDs to avoid recreating the map on every poll
+ const taskMapKey = useMemo(() => {
+ const parts: string[] = [];
+ if (directive.orchestratorTaskId) parts.push(`o:${directive.orchestratorTaskId}`);
+ for (const step of directive.steps) {
+ if (step.taskId) parts.push(`${step.id}:${step.taskId}`);
+ }
+ return parts.join(",");
+ }, [directive.orchestratorTaskId, directive.steps]);
+
const taskMap = useMemo(() => {
const map = new Map<string, string>();
if (directive.orchestratorTaskId) {
@@ -65,7 +75,7 @@ export function DirectiveDetail({
}
}
return map;
- }, [directive.orchestratorTaskId, directive.steps]);
+ }, [taskMapKey]); // eslint-disable-line react-hooks/exhaustive-deps
// Subscribe to all task outputs
const { connected, entries, clearEntries } = useMultiTaskSubscription({
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
index e67733c..0453d14 100644
--- a/makima/frontend/src/hooks/useDirectives.ts
+++ b/makima/frontend/src/hooks/useDirectives.ts
@@ -63,6 +63,19 @@ export function useDirective(id: string | undefined) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
+ // Silently refresh without setting loading state (for polls)
+ const silentRefresh = useCallback(async () => {
+ if (!id) return;
+ try {
+ const d = await getDirective(id);
+ setDirective(d);
+ setError(null);
+ } catch (e) {
+ // Don't overwrite existing data on poll failure
+ }
+ }, [id]);
+
+ // Full refresh with loading state (for initial load / explicit refresh)
const refresh = useCallback(async () => {
if (!id) return;
try {
@@ -77,9 +90,13 @@ export function useDirective(id: string | undefined) {
}
}, [id]);
+ // Reset state and fetch when ID changes
useEffect(() => {
+ setDirective(null);
+ setError(null);
+ setLoading(true);
refresh();
- }, [refresh]);
+ }, [id]); // eslint-disable-line react-hooks/exhaustive-deps
// Auto-poll while directive is active, has an orchestrator task, or has a completion task
useEffect(() => {
@@ -90,9 +107,9 @@ export function useDirective(id: string | undefined) {
directive.completionTaskId != null;
if (!needsPolling) return;
- const interval = setInterval(refresh, 5000);
+ const interval = setInterval(silentRefresh, 5000);
return () => clearInterval(interval);
- }, [directive?.status, directive?.orchestratorTaskId, directive?.completionTaskId, refresh]);
+ }, [directive?.status, directive?.orchestratorTaskId, directive?.completionTaskId, silentRefresh]);
const update = useCallback(async (req: UpdateDirectiveRequest) => {
if (!id) return;
diff --git a/makima/frontend/src/hooks/useMultiTaskSubscription.ts b/makima/frontend/src/hooks/useMultiTaskSubscription.ts
index 19d6dea..4303f1b 100644
--- a/makima/frontend/src/hooks/useMultiTaskSubscription.ts
+++ b/makima/frontend/src/hooks/useMultiTaskSubscription.ts
@@ -38,8 +38,9 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption
enabledRef.current = enabled;
}, [enabled]);
- // Derive task IDs from the map
- const taskIds = useMemo(() => Array.from(taskMap.keys()), [taskMap]);
+ // Derive task IDs from the map, stabilized to avoid unnecessary effect triggers
+ const taskIdsKey = useMemo(() => Array.from(taskMap.keys()).sort().join(","), [taskMap]);
+ const taskIds = useMemo(() => Array.from(taskMap.keys()), [taskIdsKey]); // eslint-disable-line react-hooks/exhaustive-deps
const subscribeToTask = useCallback((ws: WebSocket, taskId: string) => {
if (ws.readyState === WebSocket.OPEN) {
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 8923f97..e288eba 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -4993,11 +4993,20 @@ pub async fn list_directives_for_owner(
d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url,
d.orchestrator_task_id, d.pr_url, d.completion_task_id,
d.version, d.created_at, d.updated_at,
- COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id), 0) as total_steps,
- COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'completed'), 0) as completed_steps,
- COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'running'), 0) as running_steps,
- COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'failed'), 0) as failed_steps
+ COALESCE(s.total_steps, 0) as total_steps,
+ COALESCE(s.completed_steps, 0) as completed_steps,
+ COALESCE(s.running_steps, 0) as running_steps,
+ COALESCE(s.failed_steps, 0) as failed_steps
FROM directives d
+ LEFT JOIN LATERAL (
+ SELECT
+ COUNT(*) as total_steps,
+ COUNT(*) FILTER (WHERE status = 'completed') as completed_steps,
+ COUNT(*) FILTER (WHERE status = 'running') as running_steps,
+ COUNT(*) FILTER (WHERE status = 'failed') as failed_steps
+ FROM directive_steps
+ WHERE directive_id = d.id
+ ) s ON true
WHERE d.owner_id = $1
ORDER BY d.created_at DESC
"#,