feat(web): keep unsent drafts one click away in the sidebar - #5777
Conversation
📝 WalkthroughWalkthroughThe change preserves composer drafts containing user content, prevents new-thread actions from reusing them, and adds scoped draft display, navigation, discard actions, and empty-state handling to the sidebar. ChangesComposer draft lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Sidebar
participant composerDraftStore
participant DraftRoute
Sidebar->>composerDraftStore: Read scoped drafts with user content
composerDraftStore-->>Sidebar: Return draft context and preview data
Sidebar->>DraftRoute: Navigate to /draft/$draftId
DraftRoute->>composerDraftStore: Load selected draft
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/web/src/composerDraftStore.test.ts (1)
852-867: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the persistence filter.
This test proves the in-memory survival of an invested draft. The reload half is untested.
partializeComposerDraftStoreStatenow drops unmapped sessions that are neither promoting nor backed by persisted composer content. A test that partializes state after this remap would lock in two behaviors: the unmapped invested session survives, and an unmapped empty session does not.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/composerDraftStore.test.ts` around lines 852 - 867, Add persistence coverage to the remapping test using partializeComposerDraftStoreState: after remapping, verify the unmapped invested draft with prompt content remains in the partialized state, while an unmapped empty draft session is excluded. Reuse the existing draft/thread setup and persistence state symbols rather than changing the in-memory assertions.apps/web/src/components/Sidebar.tsx (1)
1808-1833: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the draft-visibility rule into one shared predicate.
This selector reimplements the row filter in
SidebarDraftBlockat lines 576-600. Both apply the same three rules: the session is not promoting, the composer has user content, and the project is in scope. The comment states this copy only "approximates" the block's filter. Two independent copies of one visibility rule will drift as the rule changes.Export a single predicate next to
composerDraftHasUserContentand call it from both sites. The rule is client-agnostic, sopackages/client-runtimeis also a candidate home if desktop and mobile surface draft rows later.As per coding guidelines: "shared logic belongs in
packages/client-runtimewhen appropriate".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/Sidebar.tsx` around lines 1808 - 1833, Extract the shared draft-row visibility predicate alongside composerDraftHasUserContent, preferably in packages/client-runtime, covering promotion status, user content, and project scope. Export and reuse it in both the visibleDraftSessionCount selector and SidebarDraftBlock, removing the duplicated filter logic while preserving existing behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 461-517: Update handleKeyDown for the outer draft-row button so it
only handles Enter or Space when event.currentTarget equals event.target,
ignoring keyboard events originating from the nested discard button. Preserve
the existing navigation behavior for direct row activation and leave
handleDiscard unchanged.
---
Nitpick comments:
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 1808-1833: Extract the shared draft-row visibility predicate
alongside composerDraftHasUserContent, preferably in packages/client-runtime,
covering promotion status, user content, and project scope. Export and reuse it
in both the visibleDraftSessionCount selector and SidebarDraftBlock, removing
the duplicated filter logic while preserving existing behavior.
In `@apps/web/src/composerDraftStore.test.ts`:
- Around line 852-867: Add persistence coverage to the remapping test using
partializeComposerDraftStoreState: after remapping, verify the unmapped invested
draft with prompt content remains in the partialized state, while an unmapped
empty draft session is excluded. Reuse the existing draft/thread setup and
persistence state symbols rather than changing the in-memory assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 13ae9532-d26d-4702-bc90-12e8dd57bb49
📒 Files selected for processing (4)
apps/web/src/components/Sidebar.tsxapps/web/src/composerDraftStore.test.tsapps/web/src/composerDraftStore.tsapps/web/src/hooks/useHandleNewThread.ts
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR introduces a new sidebar feature for displaying and interacting with unsent drafts, including new React components, state management for frozen draft snapshots, and changes to draft lifecycle behavior. New user-facing features with this scope warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Drafts with typed text or attachments now show as rows above the pinned block, frozen while open, gone on send/discard. New-thread surfaces always mint a fresh draft instead of resurrecting (and resetting) an invested one; the store keeps invested drafts alive unmapped when the per-project mapping moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Header reads project · env mode · branch; the model still travels with the draft, it just doesn't earn a slot in a row this small. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Env mode and branch ate the header, especially with long project names. The settings still travel with the draft; the row just stops advertising them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Keyboard activation of the discard button now discards instead of navigating (row keydown ignores events from the nested button). - navigateToDraft clears the selection anchor unconditionally so a later shift-click doesn't range from a stale anchor. - clearProjectDraftThreadId sweeps every session for the project, not just the first — invested drafts left unmapped by a remap no longer outlive project removal. Regression test added. - Rebase note: the mint-path raced-draft guard from main now only reuses a draft registered during the await, so it can't resurrect the invested draft this feature deliberately declines to reuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4f4f858 to
81a40cf
Compare
- Persist-time GC now keys off real user content, so an unmapped session whose draft holds only ambient model/mode data no longer survives as an invisible zombie. - The empty-draft reuse path re-checks the mapping and content after its await: a concurrent invocation's fresh draft can no longer be evicted by a stale continuation remapping back to the old draft. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit be716b7. Configure here.
partialize now computes kept sessions first and filters both maps with the same predicate, so a dropped session's model/mode-only composer entry can't persist forever keyed to nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What's Changed * chore: vouch gfsaaser24 by @t3dotgg in pingdotgg/t3code#5761 * chore: vouch saphid by @t3dotgg in pingdotgg/t3code#5763 * fix(server): stop Codex threads with queued follow-ups by @t3dotgg in pingdotgg/t3code#5762 * fix(web): usage page loses the cost quality panel, gains a back button by @t3dotgg in pingdotgg/t3code#5756 * feat(server): agents can now open the images you paste into chat by @t3dotgg in pingdotgg/t3code#5757 * fix(web): pinned reorder no longer reshuffles while writes land by @t3dotgg in pingdotgg/t3code#5767 * feat(web): overhaul project settings into a real settings page by @t3dotgg in pingdotgg/t3code#5768 * fix(web): usage totals no longer jump while devices report in by @t3dotgg in pingdotgg/t3code#5772 * fix(server): settle no longer leaves monitors and dev servers running by @t3dotgg in pingdotgg/t3code#5774 * feat: pick worktree or current checkout per project by @t3dotgg in pingdotgg/t3code#5766 * fix(web): sidebar rows show the branch again, not a truncated plan step by @t3dotgg in pingdotgg/t3code#5776 * feat(server): vp run migrate-dev-db seeds worktree dev dbs with real data by @t3dotgg in pingdotgg/t3code#5773 * feat(web): keep unsent drafts one click away in the sidebar by @t3dotgg in pingdotgg/t3code#5777 **Full Changelog**: pingdotgg/t3code@v0.0.33-nightly.20260809.1039...v0.0.33-nightly.20260809.1040 Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.33-nightly.20260809.1040

Preview:

Starting a new thread, ducking into another thread to grab context, then trying to get back to the draft was a mess. There was no way to navigate back, so you would hit New Thread again — which silently reset the draft's branch, worktree, and env mode, and overwrote its model and permission mode with whatever thread you were just viewing. The typed text survived; everything else did not.
Now any draft you have invested in (typed text or attachments) shows as a row at the top of the sidebar, above pinned. Clicking it is a plain navigation back to the draft, so every setting comes back exactly as you left it.
The semantics:
/landing, palette) always mint a fresh draft when the current one has content. They only reuse empty drafts, so sessions do not pile up. The sidebar rows are the only way back to an invested draft.Screenshot
(Theo: paste the sidebar screenshot here — the session can't attach images.)
Built by Claude Fable 5 running in Claude Code.
🤖 Generated with Claude Code
Note
Medium Risk
Touches composer draft lifecycle (remap, persist, project clear) and new-thread routing; wrong edge cases could orphan drafts or reset user work, but behavior is covered by new store tests and is mostly client-side UX state.
Overview
Invested composer drafts (prompt or attachments) now appear as rows at the top of the sidebar, above pinned threads. Each row shows project + preview, navigates to
/draft/$draftId, and can be discarded with a hover control. The open draft’s row uses a frozen snapshot so typing doesn’t repaint the sidebar; performance is isolated via a memoizedSidebarDraftBlockand a count-only subscription for empty state.Draft store semantics change so remapping a project to a new draft keeps the previous session when it has user content (unmapped but listed in the sidebar); empty drafts are still GC’d. Persist only keeps sessions that are mapped, promoting, or have content. Project removal sweeps all sessions for that project, including unmapped invested drafts.
getDraftSessionByProjectRefprefers the mapped draft over older unmapped ones.New-thread flows (
useHandleNewThread) only reuse empty stored drafts; invested drafts are left alone and a fresh draft is minted, with extra race guards so concurrent navigation/remap/content doesn’t clobber the winner.Reviewed by Cursor Bugbot for commit ba18319. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Note
Add unsent composer drafts with user content to the sidebar
SidebarDraftBlockandSidebarDraftRowcomponents to Sidebar.tsx that render unsent drafts above pinned threads, with click-to-navigate and inline discard.composerDraftHasUserContentpredicate to composerDraftStore.ts to identify drafts with non-empty prompt, attachments, contexts, or comments (model/mode selections excluded).useHandleNewThreadnow mint a fresh draft rather than reusing any draft that already has user content.Macroscope summarized ba18319.