Skip to content

Commit 520686b

Browse files
committed
feat(session-ui): comprehensive chat session UI overhaul
## Context The session UI had severe UX regressions from a prior incomplete overhaul (PR #4). Thinking blocks rendered as red JSON error cards, HITL approval buttons were non-functional, the transcript used a flat layout with no visual distinction between roles, and the sidebar lacked temporal grouping. This commit addresses all outstanding issues and applies benchmark patterns from open-webui and agent-chat-ui reference codebases. ## Changes ### Transcript (`transcript.tsx`) - **Message layout redesign**: User messages now right-aligned with `rounded-2xl` pill background; assistant messages left-aligned with compact indigo avatar; system messages compact and muted with smaller amber avatar - **Streaming indicator**: Staggered pulse dots `ThinkingDots` component with 150ms delays replacing single ping animation; indigo blinking cursor bar for active streaming content - **Entry animations**: `animate-in fade-in slide-in-from-bottom-2` on all message types for smooth appearance - **Empty states**: Centered Cpu icon with descriptive text; loading state uses staggered dots animation - **sessionKey prop**: Threaded through to EventRenderer and HitlApprovalCard to enable approval mutation wiring - Removed unused `User` icon import ### HITL Approval Card (`hitl-approval-card.tsx`) - **Functional Approve/Reject buttons**: Wired to `sessions.resolveApproval` tRPC mutation with Loader2 spinner during pending state - **Optimistic local state**: `localResolution` prevents stale UI after action - **Error display**: Inline mutation error message - **Animated pending indicator**: Ping dot with "Pending" label replacing static "Waiting for user" text - Added `useMutation`, `useQueryClient`, `useTRPC` imports; component now accepts optional `sessionKey` prop ### Content Normalizer (`normalize-content.ts`) - **Object-format thinking recognition**: Added handler for `{type: "thinking", thinking: "..."}` and `{type: "reasoning", ...}` payloads that were previously falling through to the unknown event type - These now render as purple "Agent Reasoning" cards with token estimates instead of raw JSON in an unknown payload card ### Gateway Client (`client.ts`) - **`execApprovalsResolve()`**: New method calling `exec.approvals.resolve` RPC with `sessionKey` and `resolution` parameters ### tRPC Sessions Router (`sessions.ts`) - **`resolveApproval` mutation**: New procedure with Zod-validated input (`sessionKey: string`, `resolution: "approved" | "rejected"`) proxying to `gateway.execApprovalsResolve()` ### Session Workspace (`session-workspace.tsx`) - **Header metadata**: Agent Cpu icon, model/thinking/token count badges, `formatTokens()` helper for compact display (e.g., "12.2k") - **Connection status**: Live indicator with Streaming (indigo ping dot) / Live (Wifi icon, emerald) / Polling (WifiOff, muted) states - **Context panel persistence**: Toggle state saved to `localStorage("claw-dash:context-panel")` with SSR-safe initializer - **sessionKey threading**: Passed to Transcript component ### Session Sidebar (`session-sidebar.tsx`) - **Time grouping**: Sessions grouped into Today/Yesterday/This Week/Older with `getTimeGroup()` helper using start-of-day calculations - **Fallback titles**: `generateFallbackTitle()` extracts agent name and formats timestamp from colon-delimited session keys - **Active state**: Indigo left border (`border-l-2 border-l-indigo-500`) with `ring-1 ring-zinc-700/50` highlight ### Deep Thought Block (`deep-thought-block.tsx`) - CSS grid `0fr → 1fr` expand animation replacing display toggle - Token count estimate (`Math.ceil(rawText.length / 4)`) - Purple theming with Brain icon; rotating chevron indicator - `max-h-96 overflow-y-auto` on expanded content ### Tool Execution Card (`tool-execution-card.tsx`) - **`TruncatedOutput` component**: 8-line truncation with gradient fade overlay and "Show N more lines" / "Show less" toggle - **Pending state**: `Loader2 animate-spin` with amber "running" label when `isPending = !isResult && !payload.result` - CSS grid expand animation; status icons (CheckCircle2/XCircle/Loader2) ### Unknown Payload Card (`unknown-payload-card.tsx`) - Changed from alarming red error styling (bg-red-950, AlertTriangle) to neutral zinc collapsible card with FileJson2 icon - CSS grid animation; CodeBlock for syntax-highlighted JSON ### Composer (`composer.tsx`) - **Inline errors**: `fileError` and `mutationError` state with auto-dismiss amber/red banners replacing browser `alert()` calls - **Drag-drop zone**: `isDragOver` state with indigo ring visual feedback - **Auto-resize textarea**: `useCallback` + `useEffect` pattern - **Compact action bar**: Model selector dropdown, thinking level pill buttons, Send/Stop with icon + spinner states ### Pre-Session Page (`page.tsx`) - **Configuration grid**: 2x2 grid for Agent/Model/Workflow/Skills selects with shared `selectClass` for consistent styling - **File handling**: `addFiles()` with 20MB limit and 5-file cap validation, inline amber error banner, drag-drop zone - **Auto-resize textarea**: Matching composer pattern - **Action bar**: Attachment button, thinking level pills, Start Session button with Loader2 spinner ## New Files - **`docs/session_frontend_prompt.md`**: Originating spec prompt for the session UI overhaul, documenting the role definition, known issues, execution workflow phases, and benchmark research requirements ## Testing/Verification - `npm run lint` — zero errors, zero warnings - `npm run build` — compiled successfully, all routes generated - Live browser audit via Chrome extension at localhost:3939/sessions: verified message layout, thinking block rendering, collapsible cards, context panel toggle, sidebar grouping, and composer functionality ## Related - Follows PR #4 (001-session-ui-overhaul) which left several items incomplete - Resolves: broken HITL buttons, red unknown payload cards, flat transcript layout, missing thinking block recognition for object payloads
1 parent ea8e1ea commit 520686b

