Skip to content

feat(ai): resizable panel, kept chats, optional icon animation (#111) - #157

Merged
Razee4315 merged 1 commit into
mainfrom
feat/ai-panel-resize-chat-history
Jul 29, 2026
Merged

feat(ai): resizable panel, kept chats, optional icon animation (#111)#157
Razee4315 merged 1 commit into
mainfrom
feat/ai-panel-resize-chat-history

Conversation

@Razee4315

Copy link
Copy Markdown
Owner

The last three asks from @Blaze-Leo''s thread in #111.

1. The AI panel is resizable

the AI chat window cannot be extended horizontally

The width was hardcoded at 400px in three places that all had to agree: the panel itself, the padding-right the 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 PanelResizeHandle drags the left edge. SplitDivider was not reusable: it resizes two in-flow panes and therefore works in ratios, while this sizes a fixed right-0 element 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. onResize fires per pointermove for layout; onCommit fires 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

it would be helpful if the chats are stored somewhere and shown in a drop down menu so that opening a new chat or closing a chat doesn''t remove that from history

Closing the panel unmounts it, so the conversation died with it, and "new chat" discarded it outright. Chats are now stored sessions:

  • the panel resumes the most recent chat on mount, so close/reopen and app restarts are non-destructive
  • past chats are listed under a new history button in the header, newest first
  • each can be deleted; deleting the open one leaves an empty chat rather than a stale transcript
  • titles derive from the first user message

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 on busy, 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

could you keep a toggle for the animation on the AI icon, I personally prefer it to be plain text

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) clean
  • bun 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 input
    • AIPanel.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 opened
    • PanelResizeHandle.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-drag
    • TitleBar.aiIcon.test.tsx (5) — both states plus the live toggle

One 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.

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
@Razee4315
Razee4315 merged commit e41b958 into main Jul 29, 2026
7 checks passed
@Razee4315
Razee4315 deleted the feat/ai-panel-resize-chat-history branch July 29, 2026 23:50
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