Skip to content

feat(web): keep unsent drafts one click away in the sidebar - #5777

Merged
t3dotgg merged 6 commits into
mainfrom
t3code/sidebar-draft-thread-view
Aug 9, 2026
Merged

feat(web): keep unsent drafts one click away in the sidebar#5777
t3dotgg merged 6 commits into
mainfrom
t3code/sidebar-draft-thread-view

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 9, 2026

Copy link
Copy Markdown
Member

Preview:
image

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:

  • A row appears once you navigate away from a draft. A fresh draft you are still typing has no row.
  • Opening a draft keeps its row (active highlight), frozen at the moment you entered — no per-keystroke repainting. It updates when you leave.
  • Send or discard removes the row. Discard is a hover X on the row.
  • New-thread surfaces (button, hotkeys, / 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.
  • The store keeps invested drafts alive (unmapped) when the per-project draft mapping moves to a fresh one, instead of deleting them. Content-less unmapped sessions get GC'd at persist time.

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 memoized SidebarDraftBlock and 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. getDraftSessionByProjectRef prefers 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

    • Added a sidebar section for unsent drafts, including project, model, session, prompt, and attachment previews.
    • Drafts can be reopened, navigated, or discarded directly from the sidebar.
    • The currently open draft remains stable while edits are in progress.
  • Bug Fixes

    • Drafts containing text or attachments are now preserved more reliably.
    • Starting a new thread no longer replaces an existing draft with user content.
    • Empty drafts are cleaned up while meaningful drafts remain available.

Note

Add unsent composer drafts with user content to the sidebar

  • Adds SidebarDraftBlock and SidebarDraftRow components to Sidebar.tsx that render unsent drafts above pinned threads, with click-to-navigate and inline discard.
  • Adds composerDraftHasUserContent predicate to composerDraftStore.ts to identify drafts with non-empty prompt, attachments, contexts, or comments (model/mode selections excluded).
  • Persistence now drops empty, unmapped, non-promoting draft sessions; remapping a project preserves the previous draft if it has user content instead of deleting it.
  • New-thread actions in useHandleNewThread now mint a fresh draft rather than reusing any draft that already has user content.
  • Uses a count-only store subscription in the sidebar to avoid full re-renders while the user is typing in a draft composer.

Macroscope summarized ba18319.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Composer draft lifecycle

Layer / File(s) Summary
Draft content detection and persistence
apps/web/src/composerDraftStore.ts, apps/web/src/composerDraftStore.test.ts
The store detects meaningful composer content, preserves invested drafts during project remapping, filters persisted metadata, and prioritizes mapped project drafts. Tests cover empty-draft removal and content preservation.
New-thread draft selection
apps/web/src/hooks/useHandleNewThread.ts
New-thread creation reuses only empty drafts. Drafts with prompts or attachments remain separate from new-thread initialization.
Sidebar draft display and navigation
apps/web/src/components/Sidebar.tsx
The sidebar renders scoped drafts with context and previews, supports navigation and discard actions, preserves the active draft snapshot, and hides the empty state when drafts exist.

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
Loading

Possibly related issues

  • pingdotgg/t3code/5534 — Addresses the same objective of making unsent composer drafts visible and manageable in the sidebar.

Suggested reviewers: juliusmarminge, maria-rcks, shivamhwp

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: keeping unsent drafts accessible from the sidebar.
Description check ✅ Passed The description clearly explains the change, rationale, behavior, UI impact, and testing risk, although it omits the template checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/sidebar-draft-thread-view

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 9, 2026
Comment thread apps/web/src/components/Sidebar.tsx
Comment thread apps/web/src/components/Sidebar.tsx
Comment thread apps/web/src/components/Sidebar.tsx
Comment thread apps/web/src/composerDraftStore.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/web/src/composerDraftStore.test.ts (1)

852-867: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the persistence filter.

This test proves the in-memory survival of an invested draft. The reload half is untested. partializeComposerDraftStoreState now 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 win

Extract the draft-visibility rule into one shared predicate.

This selector reimplements the row filter in SidebarDraftBlock at 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 composerDraftHasUserContent and call it from both sites. The rule is client-agnostic, so packages/client-runtime is also a candidate home if desktop and mobile surface draft rows later.

As per coding guidelines: "shared logic belongs in packages/client-runtime when 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

📥 Commits

Reviewing files that changed from the base of the PR and between 886195e and 4742b0c.

📒 Files selected for processing (4)
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/hooks/useHandleNewThread.ts

Comment thread apps/web/src/components/Sidebar.tsx
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB −6 B (−0.1%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB +1 B (+0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −7 B (−0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB +1 B (+0.0%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB −1 B (−0.0%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB +2 B (+0.0%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: ddaa6af · PR result: ba18319 · Source CI: success

Scenario and decoded snapshot size

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

  • Codex decoded thread snapshot: 94.6 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment thread apps/web/src/composerDraftStore.ts
@macroscopeapp

macroscopeapp Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

t3dotgg and others added 4 commits August 8, 2026 20:03
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>
@t3dotgg
t3dotgg force-pushed the t3code/sidebar-draft-thread-view branch from 4f4f858 to 81a40cf Compare August 9, 2026 03:05
Comment thread apps/web/src/composerDraftStore.ts Outdated
Comment thread apps/web/src/hooks/useHandleNewThread.ts
- 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix All in Cursor

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

Comment thread apps/web/src/composerDraftStore.ts Outdated
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>
@t3dotgg
t3dotgg merged commit 05eb051 into main Aug 9, 2026
17 checks passed
@t3dotgg
t3dotgg deleted the t3code/sidebar-draft-thread-view branch August 9, 2026 03:29
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 9, 2026
## 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant