diff --git a/canvas/src/components/Toolbar.tsx b/canvas/src/components/Toolbar.tsx index 230e5227..a51e9fa0 100644 --- a/canvas/src/components/Toolbar.tsx +++ b/canvas/src/components/Toolbar.tsx @@ -154,10 +154,10 @@ export function Toolbar() { {counts.failed > 0 && ( )} - - + + {counts.roots} workspace{counts.roots !== 1 ? "s" : ""} - {counts.children > 0 && + {counts.children} sub} + {counts.children > 0 && + {counts.children} sub} @@ -172,7 +172,7 @@ export function Toolbar() { type="button" onClick={stopAll} disabled={stopping} - className="flex items-center gap-1.5 px-2.5 py-1 bg-red-950/50 hover:bg-red-900/60 border border-red-800/40 rounded-lg transition-colors disabled:opacity-50" + className="flex items-center gap-1.5 px-2.5 py-1 bg-bad/10 hover:bg-bad/20 border border-bad/40 rounded-lg transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-bad/40" title={`Stop all running tasks (${counts.activeTasks} active)`} aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`} > @@ -191,7 +191,7 @@ export function Toolbar() { type="button" onClick={() => setRestartConfirmOpen(true)} disabled={restartingAll} - className="flex items-center gap-1.5 px-2.5 py-1 bg-amber-950/40 hover:bg-amber-900/50 border border-amber-800/40 rounded-lg transition-colors disabled:opacity-50" + className="flex items-center gap-1.5 px-2.5 py-1 bg-warm/10 hover:bg-warm/20 border border-warm/40 rounded-lg transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-warm/40" title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`} aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`} > @@ -216,10 +216,10 @@ export function Toolbar() { aria-pressed={showA2AEdges} aria-label={showA2AEdges ? "Hide A2A edges" : "Show A2A edges"} title={showA2AEdges ? "Hide A2A delegation edges" : "Show A2A delegation edges (last 60 min)"} - className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors ${ + className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${ showA2AEdges - ? "bg-blue-950/50 hover:bg-blue-900/50 border-blue-800/40 text-accent" - : "bg-surface-card/50 hover:bg-surface-card/50 border-line/40 text-ink-soft hover:text-ink-mid" + ? "bg-accent/15 hover:bg-accent/25 border-accent/50 text-accent" + : "bg-surface-card hover:bg-surface-card/70 border-line text-ink-mid hover:text-ink" }`} > {/* Mesh / network icon */} @@ -255,7 +255,7 @@ export function Toolbar() { }} aria-label="Open audit trail for selected workspace" title="Audit — view ledger for the selected workspace" - className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid" + className="flex items-center justify-center w-7 h-7 bg-surface-card hover:bg-surface-card/70 border border-line rounded-lg transition-colors text-ink-mid hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40" > {/* Scroll / ledger icon */} useCanvasStore.getState().setSearchOpen(true)} aria-label="Search workspaces" title="Search (⌘K)" - className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid" + className="flex items-center justify-center w-7 h-7 bg-surface-card hover:bg-surface-card/70 border border-line rounded-lg transition-colors text-ink-mid hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40" >
); } @@ -366,14 +366,14 @@ function WsStatusPill({ status }: { status: "connected" | "connecting" | "discon return (
); } return (
); } @@ -384,7 +384,7 @@ function HelpRow({ shortcut, text }: { shortcut: string; text: string }) { {shortcut} -

{text}

+

{text}

); } diff --git a/workspace-server/migrations/048_activity_logs_peer_indexes.down.sql b/workspace-server/migrations/048_activity_logs_peer_indexes.down.sql new file mode 100644 index 00000000..f075114e --- /dev/null +++ b/workspace-server/migrations/048_activity_logs_peer_indexes.down.sql @@ -0,0 +1,7 @@ +-- Reverse 048_activity_logs_peer_indexes.up.sql. +-- Drops the partial peer-conversation indexes added there. +-- chat_history queries fall back to the existing idx_activity_ws_type_time +-- + workspace-scoped seq scan / filter on the OR clause. + +DROP INDEX IF EXISTS idx_activity_ws_target; +DROP INDEX IF EXISTS idx_activity_ws_source; diff --git a/workspace-server/migrations/048_activity_logs_peer_indexes.up.sql b/workspace-server/migrations/048_activity_logs_peer_indexes.up.sql new file mode 100644 index 00000000..bd4b6888 --- /dev/null +++ b/workspace-server/migrations/048_activity_logs_peer_indexes.up.sql @@ -0,0 +1,42 @@ +-- Add per-peer indexes on activity_logs to make chat_history queries +-- index-driven instead of seq-scan-driven on workspaces with thousands +-- of accumulated rows. #2478. +-- +-- chat_history hits: +-- +-- SELECT ... FROM activity_logs +-- WHERE workspace_id = $1 +-- AND activity_type = 'a2a_receive' +-- AND (source_id = $2 OR target_id = $2) +-- ORDER BY created_at DESC LIMIT 20; +-- +-- The existing idx_activity_ws_type_time covers workspace_id+type +-- prefix but the (source_id = $X OR target_id = $X) clause then forces +-- a workspace-scoped seq-scan-and-filter. Two separate indexes (one per +-- nullable column) let Postgres BitmapOr them into a workspace-scoped +-- BitmapAnd against the existing index. +-- +-- Partial WHERE NOT NULL because most activity rows (heartbeats, +-- agent_log, memory_write, etc.) have NULL source_id/target_id and +-- shouldn't bloat the index. Per-row index size drops from ~all rows +-- to ~A2A-only rows. +-- +-- Anti-pattern caveat from the issue: a single compound (a, b) index +-- can't serve `a OR b` — Postgres can only use compound for prefix +-- match. Two separate indexes + BitmapOr is the right shape. +-- +-- CONCURRENTLY would be ideal for online deploys, but goose runs +-- migrations in a single transaction by default which doesn't allow +-- CONCURRENTLY. The alternative (annotating the migration to skip the +-- transaction wrapper) is a per-runner concern; leaving as plain +-- CREATE INDEX so this works under any goose config. activity_logs is +-- typically