fix(ui): resolve nested button error and stale agentId in chat

- Provider list: change outer <button> to <div role="button"> to fix
  invalid nested <button> HTML (delete button inside row button)
- Chat page: derive agentId via useMemo from URL session key instead of
  useState initializer that only runs once (stale on session switch)
- Agent change no longer creates ghost sessions — navigates to /chat
  and lets handleSend create session on first message
- Clear messages when navigating away from session (empty key only);
  session-to-session switch still replaces atomically via loadHistory
- Split ROUTES.CHAT into CHAT (nav) + CHAT_PATTERN (route definition)

Closes #349
This commit is contained in:
viettranx committed 2026-03-23 10:44:03 +07:00
1 parent 2ac5eb6c4b
commit 0973b842a2
5 files changed
+28 -16

No files matched your search

+2 -1
View File
@@ -1,7 +1,8 @@
export const ROUTES = {
LOGIN: "/login",
OVERVIEW: "/overview",
CHAT: "/chat/:sessionKey?",
CHAT: "/chat",
CHAT_PATTERN: "/chat/:sessionKey?",
AGENTS: "/agents",
AGENT_DETAIL: "/agents/:id",
SESSIONS: "/sessions",
+15 -11
View File
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useParams, useNavigate } from "react-router";
import { Eye, PanelLeftOpen } from "lucide-react";
@@ -29,13 +29,17 @@ export function ChatPage() {
// sessionKey derived from URL — single source of truth, no separate state
const sessionKey = urlSessionKey ?? "";
const [agentId, setAgentId] = useState(() => {
// Fallback agent ID used only when URL has no session key
const [agentIdFallback, setAgentIdFallback] = useState("default");
// Derive agentId from URL (source of truth), fallback to state when no session
const agentId = useMemo(() => {
if (urlSessionKey) {
const { agentId: parsed } = parseSessionKey(urlSessionKey);
if (parsed) return parsed;
}
return "default";
});
return agentIdFallback;
}, [urlSessionKey, agentIdFallback]);
const {
sessions,
@@ -91,12 +95,10 @@ export function ChatPage() {
const handleSessionSelect = useCallback(
(key: string) => {
const { agentId: parsed } = parseSessionKey(key);
if (parsed && parsed !== agentId) {
setAgentId(parsed);
}
if (parsed) setAgentIdFallback(parsed);
navigate(`/chat/${encodeURIComponent(key)}`);
},
[navigate, agentId],
[navigate],
);
const handleDeleteSession = useCallback(async (key: string) => {
@@ -113,10 +115,12 @@ export function ChatPage() {
const handleAgentChange = useCallback(
(newAgentId: string) => {
setAgentId(newAgentId);
navigate(`/chat/${encodeURIComponent(`agent:${newAgentId}:ws:direct:${crypto.randomUUID()}`)}`);
setAgentIdFallback(newAgentId);
if (sessionKey) {
navigate("/chat");
}
},
[navigate],
[navigate, sessionKey],
);
const handleSend = useCallback(
@@ -61,6 +61,11 @@ export function useChatMessages(sessionKey: string, agentId: string) {
blockRepliesRef.current = [];
cancelAnimationFrame(rafHandleRef.current);
rafPendingRef.current = false;
// Clear messages when navigating away from a session (empty key).
// When switching to another session, loadHistory() will replace them.
if (!sessionKey) {
setMessages([]);
}
}, [sessionKey]);
// Load history (no loading spinner — the empty state placeholder is shown instead)
@@ -17,9 +17,11 @@ export function ProviderListRow({ provider, onClick, onDelete }: ProviderListRow
const tb = PROVIDER_TYPE_BADGE[provider.provider_type] ?? { label: provider.provider_type, variant: "outline" as const };
return (
<button
type="button"
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }}
className="flex w-full cursor-pointer items-center gap-3 rounded-lg border bg-card px-4 py-3 text-left transition-all hover:border-primary/30 hover:shadow-sm"
>
{/* Icon */}
@@ -64,6 +66,6 @@ export function ProviderListRow({ provider, onClick, onDelete }: ProviderListRow
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</button>
</div>
);
}
+1 -1
View File
@@ -146,7 +146,7 @@ export function AppRoutes() {
>
<Route index element={<Navigate to={ROUTES.OVERVIEW} replace />} />
<Route path={ROUTES.OVERVIEW} element={<OverviewPage />} />
<Route path={ROUTES.CHAT} element={<ChatPage />} />
<Route path={ROUTES.CHAT_PATTERN} element={<ChatPage />} />
<Route path={ROUTES.AGENTS} element={<AgentsPage key="list" />} />
<Route path={ROUTES.AGENT_DETAIL} element={<AgentsPage key="detail" />} />
<Route path={ROUTES.TEAMS} element={<TeamsPage key="list" />} />