File tree

13 files changed

+912
-379
lines changed

13 files changed

+912
-379
lines changed

docs/session_frontend_prompt.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
Below is the context of the chat session aspect of the dashboard.
3+
4+
Originating prompt:
5+
6+
```md
7+
Role & Objective
8+
You are an expert Frontend Architect, UX Researcher, and Next.js Engineer. Your objective is to create a comprehensive, world-class documentation suite for overhauling and perfecting the Claw Dash chat session UI. You must design an interface that serves as a premium, highly functional command center for agent orchestration.
9+
Context & Known Issues
10+
The current chat session UI has severe UX regressions and missing features outlined in ui-audit-report.md and docs/plans/2026-02-21-claw-dash-post-audit-improvements.md. Key known issues include:
11+
* Missing Streaming: The agent does not show real-time streaming; the UI waits for final payloads instead of optimistic, live rendering.
12+
* Broken Session Metadata: The sidebar shows unparsed strings instead of human-readable conversation titles, and the active header displays raw database IDs.
13+
* Unhandled Payloads: Expected events like thinking blocks render as glaring red JSON error boundaries instead of polished accordion components.
14+
* Tag Bleed: Internal routing tags (e.g., [[reply_to_current]]) bleed into the user-facing message feed.
15+
* Outdated Composer: The active chat input lacks the robust model selection, thinking level toggles, and attachment support recently added to the pre-session layout.
16+
Execution Workflow
17+
Phase 1: Live Application Audit & Walkthrough
18+
1. Launch the local development server by running npm run dev inside the claw-dash directory.
19+
2. Utilize your browser interaction capabilities (via the Claude Chrome extension or browser tools) to navigate to http://localhost:3939/sessions.
20+
3. Conduct a full, interactive user journey. Start a new session, send messages, and observe the UI's behavior.
21+
4. Verify the known issues listed above, establish a baseline context for the current UX, and identify any additional discrepancies, layout shifts, or areas lacking premium interaction design.
22+
Phase 2: Open Source Benchmark Research
23+
To ensure the proposed design represents the absolute state-of-the-art in AI chat interfaces, comprehensively examine the frontend codebases, component architectures, and styling layouts of the following local reference repositories:
24+
* /Users/matthewmaggio/open-webui
25+
* /Users/matthewmaggio/anything-llm
26+
* /Users/matthewmaggio/agent-chat-ui Extract layout paradigms, streaming implementations, and styling best practices that can be adapted to Claw Dash's unique orchestration requirements.
27+
Phase 3: Investigation & Proposal Generation
28+
Leverage your complete arsenal of tools, knowledge, and skills to propose optimal architectural fixes and UX improvements. You have access to:
29+
* Tools: context7, tavily, shadcn ui
30+
* Skills/Plugins: frontend design, ralph loop Note: Use your best judgment on when and how to deploy these resources based on your dynamic findings. Do not wait for explicit step-by-step tool instructions.
31+
Phase 4: Comprehensive Documentation Output
32+
Synthesize your live audit, benchmark research, and architectural investigation into a definitive documentation suite.
33+
* Formulate the documentation in any way you see fit.
34+
* You are required to dynamically create directories and multiple markdown files (e.g., splitting into component specs, state management for streaming, styling guidelines, etc.) if it best serves the comprehensiveness and readability of the plan.
35+
* The final output must leave no ambiguity for the implementing engineer on how to transform the Claw Dash session workspace into a flawless, premium product.
36+
37+
```

src/app/sessions/page.tsx

Lines changed: 148 additions & 76 deletions
Large diffs are not rendered by default.

