Skip to content

feat: UI polish, conversation save/unsave, and window positioning#24

Merged
quiet-node merged 11 commits intomainfrom
worktree-happy-swinging-harbor
Apr 4, 2026
Merged

feat: UI polish, conversation save/unsave, and window positioning#24
quiet-node merged 11 commits intomainfrom
worktree-happy-swinging-harbor

Conversation

@quiet-node
Copy link
Copy Markdown
Owner

Summary

  • Header redesign: replaced "History" text+icon button with icon-only clock; added new-conversation + icon button with animated tooltips (via new Tooltip component) on all three header controls (save, new, history)
  • Save/unsave toggle: clicking the filled bookmark on a saved conversation calls delete_conversation and resets conversationId to unsaved — messages remain visible and can be re-saved
  • New-conversation flow: clicking + with unsaved messages now surfaces a SwitchConfirmation prompt (Save & Switch / Just Switch) instead of silently discarding work
  • History deletion fix: deleting the active conversation from the history panel no longer wipes the chat view — only marks it unsaved so the user can continue chatting or re-save
  • Window positioning: ask bar spawns at top-center on activation (below menu bar + margin) instead of bottom-center
  • Layout fix: max-h-[600px] overflow-hidden moved to always-present class to prevent content overflow during AnimatePresence exit animations

Test plan

  • All 257 frontend + 78 backend tests pass (bun run test:all)
  • 100% coverage maintained across lines, functions, branches, statements
  • bun run validate-build passes with zero warnings or errors
  • Tooltip appears below each header button on hover; does not clip at window edge
  • Clicking + with unsaved messages shows confirmation; "Just Switch" clears; "Save & Switch" saves then clears
  • Clicking the filled bookmark unsaves the conversation (button reverts to empty, can re-save)
  • Deleting active conversation from history panel keeps messages visible and marks as unsaved
  • Ask bar spawns near top-center of screen on activation

🤖 Generated with Claude Code

quiet-node and others added 11 commits April 3, 2026 18:58
…animated tooltips

- Move "New conversation" out of the history panel and into the header
  bar as an icon-only + button, next to the save bookmark
- Replace the text+icon History button with an icon-only button to match
- Add an animated Tooltip component (portal-based, Framer Motion entry/exit)
  with viewport-edge clamping and arrow-offset tracking so tooltips never
  clip at the window boundary; labels: "Save conversation", "New conversation",
  "Conversation history"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
When the + button is clicked with an unsaved conversation active,
open the history dropdown and surface a SwitchConfirmation prompt
("Save & Switch" / "Just Switch") instead of silently discarding
the current session. Direct reset still happens immediately when
the conversation is already saved or has no messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Extract a shared resetForNewConversation() helper called by all three
new-conversation paths (direct reset, Just Switch, Save & Switch).

The helper mirrors what replayEntranceAnimation already does for the
anchor-mode resize state:
- isPreExpandedRef.current = false  — unblocks the ResizeObserver in
  anchor mode so it can call set_window_frame with the ask-bar height
- outerContainerRef.current.style.minHeight = ''  — removes the inline
  CSS constraint that kept the outer container locked at chat height

Without these two resets the window stayed at full chat-mode height
after creating a new conversation in any session launched above a
text-selection anchor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…sition

After dragging a window that was launched in anchor mode (above a text
selection), the mouseup handler was clearing windowAnchorRef and
isPreExpandedRef but leaving outerContainerRef.current.style.minHeight
set to the height the window had when it was last anchored.

That stale minHeight kept the outer container locked at the pre-drag
height, which imposed a floor on how high the window could be dragged
and caused a snap/jump when the mouse was released near the screen top.

Fix: also clear style.minHeight in the mouseup handler, matching what
replayEntranceAnimation and requestHideOverlay already do.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ely reposition"

This reverts commit c31f8d1.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
When isChatMode flipped to false, max-h-[600px] was immediately removed
from the container class — but ConversationView was still in the DOM for
its 200ms opacity exit animation. A long conversation's natural height
exceeds 600px, so without the cap the container expanded, setSize() grew
the window, and the content visually dragged down before snapping back.

