From bc612a782e8266436621e132db447cc9d9347cbf Mon Sep 17 00:00:00 2001 From: Vader Yang Date: Fri, 15 May 2026 16:21:23 +0800 Subject: [PATCH] feat(console/list-pages): persist selected item id in URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list pages (Agent Turns, LLM Calls, HTTP Exchanges) all kept the slide-over detail's open id in React state, so copying the URL or hitting refresh dropped the selection and reopened the list with no panel — invisible to anyone trying to bookmark or share a link to a specific item. Other filters on these pages already round-trip through the URL via useSearchParamState; the selection was the one holdout. Move the selected id onto a `?selected=` query param backed by the same hook. Effects: * Open detail → URL gains ?selected=; copy / share lands a recipient on the same row with the panel open. * Refresh / back-forward navigation restores the panel. * Close panel → param removed (empty string is the default → hook drops it from the URL). LLM Calls and HTTP Exchanges also tracked `selectedIndex` for the prev/next buttons on their detail panels. The index is now derived from `items.findIndex(i => i.id === selectedId)` — one source of truth (the URL) and prev/next continues to work as long as the selected id is on the current page. If a paste-in id is no longer on the visible page, the buttons disable naturally (index = -1) but the detail still loads, since the panel queries by id rather than indexing into items. Co-Authored-By: Claude Opus 4.7 (1M context) --- console/src/pages/agent-turns.tsx | 6 +++--- console/src/pages/http-exchanges.tsx | 30 +++++++++++++++++----------- console/src/pages/llm-calls.tsx | 30 +++++++++++++++++----------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/console/src/pages/agent-turns.tsx b/console/src/pages/agent-turns.tsx index 3eb3d01..13beecb 100644 --- a/console/src/pages/agent-turns.tsx +++ b/console/src/pages/agent-turns.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react" +import { useCallback } from "react" import { ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, Loader2, Filter } from "lucide-react" import { cn } from "@/lib/utils" import { useAgentTurns } from "@/hooks/use-agent-turns" @@ -93,7 +93,7 @@ export function AgentTurnsPage() { const statusFilter = statusStr ? statusStr.split(",") : [] const agentKindFilter = agentKindStr ? agentKindStr.split(",") : [] - const [selectedId, setSelectedId] = useState(null) + const [selectedId, setSelectedId] = useSearchParamState("selected", "") const { data, isLoading, isError, error } = useAgentTurns({ page, @@ -283,7 +283,7 @@ export function AgentTurnsPage() { {/* Slide-over detail panel */} {selectedId && ( - setSelectedId(null)} /> + setSelectedId("")} /> )} ) diff --git a/console/src/pages/http-exchanges.tsx b/console/src/pages/http-exchanges.tsx index 6c9ad37..4f16d0b 100644 --- a/console/src/pages/http-exchanges.tsx +++ b/console/src/pages/http-exchanges.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react" +import { useCallback } from "react" import { ArrowUpDown, ArrowUp, @@ -131,8 +131,7 @@ export function HttpExchangesPage() { const statusFilter = statusStr ? statusStr.split(",") : [] const isSse = sseStr === "true" ? true : sseStr === "false" ? false : undefined - const [selectedId, setSelectedId] = useState(null) - const [selectedIndex, setSelectedIndex] = useState(-1) + const [selectedId, setSelectedId] = useSearchParamState("selected", "") const { data, isLoading, isError, error } = useHttpExchanges({ page, @@ -166,26 +165,33 @@ export function HttpExchangesPage() { [sortBy, sortOrder, setSortBy, setSortOrder, setPageStr], ) - const handleRowClick = useCallback((id: string, index: number) => { - setSelectedId(id) - setSelectedIndex(index) - }, []) + // Index derived from id so the selection survives URL paste / refresh: + // we own only one source of truth (the URL), and prev/next still works + // as long as the selected id is on the current page. + const selectedIndex = selectedId + ? items.findIndex((i) => i.id === selectedId) + : -1 + + const handleRowClick = useCallback( + (id: string, _index: number) => { + setSelectedId(id) + }, + [setSelectedId], + ) const handleNavigate = useCallback( (direction: "prev" | "next") => { const newIndex = direction === "prev" ? selectedIndex - 1 : selectedIndex + 1 if (newIndex >= 0 && newIndex < items.length) { - setSelectedIndex(newIndex) setSelectedId(items[newIndex].id) } }, - [selectedIndex, items], + [selectedIndex, items, setSelectedId], ) const handleClose = useCallback(() => { - setSelectedId(null) - setSelectedIndex(-1) - }, []) + setSelectedId("") + }, [setSelectedId]) return (
diff --git a/console/src/pages/llm-calls.tsx b/console/src/pages/llm-calls.tsx index cae64c9..de22dd3 100644 --- a/console/src/pages/llm-calls.tsx +++ b/console/src/pages/llm-calls.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo } from "react" +import { useCallback, useMemo } from "react" import { ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, Loader2, Filter } from "lucide-react" import { cn } from "@/lib/utils" import { useLlmCalls } from "@/hooks/use-llm-calls" @@ -145,8 +145,7 @@ export function LlmCallsPage() { .map(([label, options]) => ({ label, options: [...options].sort() })) }, [finishReasonsData]) - const [selectedId, setSelectedId] = useState(null) - const [selectedIndex, setSelectedIndex] = useState(-1) + const [selectedId, setSelectedId] = useSearchParamState("selected", "") const { data, isLoading, isError, error } = useLlmCalls({ page, @@ -179,26 +178,33 @@ export function LlmCallsPage() { [sortBy, sortOrder, setSortBy, setSortOrder, setPageStr], ) - const handleRowClick = useCallback((id: string, index: number) => { - setSelectedId(id) - setSelectedIndex(index) - }, []) + // Index derived from id so the selection survives URL paste / refresh: + // we own only one source of truth (the URL), and prev/next still works + // as long as the selected id is on the current page. + const selectedIndex = selectedId + ? items.findIndex((i) => i.id === selectedId) + : -1 + + const handleRowClick = useCallback( + (id: string, _index: number) => { + setSelectedId(id) + }, + [setSelectedId], + ) const handleNavigate = useCallback( (direction: "prev" | "next") => { const newIndex = direction === "prev" ? selectedIndex - 1 : selectedIndex + 1 if (newIndex >= 0 && newIndex < items.length) { - setSelectedIndex(newIndex) setSelectedId(items[newIndex].id) } }, - [selectedIndex, items], + [selectedIndex, items, setSelectedId], ) const handleClose = useCallback(() => { - setSelectedId(null) - setSelectedIndex(-1) - }, []) + setSelectedId("") + }, [setSelectedId]) return (