From 9def7bee756c88e3ec1fb356a16d604d223eb57b Mon Sep 17 00:00:00 2001 From: Luke Mainwaring Date: Thu, 16 Jul 2026 17:59:48 -0400 Subject: [PATCH] refactor(frontend): replace tool-panel branching with a feature-slice registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the domain panel components out of components/ into features/sessions/ and features/retrieval/ slices, each exporting a tool-name -> panel map. features/tool-panel-registry.ts is the composition root that spreads the slice maps together, and message.tsx now does a single registry lookup instead of per-tool branching — adding a tool panel no longer touches message.tsx. The shared session_id input parser moves to lib/extract-session-id.ts; the list_sessions HTML-comment ID extraction (and its rationale) moves into the sessions slice where it belongs. Behavior preserved: panels mount only on output-available, unregistered tools keep the raw ToolCall JSON view, only list_sessions hides raw output, and a failed session_id parse renders no panel. Co-Authored-By: Claude Fable 5 --- frontend/components/message.tsx | 62 +++---------------- .../retrieval}/retrieved-tracks-panel.tsx | 2 +- frontend/features/retrieval/tool-panels.tsx | 20 ++++++ .../retrieval}/waveform-viz.tsx | 0 .../sessions}/emotion-trajectory.tsx | 0 .../sessions}/session-list-panel.tsx | 2 +- .../sessions}/session-visualization.tsx | 2 +- frontend/features/sessions/tool-panels.tsx | 44 +++++++++++++ frontend/features/tool-panel-registry.ts | 19 ++++++ frontend/lib/extract-session-id.ts | 9 +++ 10 files changed, 102 insertions(+), 58 deletions(-) rename frontend/{components => features/retrieval}/retrieved-tracks-panel.tsx (99%) create mode 100644 frontend/features/retrieval/tool-panels.tsx rename frontend/{components => features/retrieval}/waveform-viz.tsx (100%) rename frontend/{components => features/sessions}/emotion-trajectory.tsx (100%) rename frontend/{components => features/sessions}/session-list-panel.tsx (98%) rename frontend/{components => features/sessions}/session-visualization.tsx (99%) create mode 100644 frontend/features/sessions/tool-panels.tsx create mode 100644 frontend/features/tool-panel-registry.ts create mode 100644 frontend/lib/extract-session-id.ts diff --git a/frontend/components/message.tsx b/frontend/components/message.tsx index 761c58c..15618fd 100644 --- a/frontend/components/message.tsx +++ b/frontend/components/message.tsx @@ -5,6 +5,7 @@ import { getToolName, isToolUIPart } from "ai"; import equal from "fast-deep-equal"; import Image from "next/image"; import { memo } from "react"; +import { TOOL_PANELS } from "@/features/tool-panel-registry"; import type { ChatMessage } from "@/lib/types"; import { cn, sanitizeText } from "@/lib/utils"; import { BouncingDots } from "./elements/bouncing-dots"; @@ -12,37 +13,6 @@ import { MessageContent } from "./elements/message"; import { Response } from "./elements/response"; import { ToolCall } from "./elements/tool-call"; import { MessageActions } from "./message-actions"; -import { RetrievedTracksPanel } from "./retrieved-tracks-panel"; -import { SessionListPanel } from "./session-list-panel"; -import { SessionVisualization } from "./session-visualization"; - -function extractSessionId(input: unknown): string | null { - if (input && typeof input === "object" && "session_id" in input) { - const value = (input as { session_id: unknown }).session_id; - if (typeof value === "string" && value.length > 0) { - return value; - } - } - return null; -} - -// The list_sessions tool embeds each session's UUID in an HTML comment -// (``) on its line so the agent can resolve "Session 07" → UUID -// without leaking the UUID to the user. We extract the same comments here so -// the rendered panel mirrors exactly what the agent decided to show — order -// and all — instead of duplicating the tool's filter logic on the frontend. -const SESSION_ID_COMMENT = //gi; - -function extractSessionIdsFromOutput(output: unknown): string[] | null { - if (typeof output !== "string") { - return null; - } - const ids: string[] = []; - for (const match of output.matchAll(SESSION_ID_COMMENT)) { - ids.push(match[1]); - } - return ids.length > 0 ? ids : null; -} const AssistantAvatar = ({ isLoading }: { isLoading?: boolean }) => (
- {analyzeSessionId && ( - - )} - {retrievalSessionId && ( - - )} - {showSessionList && ( - + {showPanel && ( + )}
); diff --git a/frontend/components/retrieved-tracks-panel.tsx b/frontend/features/retrieval/retrieved-tracks-panel.tsx similarity index 99% rename from frontend/components/retrieved-tracks-panel.tsx rename to frontend/features/retrieval/retrieved-tracks-panel.tsx index c536561..95c0be0 100644 --- a/frontend/components/retrieved-tracks-panel.tsx +++ b/frontend/features/retrieval/retrieved-tracks-panel.tsx @@ -10,9 +10,9 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { WaveformViz } from "@/components/waveform-viz"; import { BACKEND_URL } from "@/lib/constants"; import { cn } from "@/lib/utils"; +import { WaveformViz } from "./waveform-viz"; // Backend serves the cached m4a under its own CORS policy so wavesurfer can // fetch + decodeAudioData; Apple's preview CDN does not reliably set CORS diff --git a/frontend/features/retrieval/tool-panels.tsx b/frontend/features/retrieval/tool-panels.tsx new file mode 100644 index 0000000..438e494 --- /dev/null +++ b/frontend/features/retrieval/tool-panels.tsx @@ -0,0 +1,20 @@ +"use client"; + +import type { + ToolPanelEntry, + ToolPanelProps, +} from "@/features/tool-panel-registry"; +import { extractSessionId } from "@/lib/extract-session-id"; +import { RetrievedTracksPanel } from "./retrieved-tracks-panel"; + +function RetrieveTracksPanel({ input }: ToolPanelProps) { + const sessionId = extractSessionId(input); + if (!sessionId) { + return null; + } + return ; +} + +export const RETRIEVAL_TOOL_PANELS: Record = { + retrieve_tracks_from_brain_state: { Panel: RetrieveTracksPanel }, +}; diff --git a/frontend/components/waveform-viz.tsx b/frontend/features/retrieval/waveform-viz.tsx similarity index 100% rename from frontend/components/waveform-viz.tsx rename to frontend/features/retrieval/waveform-viz.tsx diff --git a/frontend/components/emotion-trajectory.tsx b/frontend/features/sessions/emotion-trajectory.tsx similarity index 100% rename from frontend/components/emotion-trajectory.tsx rename to frontend/features/sessions/emotion-trajectory.tsx diff --git a/frontend/components/session-list-panel.tsx b/frontend/features/sessions/session-list-panel.tsx similarity index 98% rename from frontend/components/session-list-panel.tsx rename to frontend/features/sessions/session-list-panel.tsx index 2437e95..5831cb2 100644 --- a/frontend/components/session-list-panel.tsx +++ b/frontend/features/sessions/session-list-panel.tsx @@ -3,6 +3,7 @@ import { memo } from "react"; import type { SessionSummarySchema } from "@/api/generated/types.gen"; import { useEnrichedSessions } from "@/api/hooks/sessions"; +import { useChatActions } from "@/components/chat-actions-provider"; import { Tooltip, TooltipContent, @@ -10,7 +11,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { useChatActions } from "./chat-actions-provider"; const QUADRANT_ORDER = ["relaxed", "calm", "excited", "stressed"] as const; type Quadrant = (typeof QUADRANT_ORDER)[number]; diff --git a/frontend/components/session-visualization.tsx b/frontend/features/sessions/session-visualization.tsx similarity index 99% rename from frontend/components/session-visualization.tsx rename to frontend/features/sessions/session-visualization.tsx index f01a3a7..0ecc7fe 100644 --- a/frontend/components/session-visualization.tsx +++ b/frontend/features/sessions/session-visualization.tsx @@ -17,8 +17,8 @@ import type { TrajectorySummary, } from "@/api/generated/types.gen"; import { useSessionSegments } from "@/api/hooks/sessions"; -import { EmotionTrajectory } from "@/components/emotion-trajectory"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { EmotionTrajectory } from "./emotion-trajectory"; type Props = { sessionId: string; diff --git a/frontend/features/sessions/tool-panels.tsx b/frontend/features/sessions/tool-panels.tsx new file mode 100644 index 0000000..13e8286 --- /dev/null +++ b/frontend/features/sessions/tool-panels.tsx @@ -0,0 +1,44 @@ +"use client"; + +import type { + ToolPanelEntry, + ToolPanelProps, +} from "@/features/tool-panel-registry"; +import { extractSessionId } from "@/lib/extract-session-id"; +import { SessionListPanel } from "./session-list-panel"; +import { SessionVisualization } from "./session-visualization"; + +// The list_sessions tool embeds each session's UUID in an HTML comment +// (``) on its line so the agent can resolve "Session 07" → UUID +// without leaking the UUID to the user. We extract the same comments here so +// the rendered panel mirrors exactly what the agent decided to show — order +// and all — instead of duplicating the tool's filter logic on the frontend. +const SESSION_ID_COMMENT = //gi; + +function extractSessionIdsFromOutput(output: unknown): string[] | null { + if (typeof output !== "string") { + return null; + } + const ids: string[] = []; + for (const match of output.matchAll(SESSION_ID_COMMENT)) { + ids.push(match[1]); + } + return ids.length > 0 ? ids : null; +} + +function AnalyzeSessionPanel({ input }: ToolPanelProps) { + const sessionId = extractSessionId(input); + if (!sessionId) { + return null; + } + return ; +} + +function ListSessionsPanel({ output }: ToolPanelProps) { + return ; +} + +export const SESSIONS_TOOL_PANELS: Record = { + analyze_session: { Panel: AnalyzeSessionPanel }, + list_sessions: { Panel: ListSessionsPanel, hideRawOutput: true }, +}; diff --git a/frontend/features/tool-panel-registry.ts b/frontend/features/tool-panel-registry.ts new file mode 100644 index 0000000..9c21a4f --- /dev/null +++ b/frontend/features/tool-panel-registry.ts @@ -0,0 +1,19 @@ +import type { ComponentType } from "react"; +import { RETRIEVAL_TOOL_PANELS } from "./retrieval/tool-panels"; +import { SESSIONS_TOOL_PANELS } from "./sessions/tool-panels"; + +export type ToolPanelProps = { input: unknown; output: unknown }; + +export type ToolPanelEntry = { + Panel: ComponentType; + /** Suppress 's raw output block when the panel replaces it. */ + hideRawOutput?: boolean; +}; + +// Composition root: the only file that imports across feature slices. +// Each slice exports its own tool-name -> panel map; adding a feature is +// one spread here. message.tsx does a lookup — never per-tool branching. +export const TOOL_PANELS: Record = { + ...SESSIONS_TOOL_PANELS, + ...RETRIEVAL_TOOL_PANELS, +}; diff --git a/frontend/lib/extract-session-id.ts b/frontend/lib/extract-session-id.ts new file mode 100644 index 0000000..650e560 --- /dev/null +++ b/frontend/lib/extract-session-id.ts @@ -0,0 +1,9 @@ +export function extractSessionId(input: unknown): string | null { + if (input && typeof input === "object" && "session_id" in input) { + const value = (input as { session_id: unknown }).session_id; + if (typeof value === "string" && value.length > 0) { + return value; + } + } + return null; +}