Extracting max-h-[600px] and overflow-hidden to the static class (always
present) keeps the height capped throughout the exit animation. The ask
bar never approaches 600px so this has no effect on ask-bar mode layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Clicking the filled bookmark on a saved conversation now calls
delete_conversation + clears conversationId, reverting to the unsaved
state without touching in-memory messages. Button label and aria-label
switch to "Remove from history" when saved and back on unsave. 100%
coverage maintained across all test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Deleting the currently-loaded conversation from the history panel no
longer calls reset() (which cleared messages and reverted to ask-bar
mode). It now only calls resetHistory() so conversationId is cleared
(isSaved → false, save button re-enabled) while messages and chat view
remain fully intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Changed the no-selection default spawn position from bottom-center
(screen_height - margin) to top-center (menu_bar_height + margin).
The conversation now grows downward from the top of the screen.
Updated three backend tests to reflect the new expected coordinates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Update two Rust test assertions in context.rs to include the +120.0
  offset added in 46fc91f (tests were asserting 40.0 but function
  returns 160.0, causing CI failure)
- Add data-history-toggle to the new-conversation (+) button so the
  click-outside mousedown handler doesn't race-close the history panel
  that handleNewConversation opens for unsaved conversations
- Correct stale doc comment on handleDeleteConversation: messages are
  kept visible (only resetHistory is called, not reset)
- Clarify useConversationHistory.reset() comment to document both the
  paired usage (full session reset) and the standalone usage (mark
  unsaved while keeping messages, e.g. after deletion from history)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node merged commit 0789f83 into main Apr 4, 2026
0 of 3 checks passed
@quiet-node quiet-node deleted the worktree-happy-swinging-harbor branch April 4, 2026 02:16
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: header UI polish — new conversation button, icon-only history, animated tooltips

- Move "New conversation" out of the history panel and into the header
  bar as an icon-only + button, next to the save bookmark
- Replace the text+icon History button with an icon-only button to match
- Add an animated Tooltip component (portal-based, Framer Motion entry/exit)
  with viewport-edge clamping and arrow-offset tracking so tooltips never
  clip at the window boundary; labels: "Save conversation", "New conversation",
  "Conversation history"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: prompt to save before starting new conversation when unsaved

When the + button is clicked with an unsaved conversation active,
open the history dropdown and surface a SwitchConfirmation prompt
("Save & Switch" / "Just Switch") instead of silently discarding
the current session. Direct reset still happens immediately when
the conversation is already saved or has no messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset window to ask-bar height when starting a new conversation

Extract a shared resetForNewConversation() helper called by all three
new-conversation paths (direct reset, Just Switch, Save & Switch).

The helper mirrors what replayEntranceAnimation already does for the
anchor-mode resize state:
- isPreExpandedRef.current = false  — unblocks the ResizeObserver in
  anchor mode so it can call set_window_frame with the ask-bar height
- outerContainerRef.current.style.minHeight = ''  — removes the inline
  CSS constraint that kept the outer container locked at chat height

Without these two resets the window stayed at full chat-mode height
after creating a new conversation in any session launched above a
text-selection anchor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: clear minHeight constraint on drag-end so window can freely reposition

After dragging a window that was launched in anchor mode (above a text
selection), the mouseup handler was clearing windowAnchorRef and
isPreExpandedRef but leaving outerContainerRef.current.style.minHeight
set to the height the window had when it was last anchored.

That stale minHeight kept the outer container locked at the pre-drag
height, which imposed a floor on how high the window could be dragged
and caused a snap/jump when the mouse was released near the screen top.

Fix: also clear style.minHeight in the mouseup handler, matching what
replayEntranceAnimation and requestHideOverlay already do.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* Revert "fix: clear minHeight constraint on drag-end so window can freely reposition"

This reverts commit c31f8d1.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep max-h-[600px] and overflow-hidden always on morphing container

When isChatMode flipped to false, max-h-[600px] was immediately removed
from the container class — but ConversationView was still in the DOM for
its 200ms opacity exit animation. A long conversation's natural height
exceeds 600px, so without the cap the container expanded, setSize() grew
the window, and the content visually dragged down before snapping back.

Extracting max-h-[600px] and overflow-hidden to the static class (always
present) keeps the height capped throughout the exit animation. The ask
bar never approaches 600px so this has no effect on ask-bar mode layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: toggle save button to unsave active conversation from history

Clicking the filled bookmark on a saved conversation now calls
delete_conversation + clears conversationId, reverting to the unsaved
state without touching in-memory messages. Button label and aria-label
switch to "Remove from history" when saved and back on unsave. 100%
coverage maintained across all test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep active conversation visible when deleted from history panel

Deleting the currently-loaded conversation from the history panel no
longer calls reset() (which cleared messages and reverted to ask-bar
mode). It now only calls resetHistory() so conversationId is cleared
(isSaved → false, save button re-enabled) while messages and chat view
remain fully intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: spawn ask bar at top-center instead of bottom-center

