From e1d635a099e12fac2ea648e0b31e4edf5e780ee7 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 11:26:28 -0700 Subject: [PATCH 1/2] fix(canvas): Toolbar contrast + focus rings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-of-canvas Toolbar had multiple low-contrast surfaces in light theme: Action buttons (Stop All, Restart Pending): - bg-red-950/50 + bg-amber-950/40 → bg-bad/10 + bg-warm/10 with bg-bad/40 + bg-warm/40 borders. Dark-tinted backgrounds with /40-/50 alpha render as nearly invisible smudges on warm-paper; semantic tokens at /10 give a clear pale-bad / pale-warm tint that scales correctly in dark mode. - Both gain focus-visible:ring-2 focus-visible:ring-{bad,warm}/40. Toggle button (A2A edges): - Active state: bg-blue-950/50 → bg-accent/15 (themes correctly). - Inactive state: bg-surface-card/50 + text-ink-soft → solid bg-surface-card + text-ink-mid; hover bumps to text-ink. Drops the redundant "hover:bg-surface-card/50" identity hover. Icon buttons (Audit, Search, Help): - Same pattern as toggle inactive: solid bg-surface-card + text-ink-mid + text-ink hover, with focus-visible:ring-2 focus-visible:ring-accent/40. Workspace count + bullet separator: - text-ink-soft (3.5:1 on warm-paper) → text-ink-mid (7:1). WS connection status: - "Live": text-ink-soft → text-ink-mid (paired with the green dot). - "Reconnecting": text-ink-soft → text-warm (semantic match for amber dot). - "Offline": text-ink-soft → text-bad (semantic match for red dot). Status text now reinforces the dot colour instead of disappearing on light surfaces. Help popover: - Close button: text-ink-soft → text-ink-mid + focus-visible:underline. - HelpRow body text: text-ink-soft → text-ink-mid (was 3.5:1 on the bg-surface-sunken/45 popover row — failed AA for body text). --- canvas/src/components/Toolbar.tsx | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) 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}

); } From db132351a3b6f1013c7786d972fbacf4cfe2cf96 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 11:34:35 -0700 Subject: [PATCH 2/2] feat(db): add per-peer btree indexes on activity_logs for chat_history scale (#2478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chat_history query WHERE workspace_id = $1 AND activity_type = 'a2a_receive' AND (source_id = $2 OR target_id = $2) ORDER BY created_at DESC forces a workspace-scoped seq-scan-and-filter at every call — idx_activity_ws_type_time covers workspace_id+type prefix but the (source OR target) clause then walks every workspace row. Demo workspaces (≤50 rows) don't notice; production workspaces accumulate thousands over months and chat_history latency grows linearly. Adds two partial btree indexes (workspace_id, source_id) WHERE NOT NULL and (workspace_id, target_id) WHERE NOT NULL. Postgres BitmapOrs them into a workspace-scoped BitmapAnd against the existing index, dropping chat_history from O(workspace_rows) to O(peer_a2a_rows). Partial WHERE NOT NULL because most activity rows (heartbeats, agent_log, memory_write, etc.) carry NULL source_id/target_id and shouldn't bloat the index. Anti-pattern caveat (per the issue): a single compound (a, b) index can't serve 'a OR b' — Postgres only uses compound for prefix match. Two separate indexes + BitmapOr is the right shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../048_activity_logs_peer_indexes.down.sql | 7 ++++ .../048_activity_logs_peer_indexes.up.sql | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 workspace-server/migrations/048_activity_logs_peer_indexes.down.sql create mode 100644 workspace-server/migrations/048_activity_logs_peer_indexes.up.sql 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