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;
+}