Changed the no-selection default spawn position from bottom-center
(screen_height - margin) to top-center (menu_bar_height + margin).
The conversation now grows downward from the top of the screen.
Updated three backend tests to reflect the new expected coordinates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: adjust overlay positioning to account for additional vertical space

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: address pre-PR review findings

- Update two Rust test assertions in context.rs to include the +120.0
  offset added in 46fc91f (tests were asserting 40.0 but function
  returns 160.0, causing CI failure)
- Add data-history-toggle to the new-conversation (+) button so the
  click-outside mousedown handler doesn't race-close the history panel
  that handleNewConversation opens for unsaved conversations
- Correct stale doc comment on handleDeleteConversation: messages are
  kept visible (only resetHistory is called, not reset)
- Clarify useConversationHistory.reset() comment to document both the
  paired usage (full session reset) and the standalone usage (mark
  unsaved while keeping messages, e.g. after deletion from history)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: header UI polish — new conversation button, icon-only history, animated tooltips

- Move "New conversation" out of the history panel and into the header
  bar as an icon-only + button, next to the save bookmark
- Replace the text+icon History button with an icon-only button to match
- Add an animated Tooltip component (portal-based, Framer Motion entry/exit)
  with viewport-edge clamping and arrow-offset tracking so tooltips never
  clip at the window boundary; labels: "Save conversation", "New conversation",
  "Conversation history"

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: prompt to save before starting new conversation when unsaved

When the + button is clicked with an unsaved conversation active,
open the history dropdown and surface a SwitchConfirmation prompt
("Save & Switch" / "Just Switch") instead of silently discarding
the current session. Direct reset still happens immediately when
the conversation is already saved or has no messages.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset window to ask-bar height when starting a new conversation

Extract a shared resetForNewConversation() helper called by all three
new-conversation paths (direct reset, Just Switch, Save & Switch).

The helper mirrors what replayEntranceAnimation already does for the
anchor-mode resize state:
- isPreExpandedRef.current = false  — unblocks the ResizeObserver in
  anchor mode so it can call set_window_frame with the ask-bar height
- outerContainerRef.current.style.minHeight = ''  — removes the inline
  CSS constraint that kept the outer container locked at chat height

Without these two resets the window stayed at full chat-mode height
after creating a new conversation in any session launched above a
text-selection anchor.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: clear minHeight constraint on drag-end so window can freely reposition

After dragging a window that was launched in anchor mode (above a text
selection), the mouseup handler was clearing windowAnchorRef and
isPreExpandedRef but leaving outerContainerRef.current.style.minHeight
set to the height the window had when it was last anchored.

That stale minHeight kept the outer container locked at the pre-drag
height, which imposed a floor on how high the window could be dragged
and caused a snap/jump when the mouse was released near the screen top.

Fix: also clear style.minHeight in the mouseup handler, matching what
replayEntranceAnimation and requestHideOverlay already do.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* Revert "fix: clear minHeight constraint on drag-end so window can freely reposition"

This reverts commit c31f8d1.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep max-h-[600px] and overflow-hidden always on morphing container

When isChatMode flipped to false, max-h-[600px] was immediately removed
from the container class — but ConversationView was still in the DOM for
its 200ms opacity exit animation. A long conversation's natural height
exceeds 600px, so without the cap the container expanded, setSize() grew
the window, and the content visually dragged down before snapping back.

Extracting max-h-[600px] and overflow-hidden to the static class (always
present) keeps the height capped throughout the exit animation. The ask
bar never approaches 600px so this has no effect on ask-bar mode layout.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: toggle save button to unsave active conversation from history

Clicking the filled bookmark on a saved conversation now calls
delete_conversation + clears conversationId, reverting to the unsaved
state without touching in-memory messages. Button label and aria-label
switch to "Remove from history" when saved and back on unsave. 100%
coverage maintained across all test files.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep active conversation visible when deleted from history panel

Deleting the currently-loaded conversation from the history panel no
longer calls reset() (which cleared messages and reverted to ask-bar
mode). It now only calls resetHistory() so conversationId is cleared
(isSaved → false, save button re-enabled) while messages and chat view
remain fully intact.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: spawn ask bar at top-center instead of bottom-center

Changed the no-selection default spawn position from bottom-center
(screen_height - margin) to top-center (menu_bar_height + margin).
The conversation now grows downward from the top of the screen.
Updated three backend tests to reflect the new expected coordinates.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: adjust overlay positioning to account for additional vertical space

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: address pre-PR review findings

- Update two Rust test assertions in context.rs to include the +120.0
  offset added in 46fc91f (tests were asserting 40.0 but function
  returns 160.0, causing CI failure)
- Add data-history-toggle to the new-conversation (+) button so the
  click-outside mousedown handler doesn't race-close the history panel
  that handleNewConversation opens for unsaved conversations
