From cbc523a2d99f271f54af5fa895b1d7f3c30cb2b5 Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 20:33:52 +0000 Subject: [PATCH 1/4] fix(canvas): wire aria-controls on MemoryEntryRow expand toggle Add bodyId derived from entry.key, attach aria-controls={bodyId} to the toggle button, and add id={bodyId} role="region" aria-label to the collapsible body div. Screen readers can now announce the expand/collapse relationship between the button and the region it controls (WCAG 4.1.2). Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/components/MemoryInspectorPanel.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/canvas/src/components/MemoryInspectorPanel.tsx b/canvas/src/components/MemoryInspectorPanel.tsx index ed54d8b5..70ed8b8e 100644 --- a/canvas/src/components/MemoryInspectorPanel.tsx +++ b/canvas/src/components/MemoryInspectorPanel.tsx @@ -410,6 +410,7 @@ function MemoryEntryRow({ onCancelEdit, onDelete, }: MemoryEntryRowProps) { + const bodyId = `memory-body-${entry.key.replace(/\s+/g, "-")}`; return (
{/* Header row — click to expand/collapse */} @@ -417,6 +418,7 @@ function MemoryEntryRow({ className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-zinc-800/30 transition-colors" onClick={onToggle} aria-expanded={isExpanded} + aria-controls={bodyId} > {entry.key} @@ -444,7 +446,12 @@ function MemoryEntryRow({ {/* Expanded body */} {isExpanded && ( -
+
{entry.expires_at && (

Expires: {new Date(entry.expires_at).toLocaleString()} From d07909f46b28667f8d79c0efccbf238bda531f18 Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 20:34:04 +0000 Subject: [PATCH 2/4] fix(canvas): fix degraded error text contrast and accessibility Replace title attribute (not read by screen readers for truncated text) with aria-label, add role="status" so live regions announce the error, and raise text color from text-amber-300/60 (~2.1:1) to text-amber-400 (~10.6:1) to meet WCAG AA contrast (4.5:1 minimum). Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/components/WorkspaceNode.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/canvas/src/components/WorkspaceNode.tsx b/canvas/src/components/WorkspaceNode.tsx index ad469de6..9913ad82 100644 --- a/canvas/src/components/WorkspaceNode.tsx +++ b/canvas/src/components/WorkspaceNode.tsx @@ -256,8 +256,9 @@ export function WorkspaceNode({ id, data }: NodeProps>) {/* Degraded error preview */} {data.status === "degraded" && data.lastSampleError && (

{data.lastSampleError}
From 56f085bae40b53aa52cea6e25395d0f7dba3ad32 Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 20:34:48 +0000 Subject: [PATCH 3/4] fix(canvas): expose loadMessagesFromDB failures with error banner + Retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously loadMessagesFromDB swallowed all errors and returned [] — a network failure was indistinguishable from an empty history, so the user had no way to know loading failed. Now the function returns { messages, error } and the MyChatPanel renders a role="alert" banner with the error message and a Retry button when messages are empty and a load error occurred. Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/components/tabs/ChatTab.tsx | 42 ++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/canvas/src/components/tabs/ChatTab.tsx b/canvas/src/components/tabs/ChatTab.tsx index f1b8bbb0..f3063baa 100644 --- a/canvas/src/components/tabs/ChatTab.tsx +++ b/canvas/src/components/tabs/ChatTab.tsx @@ -55,7 +55,7 @@ function extractReplyText(resp: A2AResponse): string { * Load chat history from the activity_logs database via the platform API. * Uses source=canvas to only get user-initiated messages (not agent-to-agent). */ -async function loadMessagesFromDB(workspaceId: string): Promise { +async function loadMessagesFromDB(workspaceId: string): Promise<{ messages: ChatMessage[]; error: string | null }> { try { const activities = await api.get { } } } - return messages; - } catch { - return []; + return { messages, error: null }; + } catch (err) { + return { + messages: [], + error: err instanceof Error ? err.message : "Failed to load chat history", + }; } } @@ -162,6 +165,7 @@ function MyChatPanel({ workspaceId, data }: Props) { const [thinkingElapsed, setThinkingElapsed] = useState(0); const [activityLog, setActivityLog] = useState([]); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); const currentTaskRef = useRef(data.currentTask); const sendingFromAPIRef = useRef(false); const [agentReachable, setAgentReachable] = useState(false); @@ -172,8 +176,10 @@ function MyChatPanel({ workspaceId, data }: Props) { // Load chat history from database on mount useEffect(() => { setLoading(true); - loadMessagesFromDB(workspaceId).then((msgs) => { + setLoadError(null); + loadMessagesFromDB(workspaceId).then(({ messages: msgs, error: fetchErr }) => { setMessages(msgs); + setLoadError(fetchErr); setLoading(false); }); }, [workspaceId]); @@ -355,7 +361,31 @@ function MyChatPanel({ workspaceId, data }: Props) { {loading && (
Loading chat history...
)} - {!loading && messages.length === 0 && ( + {!loading && loadError !== null && messages.length === 0 && ( +
+

+ Failed to load chat history: {loadError} +

+ +
+ )} + {!loading && loadError === null && messages.length === 0 && (
No messages yet. Send a message to start chatting with this agent.
From 8697a424471f654f00d917a13f97d6607aacdb77 Mon Sep 17 00:00:00 2001 From: Molecule AI Frontend Engineer Date: Fri, 17 Apr 2026 20:35:15 +0000 Subject: [PATCH 4/4] fix(canvas): add keyboard resize + ARIA to SidePanel resize handle Add role="separator" + aria-valuenow/min/max/orientation + tabIndex={0} to make the resize handle focusable and discoverable by screen readers (WAI-ARIA slider pattern). Add onKeyDown handler: ArrowLeft/Right moves by 16px, Home/End snaps to min/max. Persist width to localStorage on keyboard resize, matching the existing mouse behaviour. Focus ring uses focus-visible:ring-2 to avoid showing on mouse click. Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/components/SidePanel.tsx | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/canvas/src/components/SidePanel.tsx b/canvas/src/components/SidePanel.tsx index 64ec2601..6f917de5 100644 --- a/canvas/src/components/SidePanel.tsx +++ b/canvas/src/components/SidePanel.tsx @@ -23,6 +23,7 @@ import { summarizeWorkspaceCapabilities } from "@/store/canvas"; const SIDEPANEL_WIDTH_KEY = "molecule:sidepanel-width"; const SIDEPANEL_DEFAULT_WIDTH = 480; const SIDEPANEL_MIN_WIDTH = 320; +const SIDEPANEL_MAX_WIDTH = 800; const TABS: { id: PanelTab; label: string; icon: string }[] = [ { id: "chat", label: "Chat", icon: "◈" }, @@ -72,6 +73,29 @@ export function SidePanel() { document.body.style.userSelect = "none"; }, [width]); + const onResizeKeyDown = useCallback((e: React.KeyboardEvent) => { + const STEP = 16; + let newWidth: number | null = null; + if (e.key === "ArrowLeft") { + e.preventDefault(); + newWidth = Math.min(width + STEP, SIDEPANEL_MAX_WIDTH); + } else if (e.key === "ArrowRight") { + e.preventDefault(); + newWidth = Math.max(width - STEP, SIDEPANEL_MIN_WIDTH); + } else if (e.key === "Home") { + e.preventDefault(); + newWidth = SIDEPANEL_MIN_WIDTH; + } else if (e.key === "End") { + e.preventDefault(); + newWidth = SIDEPANEL_MAX_WIDTH; + } + if (newWidth !== null) { + setWidth(newWidth); + widthRef.current = newWidth; + localStorage.setItem(SIDEPANEL_WIDTH_KEY, String(newWidth)); + } + }, [width]); + useEffect(() => { const onMouseMove = (e: MouseEvent) => { if (!dragging.current) return; @@ -111,8 +135,16 @@ export function SidePanel() { > {/* Resize handle */}
{/* Header */}