src/components/sessions/composer.tsx

Lines changed: 108 additions & 59 deletions
Large diffs are not rendered by default.
Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
11
import * as React from "react";
2-
import { Brain, ChevronRight, ChevronDown } from "lucide-react";
2+
import { Brain, ChevronRight } from "lucide-react";
33
import { ThoughtPayload } from "@/lib/normalize-content";
44

5-
65
interface Props {
76
payload: ThoughtPayload;
87
}
98

109
export function DeepThoughtBlock({ payload }: Props) {
1110
const [isOpen, setIsOpen] = React.useState(false);
11+
const estimatedTokens = payload.tokenCountEstimate ?? Math.ceil(payload.rawText.length / 4);
1212

1313
return (
14-
<div className="my-3 rounded-md border border-zinc-800/60 bg-zinc-950/30 overflow-hidden">
14+
<div className="my-3 rounded-md border border-purple-900/30 bg-purple-950/10 overflow-hidden">
1515
<button
1616
onClick={() => setIsOpen(!isOpen)}
17-
className="flex w-full items-center justify-between px-4 py-2 hover:bg-zinc-900/50 transition-colors text-left"
17+
className="flex w-full items-center justify-between px-4 py-2.5 hover:bg-purple-950/20 transition-colors text-left"
1818
>
1919
<div className="flex items-center gap-2">
20-
<Brain className="h-4 w-4 text-purple-400" />
21-
<span className="text-xs font-mono text-purple-400">Agent Reasoning</span>
22-
{payload.tokenCountEstimate && (
23-
<span className="text-[10px] text-zinc-600 ml-2">~{payload.tokenCountEstimate} tokens</span>
24-
)}
20+
<Brain className="h-4 w-4 text-purple-400/70" />
21+
<span className="text-xs font-medium text-purple-300/80">Agent Reasoning</span>
22+
<span className="text-[10px] text-zinc-600 tabular-nums">~{estimatedTokens.toLocaleString()} tokens</span>
2523
</div>
26-
<div className="text-zinc-500">
27-
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
24+
<div className="text-zinc-500 transition-transform duration-200" style={{ transform: isOpen ? "rotate(90deg)" : "rotate(0deg)" }}>
25+
<ChevronRight className="h-4 w-4" />
2826
</div>
2927
</button>
30-
31-
{isOpen && (
32-
<div className="px-4 py-3 bg-zinc-950/60 border-t border-zinc-800/60 text-sm text-zinc-400 italic leading-relaxed break-words whitespace-pre-wrap">
33-
{payload.rawText}
28+
<div
29+
className="grid transition-all duration-200 ease-in-out"
30+
style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
31+
>
32+
<div className="overflow-hidden">
33+
<div className="px-4 py-3 border-t border-purple-900/20 text-sm text-zinc-400/90 italic leading-relaxed break-words whitespace-pre-wrap max-h-96 overflow-y-auto">
34+
{payload.rawText}
35+
</div>
3436
</div>
35-
)}
37+
</div>
3638
</div>
3739
);
3840
}

src/components/sessions/orchestration/hitl-approval-card.tsx

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,105 @@
1+
"use client";
2+
13
import * as React from "react";
2-
import { ShieldAlert, CheckCircle2, XCircle } from "lucide-react";
4+
import { ShieldAlert, CheckCircle2, XCircle, Loader2 } from "lucide-react";
35
import { ApprovalPayload } from "@/lib/normalize-content";
6+
import { useMutation, useQueryClient } from "@tanstack/react-query";
7+
import { useTRPC } from "@/lib/trpc/react";
48

59
interface Props {
610
payload: ApprovalPayload;
11+
sessionKey?: string;
712
}
813

