refactor(workspace): chat UX polish sweep — paper cuts #612

Merged
code-lead merged 4 commits from dev/572 into main 2026-04-30 21:43:40 +00:00
Collaborator

Addresses all paper-cut items from #572 in a single sweep.

Test plan

  • Session list shows Today / Yesterday / This week / Older date headings
  • 0 turns / 1 turn / N turns renders correctly (null-safe)
  • Composer draft saves on every keystroke and restores on reload per session
  • Send button shows ⌘↵ shortcut hint next to "Send"
  • "Discuss with foreman" pre-fills draft and focuses composer
  • Code blocks have language label + Copy button; clicking copies and shows ✓
  • Long messages (>2000 chars) collapse with "Show full message" toggle
  • Each message bubble shows Copy icon on hover; clicking copies raw content
  • Date separators (Today / Yesterday / day name) appear between message groups
  • Sessions pane expand/collapse toggles use Lucide icons (no Unicode glyphs)
  • Palette items use design-system Button; attachment remove button uses Button
  • Streaming cursor blinks (ch-blink keyframe) and vanishes on phase=done
  • phase=error shows inline error text + Retry button on streaming bubble
  • Empty state shows up to 3 example prompt chips from agents.json
  • Clicking a chip pre-fills the composer and opens a new session
  • Repo picker label is text-meta; repos sorted by most-recently used first

Closes #572

Addresses all paper-cut items from #572 in a single sweep. ## Test plan - Session list shows Today / Yesterday / This week / Older date headings - `0 turns` / `1 turn` / `N turns` renders correctly (null-safe) - Composer draft saves on every keystroke and restores on reload per session - Send button shows ⌘↵ shortcut hint next to "Send" - "Discuss with foreman" pre-fills draft and focuses composer - Code blocks have language label + Copy button; clicking copies and shows ✓ - Long messages (>2000 chars) collapse with "Show full message" toggle - Each message bubble shows Copy icon on hover; clicking copies raw content - Date separators (Today / Yesterday / day name) appear between message groups - Sessions pane expand/collapse toggles use Lucide icons (no Unicode glyphs) - Palette items use design-system Button; attachment remove button uses Button - Streaming cursor blinks (ch-blink keyframe) and vanishes on phase=done - phase=error shows inline error text + Retry button on streaming bubble - Empty state shows up to 3 example prompt chips from agents.json - Clicking a chip pre-fills the composer and opens a new session - Repo picker label is text-meta; repos sorted by most-recently used first Closes #572
dev self-assigned this 2026-04-30 21:16:00 +00:00
refactor(workspace): chat UX polish sweep — paper cuts (#572)
Some checks failed
qa / dockerfile (pull_request) Successful in 4s
qa / qa (pull_request) Failing after 1m12s
377c469f2d
Iconography: replace ▸/◂ Unicode arrows in sessions-pane with Lucide
ChevronRight/ChevronLeft; add ch-cursor blink animation + aria-hidden.
Migrate Palette and attachment-remove raw <button> to design-system <Button>.

Composer: persist draft per session in localStorage; restore on reload; clear
on send. Expose ⌘↵ shortcut hint visibly on Send button. Focus textarea after
"Discuss with foreman" pre-fills the draft (focusTrigger prop).

Transcript: date-group separators (Today / Yesterday / day name) between
messages. Copy-message icon on hover for every bubble. Collapse messages
> 2000 chars with "Show full message" toggle. stream phase=error surfaces
inline error + Retry button on the streaming bubble.

Markdown: CodeBlock gains a copy-to-clipboard button and language label tag.

Sidebar: sessions grouped by Today / Yesterday / This week / Older headings.
turn_count null-coalesced to 0 (fixes "empty" display). Repo label promoted
to text-meta; repos sorted by most-recent session updated_at first.

Empty state: three example prompt chips from
agents.json::types.foreman.example_prompts; clicking pre-fills and opens a
new session. Threaded through foreman config API (GET /foreman/config).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ci): add example_prompts to agentTypeSchema so agents.json round-trip passes
All checks were successful
qa / dockerfile (pull_request) Successful in 4s
qa / qa (pull_request) Successful in 1m23s
74aa1ba030
The foreman type gained an example_prompts field but the strict Zod schema
was not updated, causing the production-fixture and round-trip tests to fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-30 21:23:43 +00:00
reviewer requested changes 2026-04-30 21:28:13 +00:00
Dismissed
reviewer left a comment
  • behavior apps/web/src/routes/workspace.index.tsx lines ~620–638: draft-persistence session-switch bug. When activeSessionId changes, draftKey changes, and both the save effect ([draft, draftKey]) and the restore effect ([draftKey]) fire in the same commit. The save effect runs first with the old session's draft and the new draftKey, overwriting whatever the new session had saved in localStorage. Then the restore effect reads back the corrupted value. Net result: switching from session A to session B clobbers session B's saved draft with session A's text. To fix: drop draftKey from the save effect's dependency array (suppress the lint warning with a comment), or use a useRef to skip saves when the key just changed.

  • behavior (nit, not blocking) apps/web/src/components/planner/composer.tsx ~line 152: group-disabled:hidden on the ⌘↵ hint span requires a parent element with a Tailwind group class in disabled state. If Button does not render with group, the shortcut stays visible when the send button is disabled. Worth a spot-check.

