feat(ai): resizable panel, kept chats, optional icon animation (#111) - #157
Merged
Conversation
The last three asks from Blaze-Leo''s thread. Resizable panel. The width was hardcoded at 400px in three places that all had to agree: the panel, the padding-right the editor reserves so content reflows beside it, and the floating mode toggle that sits clear of it. App now owns one number and hands it to all three. New PanelResizeHandle drags the left edge; SplitDivider could not be reused because it resizes two in-flow panes and therefore works in ratios, while this sizes a fixed right-anchored element and works in pixels from the right edge. Arrow keys nudge it, and the value is clamped 280-900 on both read and write so a stale or hand-edited entry cannot squeeze the document to nothing. onResize fires per pointermove for layout; onCommit fires once on release, which is where the localStorage write goes. Kept chats. Closing the panel unmounts it, so the conversation used to die with it, and "new chat" discarded it outright. Chats are now stored sessions: the panel resumes the most recent one on mount, past chats are listed under a history button, and each can be deleted. Titles derive from the first user message. Growth is capped on two axes, count and total characters, evicting least-recently-updated first, because localStorage is small, shared and has no quota guarantee; the newest chat is kept even if it alone exceeds the budget, since discarding the conversation in progress is worse than briefly exceeding a self-imposed limit. Reads go through sanitizeSessions so a corrupted entry degrades to "no history" instead of breaking the panel. Writes are gated on `busy`, so a streaming reply does not hit storage on every token. Optional icon animation. Settings, AI, "Animate the AI icon" turns the title-bar shimmer off and leaves the glyph as plain text, which is what was asked for. On by default, so nothing changes unless you touch it. TitleBar holds it locally and refreshes from the settings event rather than having it threaded down from App, since nothing in between needs to know. Verified: 56 new tests, 415 total, build clean. - chatSessions.test.ts (25) covers title derivation, both eviction axes, the keep-the-newest rule, and sanitizing corrupt input - AIPanel.history.test.tsx (14) covers resume-on-mount, surviving an actual unmount/remount, switching, deleting the open chat, and not storing an empty chat just because the panel was opened - PanelResizeHandle.test.tsx (12) covers clamping at both bounds, commit exactly once on release, commit on a cancelled pointer, and cursor cleanup when unmounted mid-drag - TitleBar.aiIcon.test.tsx (5) covers both states and the live toggle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last three asks from @Blaze-Leo''s thread in #111.
1. The AI panel is resizable
The width was hardcoded at 400px in three places that all had to agree: the panel itself, the
padding-rightthe editor reserves so content reflows beside it rather than under it, and the floating mode toggle that sits clear of it. App now owns one number and hands it to all three.New
PanelResizeHandledrags the left edge.SplitDividerwas not reusable: it resizes two in-flow panes and therefore works in ratios, while this sizes afixed right-0element and works in pixels from the right edge. Arrow keys nudge it too (left widens, matching the drag direction).Clamped 280 to 900 on both read and write, so a stale or hand-edited value can never squeeze the document to nothing or push the panel off screen.
onResizefires perpointermovefor layout;onCommitfires once on release, which is where the localStorage write goes. Writing on every move would hit storage a hundred times a second.2. Chats are kept
Closing the panel unmounts it, so the conversation died with it, and "new chat" discarded it outright. Chats are now stored sessions:
Growth is capped on two axes, count (20) and total characters (200k), evicting least-recently-updated first, because localStorage is small, shared, and has no quota guarantee. The newest chat is kept even if it alone busts the budget: discarding the conversation the user is having right now is worse than briefly exceeding a self-imposed limit.
Reads go through
sanitizeSessions, so a corrupted or hand-edited entry degrades to "no history" instead of breaking the panel on render. Writes are gated onbusy, so a streaming reply does not hit storage on every token; the transition back to idle is what persists.The stored transcript is only the visible Q&A. The document is attached to the outgoing request, never to the saved chat.
3. The icon animation is optional
Settings → AI → "Animate the AI icon". Off leaves the glyph as plain text. On by default, so nothing changes unless you touch it.
TitleBar holds this locally and refreshes from the settings event rather than having it threaded down from App, since nothing on the way between needs to know about it.
Verification
bun run build(tsc + vite) cleanbun run test: 415 tests, 40 files, up from 359. 56 new.chatSessions.test.ts(25) — title derivation and truncation, both eviction axes, the keep-the-newest rule, no-empty-chats, and sanitizing corrupt inputAIPanel.history.test.tsx(14) — resume on mount, surviving an actual unmount/remount (the real bug), switching chats, deleting the open chat, dropdown dismissal, and not storing an empty chat just because the panel was openedPanelResizeHandle.test.tsx(12) — clamping at both bounds, commit exactly once on release, commit on a cancelled pointer so a lost drag still persists, ignoring a stray pointerup, and cursor cleanup when unmounted mid-dragTitleBar.aiIcon.test.tsx(5) — both states plus the live toggleOne note while writing these: my first draft of the history tests used fixture titles that disagreed with their messages. Since titles are re-derived from the first user message when a chat is committed, those got silently rewritten and the tests chased a ghost. The fixture now keeps title and text in sync, and there is a comment saying why.
Not verified here
The drag itself is exercised through synthetic pointer events in jsdom, which has no layout and no pointer capture. The arithmetic is covered, but the feel of the drag wants a real window. A Test Build is queued.
No version bump and no release in this PR.