9-
export function HitlApprovalCard({ payload }: Props) {
10-
const isResolved = !!payload.resolution;
11-
const isApproved = payload.resolution === "approved";
14+
export function HitlApprovalCard({ payload, sessionKey }: Props) {
15+
const [localResolution, setLocalResolution] = React.useState<"approved" | "rejected" | undefined>(undefined);
16+
const trpc = useTRPC();
17+
const queryClient = useQueryClient();
18+
19+
const resolution = localResolution ?? payload.resolution;
20+
const isResolved = !!resolution;
21+
const isApproved = resolution === "approved";
22+
23+
const mutation = useMutation(
24+
trpc.sessions.resolveApproval.mutationOptions({
25+
onSuccess: (_data, variables) => {
26+
setLocalResolution(variables.resolution);
27+
queryClient.invalidateQueries({ queryKey: trpc.sessions.history.queryKey() });
28+
},
29+
})
30+
);
31+
32+
const handleResolve = (decision: "approved" | "rejected") => {
33+
if (!sessionKey || mutation.isPending) return;
34+
mutation.mutate({ sessionKey, resolution: decision });
35+
};
1236

1337
return (
14-
<div className="my-4 rounded-lg border border-amber-900/50 bg-amber-950/20 overflow-hidden shadow-sm">
15-
<div className="flex items-center justify-between px-4 py-3 bg-amber-950/40 border-b border-amber-900/50">
16-
<div className="flex items-center gap-3">
17-
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-amber-500/20 text-amber-500 ring-1 ring-inset ring-amber-500/30">
18-
<ShieldAlert className="h-4 w-4" />
19-
</div>
20-
<div>
21-
<div className="text-sm font-medium text-amber-500">
22-
Approval Required
23-
</div>
38+
<div className="my-3 rounded-lg border border-amber-900/40 bg-amber-950/10 overflow-hidden">
39+
<div className="flex items-center justify-between px-4 py-2.5 bg-amber-950/20 border-b border-amber-900/30">
40+
<div className="flex items-center gap-2.5">
41+
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-amber-500/15 text-amber-500 ring-1 ring-inset ring-amber-500/20">
42+
<ShieldAlert className="h-3.5 w-3.5" />
2443
</div>
44+
<span className="text-xs font-medium text-amber-400">
45+
Approval Required
46+
</span>
2547
</div>
2648
<div>
2749
{isResolved ? (
2850
isApproved ? (
29-
<div className="flex items-center gap-2 text-xs text-emerald-500 bg-emerald-500/10 px-2.5 py-1 rounded-full border border-emerald-500/20">
51+
<div className="flex items-center gap-1.5 text-[10px] text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20 font-medium">
3052
<CheckCircle2 className="h-3 w-3" /> Approved
3153
</div>
3254
) : (
33-
<div className="flex items-center gap-2 text-xs text-red-500 bg-red-500/10 px-2.5 py-1 rounded-full border border-red-500/20">
55+
<div className="flex items-center gap-1.5 text-[10px] text-red-500 bg-red-500/10 px-2 py-0.5 rounded-full border border-red-500/20 font-medium">
3456
<XCircle className="h-3 w-3" /> Rejected
3557
</div>
3658
)
3759
) : (
38-
<div className="flex items-center gap-2 text-xs text-amber-500 bg-amber-500/10 px-2.5 py-1 rounded-full border border-amber-500/20 animate-pulse">
39-
Waiting for user
60+
<div className="flex items-center gap-1.5 text-[10px] text-amber-500/80 font-medium">
61+
<span className="relative flex h-1.5 w-1.5">
62+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-500 opacity-75" />
63+
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-amber-500" />
64+
</span>
65+
Pending
4066
</div>
4167
)}
4268
</div>
4369
</div>
4470

45-
<div className="px-4 py-3 text-sm text-zinc-300">
71+
<div className="px-4 py-2.5 text-sm text-zinc-300">
4672
{payload.actionDescription}
4773
</div>
4874

49-
{!isResolved && (
50-
<div className="px-4 py-3 bg-black/20 border-t border-amber-900/30 flex gap-3">
51-
<button className="flex-1 rounded-md bg-emerald-600 hover:bg-emerald-500 text-white py-1.5 text-xs font-medium transition-colors">
52-
Approve
75+
{mutation.error && (
76+
<div className="px-4 pb-2 text-xs text-red-400">
77+
{mutation.error.message}
78+
</div>
79+
)}
80+
81+
{!isResolved && sessionKey && (
82+
<div className="px-4 py-2.5 bg-black/20 border-t border-amber-900/20 flex gap-2.5">
83+
<button
84+
onClick={() => handleResolve("approved")}
85+
disabled={mutation.isPending}
86+
className="flex-1 flex items-center justify-center gap-1.5 rounded-md bg-emerald-600/90 hover:bg-emerald-500 disabled:opacity-50 text-white py-1.5 text-xs font-medium transition-colors"
87+
>
88+
{mutation.isPending ? (
89+
<Loader2 className="h-3 w-3 animate-spin" />
90+
) : (
91+
<>
92+
<CheckCircle2 className="h-3 w-3" />
93+
Approve
94+
</>
95+
)}
5396
</button>
54-
<button className="flex-1 rounded-md border border-red-900/50 hover:bg-red-950 text-red-500 py-1.5 text-xs font-medium transition-colors">
97+
<button
98+
onClick={() => handleResolve("rejected")}
99+
disabled={mutation.isPending}
100+
className="flex-1 flex items-center justify-center gap-1.5 rounded-md border border-red-900/40 hover:bg-red-950/50 disabled:opacity-50 text-red-400 py-1.5 text-xs font-medium transition-colors"
101+
>
102+
<XCircle className="h-3 w-3" />
55103
Reject
56104
</button>
57105
</div>

0 commit comments

Comments
 (0)