Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion canvas/src/components/MemoryInspectorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,15 @@ function MemoryEntryRow({
onCancelEdit,
onDelete,
}: MemoryEntryRowProps) {
const bodyId = `memory-body-${entry.key.replace(/\s+/g, "-")}`;
return (
<div className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 overflow-hidden">
{/* Header row — click to expand/collapse */}
<button
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}
>
<span className="text-[10px] font-mono text-blue-400 truncate flex-1 min-w-0">
{entry.key}
Expand Down Expand Up @@ -444,7 +446,12 @@ function MemoryEntryRow({

{/* Expanded body */}
{isExpanded && (
<div className="border-t border-zinc-800/50 px-3 pb-3 pt-2 space-y-2">
<div
id={bodyId}
role="region"
aria-label={`Details for ${entry.key}`}
className="border-t border-zinc-800/50 px-3 pb-3 pt-2 space-y-2"
>
{entry.expires_at && (
<p className="text-[9px] text-zinc-500">
Expires: {new Date(entry.expires_at).toLocaleString()}
Expand Down
34 changes: 33 additions & 1 deletion canvas/src/components/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "◈" },
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -111,8 +135,16 @@ export function SidePanel() {
>
{/* Resize handle */}
<div
role="separator"
aria-label="Resize workspace panel"
aria-valuenow={width}
aria-valuemin={SIDEPANEL_MIN_WIDTH}
aria-valuemax={SIDEPANEL_MAX_WIDTH}
aria-orientation="vertical"
tabIndex={0}
onMouseDown={onMouseDown}
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-blue-500/30 active:bg-blue-500/50 transition-colors z-10"
onKeyDown={onResizeKeyDown}
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-blue-500/30 active:bg-blue-500/50 transition-colors z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset"
/>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800/40 bg-zinc-900/30">
Expand Down
5 changes: 3 additions & 2 deletions canvas/src/components/WorkspaceNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
{/* Degraded error preview */}
{data.status === "degraded" && data.lastSampleError && (
<div
className="text-[10px] text-amber-300/60 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20"
title={data.lastSampleError}
role="status"
className="text-[10px] text-amber-400 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20"
aria-label={`Error: ${data.lastSampleError}`}
>
{data.lastSampleError}
</div>
Expand Down
42 changes: 36 additions & 6 deletions canvas/src/components/tabs/ChatTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatMessage[]> {
async function loadMessagesFromDB(workspaceId: string): Promise<{ messages: ChatMessage[]; error: string | null }> {
try {
const activities = await api.get<Array<{
activity_type: string;
Expand Down Expand Up @@ -83,9 +83,12 @@ async function loadMessagesFromDB(workspaceId: string): Promise<ChatMessage[]> {
}
}
}
return messages;
} catch {
return [];
return { messages, error: null };
} catch (err) {
return {
messages: [],
error: err instanceof Error ? err.message : "Failed to load chat history",
};
}
}

Expand Down Expand Up @@ -162,6 +165,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
const [thinkingElapsed, setThinkingElapsed] = useState(0);
const [activityLog, setActivityLog] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const currentTaskRef = useRef(data.currentTask);
const sendingFromAPIRef = useRef(false);
const [agentReachable, setAgentReachable] = useState(false);
Expand All @@ -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]);
Expand Down Expand Up @@ -355,7 +361,31 @@ function MyChatPanel({ workspaceId, data }: Props) {
{loading && (
<div className="text-xs text-zinc-500 text-center py-4">Loading chat history...</div>
)}
{!loading && messages.length === 0 && (
{!loading && loadError !== null && messages.length === 0 && (
<div
role="alert"
className="mx-2 mt-2 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5"
>
<p className="text-[11px] text-red-400 mb-1.5">
Failed to load chat history: {loadError}
</p>
<button
onClick={() => {
setLoading(true);
setLoadError(null);
loadMessagesFromDB(workspaceId).then(({ messages: msgs, error: fetchErr }) => {
setMessages(msgs);
setLoadError(fetchErr);
setLoading(false);
});
}}
className="text-[10px] px-2 py-0.5 rounded bg-red-800/40 text-red-300 hover:bg-red-700/50 transition-colors"
>
Retry
</button>
</div>
)}
{!loading && loadError === null && messages.length === 0 && (
<div className="text-xs text-zinc-500 text-center py-8">
No messages yet. Send a message to start chatting with this agent.
</div>
Expand Down