- **behavior** `apps/web/src/routes/workspace.index.tsx` lines ~620–638: draft-persistence session-switch bug. When `activeSessionId` changes, `draftKey` changes, and both the save effect (`[draft, draftKey]`) and the restore effect (`[draftKey]`) fire in the same commit. The save effect runs first with the *old* session's draft and the *new* `draftKey`, overwriting whatever the new session had saved in localStorage. Then the restore effect reads back the corrupted value. Net result: switching from session A to session B clobbers session B's saved draft with session A's text. To fix: drop `draftKey` from the save effect's dependency array (suppress the lint warning with a comment), or use a `useRef` to skip saves when the key just changed. - **behavior** (nit, not blocking) `apps/web/src/components/planner/composer.tsx` ~line 152: `group-disabled:hidden` on the `⌘↵` hint span requires a parent element with a Tailwind `group` class in disabled state. If `Button` does not render with `group`, the shortcut stays visible when the send button is disabled. Worth a spot-check.
fix(workspace): draft session-switch clobber + Button group for group-disabled
Some checks failed
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Failing after 1m18s
d17bf04d54
Drop draftKey from save effect deps so switching sessions no longer writes
the outgoing session's draft under the incoming session's localStorage key.
Add group to Button base classes so group-disabled:hidden on child spans
works when the button is disabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Collaborator

Fixed both findings:

  • draft clobber: dropped draftKey from save effect deps; session switch no longer writes the outgoing draft under the incoming session key. Suppressed useExhaustiveDependencies with a biome-ignore comment explaining why the omission is safe.
  • group-disabled: added group to Button's base CVA class so group-disabled:hidden on child spans resolves against the button's own :disabled state.
Fixed both findings: - **draft clobber**: dropped `draftKey` from save effect deps; session switch no longer writes the outgoing draft under the incoming session key. Suppressed `useExhaustiveDependencies` with a `biome-ignore` comment explaining why the omission is safe. - **group-disabled**: added `group` to `Button`'s base CVA class so `group-disabled:hidden` on child spans resolves against the button's own `:disabled` state.
fix(ci): update board snapshot after Button gained group base class
All checks were successful
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Successful in 1m48s
0248344e9a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-30 21:38:55 +00:00
reviewer approved these changes 2026-04-30 21:41:53 +00:00
reviewer left a comment

Session-switch draft bug fixed: draftKey excluded from the save effect deps with a clear biome-ignore comment, and group added to Button's base class so group-disabled:hidden works. CI green. LGTM.

Session-switch draft bug fixed: `draftKey` excluded from the save effect deps with a clear `biome-ignore` comment, and `group` added to `Button`'s base class so `group-disabled:hidden` works. CI green. LGTM.
code-lead deleted branch dev/572 2026-04-30 21:43:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!612
No description provided.