- Correct stale doc comment on handleDeleteConversation: messages are
  kept visible (only resetHistory is called, not reset)
- Clarify useConversationHistory.reset() comment to document both the
  paired usage (full session reset) and the standalone usage (mark
  unsaved while keeping messages, e.g. after deletion from history)

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 11, 2026
* feat: header UI polish — new conversation button, icon-only history, animated tooltips

- Move "New conversation" out of the history panel and into the header
  bar as an icon-only + button, next to the save bookmark
- Replace the text+icon History button with an icon-only button to match
- Add an animated Tooltip component (portal-based, Framer Motion entry/exit)
  with viewport-edge clamping and arrow-offset tracking so tooltips never
  clip at the window boundary; labels: "Save conversation", "New conversation",
  "Conversation history"

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: prompt to save before starting new conversation when unsaved

When the + button is clicked with an unsaved conversation active,
open the history dropdown and surface a SwitchConfirmation prompt
("Save & Switch" / "Just Switch") instead of silently discarding
the current session. Direct reset still happens immediately when
the conversation is already saved or has no messages.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset window to ask-bar height when starting a new conversation

Extract a shared resetForNewConversation() helper called by all three
new-conversation paths (direct reset, Just Switch, Save & Switch).

The helper mirrors what replayEntranceAnimation already does for the
anchor-mode resize state:
- isPreExpandedRef.current = false  — unblocks the ResizeObserver in
  anchor mode so it can call set_window_frame with the ask-bar height
- outerContainerRef.current.style.minHeight = ''  — removes the inline
  CSS constraint that kept the outer container locked at chat height

Without these two resets the window stayed at full chat-mode height
after creating a new conversation in any session launched above a
text-selection anchor.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: clear minHeight constraint on drag-end so window can freely reposition

After dragging a window that was launched in anchor mode (above a text
selection), the mouseup handler was clearing windowAnchorRef and
isPreExpandedRef but leaving outerContainerRef.current.style.minHeight
set to the height the window had when it was last anchored.

That stale minHeight kept the outer container locked at the pre-drag
height, which imposed a floor on how high the window could be dragged
and caused a snap/jump when the mouse was released near the screen top.

Fix: also clear style.minHeight in the mouseup handler, matching what
replayEntranceAnimation and requestHideOverlay already do.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* Revert "fix: clear minHeight constraint on drag-end so window can freely reposition"

This reverts commit c31f8d1.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep max-h-[600px] and overflow-hidden always on morphing container

When isChatMode flipped to false, max-h-[600px] was immediately removed
from the container class — but ConversationView was still in the DOM for
its 200ms opacity exit animation. A long conversation's natural height
exceeds 600px, so without the cap the container expanded, setSize() grew
the window, and the content visually dragged down before snapping back.

Extracting max-h-[600px] and overflow-hidden to the static class (always
present) keeps the height capped throughout the exit animation. The ask
bar never approaches 600px so this has no effect on ask-bar mode layout.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: toggle save button to unsave active conversation from history

Clicking the filled bookmark on a saved conversation now calls
delete_conversation + clears conversationId, reverting to the unsaved
state without touching in-memory messages. Button label and aria-label
switch to "Remove from history" when saved and back on unsave. 100%
coverage maintained across all test files.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: keep active conversation visible when deleted from history panel

Deleting the currently-loaded conversation from the history panel no
longer calls reset() (which cleared messages and reverted to ask-bar
mode). It now only calls resetHistory() so conversationId is cleared
(isSaved → false, save button re-enabled) while messages and chat view
remain fully intact.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: spawn ask bar at top-center instead of bottom-center

Changed the no-selection default spawn position from bottom-center
(screen_height - margin) to top-center (menu_bar_height + margin).
The conversation now grows downward from the top of the screen.
Updated three backend tests to reflect the new expected coordinates.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: adjust overlay positioning to account for additional vertical space

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: address pre-PR review findings

- Update two Rust test assertions in context.rs to include the +120.0
  offset added in 46fc91f (tests were asserting 40.0 but function
  returns 160.0, causing CI failure)
- Add data-history-toggle to the new-conversation (+) button so the
  click-outside mousedown handler doesn't race-close the history panel
  that handleNewConversation opens for unsaved conversations
- Correct stale doc comment on handleDeleteConversation: messages are
  kept visible (only resetHistory is called, not reset)
- Clarify useConversationHistory.reset() comment to document both the
  paired usage (full session reset) and the standalone usage (mark
  unsaved while keeping messages, e.g. after deletion from history)

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
This was referenced Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant