From ff3dcd37f6db6f036e8a23baddf26c27a774e0d2 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 10:09:15 -0700 Subject: [PATCH 1/2] fix(chat-history): correct docstring inversion + pin empty-history JSON shape (#2485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from the multi-axis review of #2474: 1. **Docstring inversion** in tool_chat_history. The doc said '(source_id=peer)' meant 'this workspace is the sender' — actually it means the *peer* is the sender (source_id is where the activity came FROM). Reframed to 'where the peer is either the sender or the recipient' to match the underlying SQL semantics. 2. **Empty-history test**. TestChatHistory had 10 tests but no 200+[] happy-path pin. Added test_empty_history_returns_empty_json_list asserting result == '[]' on exact-equality (per assert-exact memory — substring '[]' would match envelope shapes too). Both changes are pure docs+tests — no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) --- workspace/a2a_tools.py | 7 ++++--- workspace/tests/test_a2a_tools_impl.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/workspace/a2a_tools.py b/workspace/a2a_tools.py index cf855b61..a6ffed7e 100644 --- a/workspace/a2a_tools.py +++ b/workspace/a2a_tools.py @@ -559,9 +559,10 @@ async def tool_chat_history(peer_id: str, limit: int = 20, before_ts: str = "") Hits ``/workspaces//activity?peer_id=&limit=`` against the workspace-server, which returns activity rows where - this workspace is either the sender (``source_id=peer``) or the - recipient (``target_id=peer``) of an A2A turn — both sides of the - conversation in chronological order. + the peer is either the sender (``source_id=peer`` — they sent us + the message) or the recipient (``target_id=peer`` — we sent to + them) of an A2A turn — both sides of the conversation in + chronological order. Args: peer_id: The other workspace's UUID. Same value the agent diff --git a/workspace/tests/test_a2a_tools_impl.py b/workspace/tests/test_a2a_tools_impl.py index 1dd2fa14..5d994280 100644 --- a/workspace/tests/test_a2a_tools_impl.py +++ b/workspace/tests/test_a2a_tools_impl.py @@ -1050,6 +1050,27 @@ class TestChatHistory: assert mc.get.call_args.kwargs["params"]["before_ts"] == "2026-05-01T00:00:00Z" + async def test_empty_history_returns_empty_json_list(self): + """Pin the happy-path-with-no-rows shape: server returns 200 + with an empty list, the wheel returns the JSON literal ``"[]"``. + + Without this pin the surrounding tests all pre-populate rows; + none verify what an agent sees when there's literally no chat + history with this peer yet (a fresh A2A peering, or a peer + whose history was rotated out). #2485. + """ + import a2a_tools + + mc = _make_http_mock(get_resp=_resp(200, [])) + with patch("a2a_tools.httpx.AsyncClient", return_value=mc): + result = await a2a_tools.tool_chat_history(peer_id=_PEER) + + # Exact-equality on the JSON literal (per assert-exact memory) — + # substring "[]" would also match `{"items": []}` or any number + # of envelope shapes, only `result == "[]"` discriminates the + # bare-list contract callers depend on. + assert result == "[]" + async def test_reverses_desc_response_to_chronological(self): """Server returns DESC (newest first); the wheel reverses to chronological so the agent reads the chat top-down — same From a4a32cded514f6a12e95c3e2f30c56bc3a85bc65 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 10:28:49 -0700 Subject: [PATCH 2/2] fix(canvas): WorkspaceNode + tier-config contrast in light theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cards on the canvas had multiple low-contrast surfaces in light mode: WorkspaceNode.tsx (parent + TeamMemberChip) — same fixes both copies: - "N sub" badge: hardcoded text-violet-300 + bg-violet-900/40 → semantic text-accent + bg-accent/15 + border-accent/40 (themes correctly). - "REMOTE" pill: hardcoded violet/40 alpha → solid bg-violet-600 text-white (works on either surface with WCAG AA contrast). - Runtime pill: drop /60 + /30 alpha modifiers, use solid surface-card + border-line tokens. - Skill chips (online): text-good/80 + bg-emerald-950/30 (washed-out on warm-paper) → text-good + bg-good/15 + border-good/40 semantic. - Skill chips (offline): text-ink-mid + bg-surface-card without alpha. - Restart-to-apply banner: bg-sky-950/30 + text-sky-300/80 → bg-accent/10 + text-accent (sky-950 was nearly invisible on cream). - Provisioning status text: text-sky-400 (poor on cream) → text-accent. - "+N more" badges: text-ink-soft (3.5:1) → text-ink-mid (7:1). - Active-tasks dot: bg-amber-400 + text-warm/80 → semantic bg-warm + text-warm. - Degraded error preview: bg-amber-950/20 + text-warm/60 → bg-warm/10 + text-warm + border-warm/40. - Eject icon hover: hover:text-sky-400 → hover:text-accent. - Role text: text-ink-soft → text-ink-mid. design-tokens.ts: - TIER_CONFIG was dark-only: T2 (text-sky-400 + bg-sky-950/50), T3 (text-violet-400 + bg-violet-950/50), T4 (text-warm + bg-amber-950/50). Migrated to solid bg + white text patterns: T2=accent, T3=violet-600, T4=warm. T1 stays neutral (surface-card + ink-mid). All four pass WCAG AA on either theme. No globals.css changes; uses existing semantic tokens. --- canvas/src/components/WorkspaceNode.tsx | 46 ++++++++++++------------- canvas/src/lib/design-tokens.ts | 8 ++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/canvas/src/components/WorkspaceNode.tsx b/canvas/src/components/WorkspaceNode.tsx index 8dbdeb38..b2154dd3 100644 --- a/canvas/src/components/WorkspaceNode.tsx +++ b/canvas/src/components/WorkspaceNode.tsx @@ -36,7 +36,7 @@ function EjectIcon(props: React.SVGProps) { export function WorkspaceNode({ id, data }: NodeProps>) { const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline; - const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" }; + const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-mid bg-surface-card border border-line" }; // Org-deploy context — four derived flags off one store subscription. // Drives the shimmer while provisioning, the dimmed/non-draggable // treatment on locked descendants, and the Cancel pill on the root. @@ -179,7 +179,7 @@ export function WorkspaceNode({ id, data }: NodeProps>)
{hasChildren && ( - + {descendantCount} sub )} @@ -207,13 +207,13 @@ export function WorkspaceNode({ id, data }: NodeProps>)
{runtime === "external" ? ( ★ REMOTE ) : ( - + {runtime} )} @@ -237,15 +237,15 @@ export function WorkspaceNode({ id, data }: NodeProps>) key={skill} className={`text-[10px] px-1.5 py-0.5 rounded-md border ${ isOnline - ? "text-good/80 bg-emerald-950/30 border-emerald-800/30" - : "text-ink-mid bg-surface-card/60 border-line/40" + ? "text-good bg-good/15 border-good/40" + : "text-ink-mid bg-surface-card border-line" }`} > {skill} ))} {skills.length > 4 && ( - + +{skills.length - 4} )} @@ -274,10 +274,10 @@ export function WorkspaceNode({ id, data }: NodeProps>) e.stopPropagation(); useCanvasStore.getState().restartWorkspace(id).catch(() => showToast("Restart failed", "error")); }} - className="flex items-center gap-1.5 mt-1 w-full bg-sky-950/30 px-2 py-1 rounded-md border border-sky-800/30 hover:bg-sky-900/40 transition-colors text-left focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none" + className="flex items-center gap-1.5 mt-1 w-full bg-accent/10 px-2 py-1 rounded-md border border-accent/40 hover:bg-accent/20 transition-colors text-left focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none" > - - Restart to apply changes + + Restart to apply changes )} @@ -287,8 +287,8 @@ export function WorkspaceNode({ id, data }: NodeProps>)
{statusCfg.label}
@@ -296,8 +296,8 @@ export function WorkspaceNode({ id, data }: NodeProps>) {data.activeTasks > 0 && (
-
- +
+ {data.activeTasks} task{data.activeTasks > 1 ? "s" : ""}
@@ -307,7 +307,7 @@ export function WorkspaceNode({ id, data }: NodeProps>) {/* Degraded error preview */} {data.status === "degraded" && data.lastSampleError && (
{data.lastSampleError} @@ -357,7 +357,7 @@ function TeamMemberChip({ }) { const { data } = node; const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline; - const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" }; + const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-mid bg-surface-card border border-line" }; const isOnline = data.status === "online"; const skills = getSkillNames(data.agentCard); @@ -408,7 +408,7 @@ function TeamMemberChip({
{hasSubChildren && ( - + {descendantCount} )} @@ -423,7 +423,7 @@ function TeamMemberChip({ e.stopPropagation(); onExtract(node.id); }} - className="opacity-0 group-hover/child:opacity-100 text-ink-soft hover:text-sky-400 transition-all focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none rounded" + className="opacity-0 group-hover/child:opacity-100 text-ink-mid hover:text-accent transition-all focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none rounded" >