feat(ux): improve session list chronology, pinning, and compact control layout#15
feat(ux): improve session list chronology, pinning, and compact control layout#15haddadrm wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
UX-focused update to improve session discoverability and conversation usability by adding last-activity chronology, pinned sessions, richer filtering/sorting, find-in-conversation navigation, and stronger accessibility/focus handling across the app shell.
Changes:
- Add
lastUpdatedAtto sessions and use it for update detection + sorting by “last activity”. - Enhance session list with grouping, search highlighting, recency filters, pinned sessions (local storage), and keyboard navigation.
- Improve conversation view with find-in-conversation navigation, message metadata display, and expandable long tool outputs; add connection status + persisted UI preferences.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web/utils.ts | Adds localStorage helpers + URL session query param helpers for persisted UI state and deep linking. |
| web/index.css | Introduces Geist sans font usage, density-based font sizing, root sizing tweaks, and global focus-visible styling. |
| web/hooks/use-event-source.ts | Adds onOpen/onRetry hooks and improves reconnect handling behavior. |
| web/components/tool-renderers/read-renderer.tsx | Adds expandable “show all/less” for truncated file outputs. |
| web/components/tool-renderers/bash-renderer.tsx | Adds expandable “show all/less” for long bash outputs. |
| web/components/session-view.tsx | Adds find-in-conversation UI with match navigation + matched-message highlighting. |
| web/components/session-list.tsx | Adds grouping, recency filters, sort modes, pinned sessions persistence, highlight search matches, and keyboard selection behavior. |
| web/components/scroll-to-bottom-button.tsx | Improves focus styles and adds an ARIA label. |
| web/components/message-block.tsx | Adds message metadata display, improves tool result expansion behavior and focus-visible styling. |
| web/app.tsx | Persists selected project/session/sidebar/density, adds deep linking, mobile drawer behavior, connection indicator, and safer resume command building. |
| api/storage.ts | Adds lastUpdatedAt, improves project path normalization/candidate matching, and computes last activity from file mtimes. |
| api/server.ts | Updates SSE session change detection to use lastUpdatedAt, listens to session file changes, and hardens stream offset parsing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| @@ -98,23 +301,75 @@ function App() { | |||
| sessionMap.set(update.id, update); | |||
| } | |||
| return Array.from(sessionMap.values()).sort( | |||
| (a, b) => b.timestamp - a.timestamp, | |||
| (a, b) => b.lastUpdatedAt - a.lastUpdatedAt, | |||
| ); | |||
There was a problem hiding this comment.
handleSessionsUpdate sorts only by lastUpdatedAt. When multiple sessions share the same lastUpdatedAt, the ordering becomes unstable (depends on Map insertion order), which can cause UI reordering/flicker and affect the “pick first session” logic. Add a deterministic tie-breaker (e.g. timestamp then id) to keep ordering stable.
| <div className="flex h-screen bg-zinc-950 text-zinc-100"> | ||
| {!sidebarCollapsed && ( | ||
| <aside className="w-80 border-r border-zinc-800/60 flex flex-col bg-zinc-950"> | ||
| <div className="relative flex h-screen bg-zinc-950 text-zinc-100"> |
There was a problem hiding this comment.
The app container uses h-screen (100vh), but index.css sets #root to 100dvh when supported. Using h-screen here effectively bypasses the 100dvh fix on mobile browsers. Consider switching this container to h-full (and rely on #root height) to avoid mobile viewport-height issues.
| <div className="relative flex h-screen bg-zinc-950 text-zinc-100"> | |
| <div className="relative flex h-full bg-zinc-950 text-zinc-100"> |
| target instanceof HTMLSelectElement || | ||
| target instanceof HTMLButtonElement |
There was a problem hiding this comment.
handleListKeyDown bails out when the event target is a HTMLButtonElement. Since each session row is a <button>, arrow-key navigation won’t work after a user clicks a row (focus moves to the button). Consider only excluding form fields where arrow keys are meaningful (inputs/selects/textareas), or explicitly allow navigation when focus is on the session row button.
| target instanceof HTMLSelectElement || | |
| target instanceof HTMLButtonElement | |
| target instanceof HTMLSelectElement |
|
|
||
| button:focus-visible, | ||
| input:focus-visible, | ||
| select:focus-visible, |
There was a problem hiding this comment.
Global outline: none removes the default focus indicator for all buttons/inputs/selects/links. Only :focus-visible gets a custom ring, so browsers/environments without :focus-visible support (or if it’s disabled) will have no visible focus state. Consider keeping a :focus fallback (or not clearing outline globally) so keyboard users always have a focus indicator.
| button:focus-visible, | |
| input:focus-visible, | |
| select:focus-visible, | |
| button:focus, | |
| button:focus-visible, | |
| input:focus, | |
| input:focus-visible, | |
| select:focus, | |
| select:focus-visible, | |
| a:focus, |
| const sessionsWithActivity = await Promise.all( | ||
| sessions.map(async (session) => { | ||
| const filePath = await findSessionFile(session.id); | ||
| if (!filePath) { | ||
| return session; | ||
| } | ||
|
|
||
| try { | ||
| const fileStat = await stat(filePath); | ||
| return { | ||
| ...session, | ||
| lastUpdatedAt: Math.max(session.timestamp, fileStat.mtimeMs), | ||
| }; | ||
| } catch { | ||
| return session; | ||
| } | ||
| }), | ||
| ); |
There was a problem hiding this comment.
getSessions() now stat()s every session file to compute lastUpdatedAt on each call. Since this runs on every sessions SSE publish, it adds O(n) filesystem I/O per update and can become slow with many sessions. Consider caching mtimeMs (keyed by sessionId/filePath) and only restatting the changed session(s) from onSessionChange, or persisting lastUpdatedAt in the history cache so updates are incremental.
|
|
||
| if (isWindowsPath) { | ||
| const escapedPath = escapeForDoubleQuotes(projectPath); | ||
| return `cd "${escapedPath}"; claude --resume ${sessionId}`; |
There was a problem hiding this comment.
buildResumeCommand uses cd "..."; ... for Windows paths. The ; command separator isn't valid in cmd.exe, and plain cd won't switch drives (needs cd /d). Consider emitting a Windows-friendly command (e.g. using && + /d) or offering separate PowerShell vs cmd variants so the copied resume command works reliably on Windows.
| return `cd "${escapedPath}"; claude --resume ${sessionId}`; | |
| return `cd /d "${escapedPath}" && claude --resume ${sessionId}`; |
|
@copilot apply changes based on the comments in this thread |
Summary
This PR delivers a comprehensive UX pass for claude-run focused on session discoverability, chronology accuracy, accessibility, and responsive behavior.
What changed
lastUpdatedAtsession metadata in API responses.lastUpdatedAt.Updated (Newest/Oldest)andStarted (Newest/Oldest).Pinnedquick filter.show more/less) and error-first expansion behavior.Validation
pnpm buildpasses on the integrated branch after all merges.Notes