Skip to content

feat(drafts): show where unsent drafts wait - #2853

Merged
tulsi-builder merged 4 commits into
mainfrom
basil/draft-pencil
Nov 6, 2026
Merged

tulsi-builder merged 4 commits into
mainfrom
basil/draft-pencil

Conversation

@tulsi-builder

@tulsi-builder tulsi-builder commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

What

A conversation where you left an unsent draft now shows that draft is waiting.

  • Sidebar: channel and DM rows show a pencil at the row's right edge when that conversation, or any of its threads, holds a draft. The pencil is hidden while that conversation is open. If a row would otherwise show only the temporary-channel clock, the pencil takes its place.
  • Message reply line: a thread with replies shows · ✎ Draft after its reply count. A message with no replies yet shows ✎ Draft reply. Both are hidden while that thread's composer is on screen.
  • Accessible names: the sidebar marker is an image labelled "Draft". The reply button reads "View thread: 23 replies. Draft reply" or "View thread: Draft reply".

Drafts & sent and every marker share one rule for what counts as a draft (draftCounts): non-whitespace text or an attached file. That way the count and the markers can't disagree.

Screenshots

Synthetic fixture data only.

Light Dark
Sidebar: pencil on a channel and a DM (Alpha is open, so it shows none)
Reply line with replies
Reply line, no replies yet

The images are hosted on the PNG-only branch basil/draft-pencil-screenshots, which can be deleted after merge.

How

  • src/features/messages/draft-places.ts: one shared store per session that scans saved drafts once per change and is read through boolean selectors, so typing in one composer doesn't re-render every row. Composers register while on screen (useShowingDraft), so an open thread doesn't mark itself.
  • ChannelSidebarItem, MessageRow, ReplySummary and MessageComposer consume it. drafts-sent/drafts.ts uses the shared rule.
  • docs/buzz-help.md describes the markers.

Inbox dwell test

An earlier revision carried a test-only fix for InboxPage.test.tsx › "a DM thread tab's dwell reads only up to a top-level message it never showed". On a loaded machine it blocked the pre-push gate. main has since landed an equivalent fix (it waits for reading-positioned before the fake clock, via #2829), so this PR dropped its own copy on rebase.

Checks

  • tsc --noEmit, biome check, design-system checks and git diff --check are clean.
  • Pre-push gate types-and-related-unit-tests passed on the rebased head 165b7a1f5.
  • Affected suites pass after rebasing onto 15fdd363c: drafts-sent, MessageRow, ChannelSidebarItem, MessageComposer and InboxPage (965 tests).
  • New tests cover:
    • the sidebar pencil, whitespace-only drafts, hiding while selected, the clock yielding to the pencil, and clearing
    • a Profiler check that an unrelated draft doesn't re-render a row
    • the thread draft label for both reply states, and hiding while the thread's composer is shown
  • The full Vitest suite passed (998 files, 18,868 tests) before the rebase onto main with Make drafts and sent part of Messages #2728. It hasn't been re-run in full since; hosted CI will cover that.
  • Playwright (Chromium, headless) exercised the sidebar and reply-line markers in light and dark to produce the screenshots. No browser cases were added or removed.
  • Four journeys (composer-focus, layout ×2, page-placement, scroll) now select a row left with a draft by its new accessible name, e.g. "Alpha Draft" (f142f3e00). These are the same updates feat(sidebar): show a pencil on channels and DMs with an unsent draft #2755 made. Hosted CI failed on exactly these before the change. Locally they pass in Chromium, apart from layout.spec.mjs:1401 "Alt+ArrowUp from a message link…", which also fails locally with no drafts involved.

Review

  • Xela (visual): approved placement and colour. --text-subtle measures ≥ 7.8:1 contrast in both themes.
  • YOU-X (interaction): shaped the copy and hide rules.
  • Crack Catcher (code review): no blockers on the feature. Their full run had one unrelated failure in a Builderlab Instructions test, which passed on rerun.

Known gaps

  • While agent activity shows on a reply line, the reply count stays hidden, as on main. The draft cue still reads "· Draft" when the thread has replies, so it's consistent either way (95f65e050, per design review).
  • onHideDm isn't passed anywhere today, so the DM row's hover ✕ never renders. Its spacing beside the pencil is unverified in the app.

Deferred

  • Native desktop check and human testing by the author (checklist steps 2–3). buzz-review-completed is intentionally absent.

🤖 Generated with Claude Code

tulsi-builder and others added 4 commits November 6, 2026 13:12
A pencil marks sidebar channel and DM rows whose conversation or threads
hold a draft (hidden on the open row), and a channel message's reply line
reads "Draft" or "Draft reply" while its thread's composer is out of
view. Markers and the Drafts & sent count share one draft rule.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
A sidebar row left holding a draft now carries the Draft pencil, which is
part of its accessible name ("Alpha Draft"). Journeys that return to a row
after typing in it select it by that name.

The spec updates match those in #2755 by @cheng-block.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
…raft

Design review: a thread with replies said "Draft reply" while an agent's
activity line hid its reply count, but "Draft" otherwise. The cue now
follows whether the thread has replies, not whether the count is shown.
The sidebar pencil's tooltip says "Draft", matching its accessible name.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>

@kalvinnchau kalvinnchau 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.

🤖 Approved after re-review of 165b7a1. No remaining actionable correctness or security findings.

The rebase preserves the four feature commits and removes the unrelated Inbox test repair already covered on main. The shared draft store retains community/viewer scoping, stable boolean snapshots and reference-counted visibility cleanup. The composer registration still matches the thread draft key after the upstream changes.

The browser locators now account for the draft suffix while retaining focus, restored-content, scroll and reload assertions. Reply-marker wording uses actual reply presence when activity hides the displayed count, with coverage for both wording states. The earlier locator issue is addressed in source.

Validation limitation: DCO, Semgrep and zizmor passed, but no Actions/browser/Vitest results were available for this head. Local browser setup stalled before usable UI, so interaction, persistence/recovery and native behavior were not validated locally. This approval reflects source review; required CI should pass before merge.

@tulsi-builder
tulsi-builder merged commit 696454a into main Nov 6, 2026
200 of 204 checks passed
@tulsi-builder
tulsi-builder deleted the basil/draft-pencil branch November 6, 2026 19:31
cheng-block added a commit to cheng-block/buzz-app that referenced this pull request Nov 6, 2026
…ime-hint

* upstream/main:
  ci: give the native job room for a cold Rust cache (block#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (block#2835)
  Add per-interest Attention pause and management controls (block#2847)
  feat(drafts): show where unsent drafts wait (block#2853)
  feat(agents): let app-hosted agents read their owner's channels and members (block#2745)
  NEXUS-4271: Expose installed plugin status (block#2754)
  fix(menu): stop clipping lifecycle actions behind a hidden scrollbar (block#2716)
  feat(agents): give approved external apps the selected agent's full Buzz tools (block#2707)
  AIDA-2549: Keep Claude settings drafts in sync (block#2841)
  fix(channels): keep a channel's side panels open when returning to it (block#2838)
  fix(builderlab): clarify remote-agent account availability (block#2764)
  fix(sidebar): rename Remove from Team to Close conversation (block#2845)
  perf(channels): cut header and name-matching work in large channels (block#2726)
  fix(browser): silence a browser page once its tab goes away (block#2721)
  Community Mesh: shared compute plugin (including agent type) (block#941)
  perf(relay): cut main-thread work from profile updates (block#2730)
  fix(composer): lead the channel placeholder with its sidebar icon (block#2829)
  test(activity): scope preview assertions to the request row (block#2843)
  perf(search): fold people names once across palette keystrokes (block#2570)
  fix(github): keep PR watches running when localStorage is full (block#2699)

Signed-off-by: Cheng Peng <[email protected]>
Co-authored-by: Cursor <[email protected]>
cheng-block added a commit to cheng-block/buzz-app that referenced this pull request Nov 6, 2026
…ions-literal-markdown

* upstream/main: (38 commits)
  fix(composer): let Option+Up/Down switch conversations from an empty composer (block#2832)
  fix(reactions): Scope timed reactions to the selected video (block#2796)
  fix(communities): name what the Join dialog is doing instead of "Working…" (block#2822)
  docs(agents): explain the per-conversation MCP cost of Claude agents (block#2683)
  fix(avatars): keep unsaved avatar drafts and focus, and validate the URL late (block#2714)
  fix(agents): offer stream and forum connection setup only to channel members (block#2749)
  Describe Attention as what the agent does, not its tools (block#2865)
  Let a local Codex agent join its owner's huddle by voice (block#2321)
  ci: give the native job room for a cold Rust cache (block#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (block#2835)
  Add per-interest Attention pause and management controls (block#2847)
  feat(drafts): show where unsent drafts wait (block#2853)
  feat(agents): let app-hosted agents read their owner's channels and members (block#2745)
  NEXUS-4271: Expose installed plugin status (block#2754)
  fix(menu): stop clipping lifecycle actions behind a hidden scrollbar (block#2716)
  feat(agents): give approved external apps the selected agent's full Buzz tools (block#2707)
  AIDA-2549: Keep Claude settings drafts in sync (block#2841)
  fix(channels): keep a channel's side panels open when returning to it (block#2838)
  fix(builderlab): clarify remote-agent account availability (block#2764)
  fix(sidebar): rename Remove from Team to Close conversation (block#2845)
  ...

Signed-off-by: Cheng Peng <[email protected]>
Co-authored-by: Cursor <[email protected]>
wpfleger96 added a commit that referenced this pull request Nov 6, 2026
…leanup

* origin/main:
  feat(terminal): add an Option as Meta key setting on macOS (#2850)
  fix(agents): restrict workflow requests in bb-block only (#2518)
  fix(dialogs): Enter presses the highlighted button in every dialog (#2782)
  Polish Compact messages and conversation appearance settings (#2863)
  fix(composer): let Option+Up/Down switch conversations from an empty composer (#2832)
  fix(reactions): Scope timed reactions to the selected video (#2796)
  fix(communities): name what the Join dialog is doing instead of "Working…" (#2822)
  docs(agents): explain the per-conversation MCP cost of Claude agents (#2683)
  fix(avatars): keep unsaved avatar drafts and focus, and validate the URL late (#2714)
  fix(agents): offer stream and forum connection setup only to channel members (#2749)
  Describe Attention as what the agent does, not its tools (#2865)
  Let a local Codex agent join its owner's huddle by voice (#2321)
  ci: give the native job room for a cold Rust cache (#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (#2835)
  Add per-interest Attention pause and management controls (#2847)
  feat(drafts): show where unsent drafts wait (#2853)

# Conflicts:
#	.github/workflows/release.yml
morgmart added a commit that referenced this pull request Nov 6, 2026
…-buzz-branding

* origin/main: (385 commits)
  fix(communities): let Cancel, × and Escape leave a Join lookup and name the host that didn't answer (#2821)
  fix(agents): let watches hear public channels the agent is not in (#1854)
  Preview and send Drafts & sent in place, with a shared count bubble (#2833)
  feat(terminal): add an Option as Meta key setting on macOS (#2850)
  fix(agents): restrict workflow requests in bb-block only (#2518)
  fix(dialogs): Enter presses the highlighted button in every dialog (#2782)
  Polish Compact messages and conversation appearance settings (#2863)
  fix(composer): let Option+Up/Down switch conversations from an empty composer (#2832)
  fix(reactions): Scope timed reactions to the selected video (#2796)
  fix(communities): name what the Join dialog is doing instead of "Working…" (#2822)
  docs(agents): explain the per-conversation MCP cost of Claude agents (#2683)
  fix(avatars): keep unsaved avatar drafts and focus, and validate the URL late (#2714)
  fix(agents): offer stream and forum connection setup only to channel members (#2749)
  Describe Attention as what the agent does, not its tools (#2865)
  Let a local Codex agent join its owner's huddle by voice (#2321)
  ci: give the native job room for a cold Rust cache (#2867)
  fix(pairing): refuse unknown communities natively and name the requesting process (#2835)
  Add per-interest Attention pause and management controls (#2847)
  feat(drafts): show where unsent drafts wait (#2853)
  feat(agents): let app-hosted agents read their owner's channels and members (#2745)
  ...

# Conflicts:
#	src/bundled/builderlab/README.md
#	src/bundled/builderlab/oauth/browser.test.ts
#	src/bundled/builderlab/oauth/browser.ts
baxen added a commit that referenced this pull request Nov 7, 2026
## What

This removes the draft pencil that #2853 added to channel and DM rows in
the sidebar.

## Why

The pencil appears when a conversation **or any of its threads** has a
draft. For a thread draft, the row doesn't say which thread it's in, so
it's hard to find. Drafts & sent already lists every draft and links to
where it is. Discussion: baxen and kalvin in #buzz-onedotzero.

## Unchanged

- The reply-line cue from #2853: `· ✎ Draft` after a reply count, or `✎
Draft reply` when nobody has replied yet. This is what helps you find a
thread draft.
- Drafts & sent and the shared `draftCounts` rule.
- The temporary-channel clock shows again on rows where the pencil used
to take its place.

## How

- `ChannelSidebarItem.tsx`: removes the marker, its `useChannelDraft`
read and the `!draft` guard on `TemporaryMarker`.
- `Channels.module.css`: removes `.draftMarker` and the rule it used to
hide an empty indicator stack.
- `draft-places.ts`: removes `useChannelDraft`, which no longer has
callers. `useThreadDraft`, `useShowingDraft` and `draftCounts` stay.
- Removes the two sidebar pencil tests from
`ChannelSidebarItem.test.tsx`.
- Changes the four browser journeys back to the plain row names
(`"Alpha"` instead of `"Alpha Draft"`).
- `docs/buzz-help.md`: describes only the reply-line cue now.

## Checks

- `tsc --noEmit` and biome are clean.
- `vitest run src/bundled/channels src/features/messages
src/features/drafts-sent`: 164 files and 4118 tests pass.
- The commit hooks and the pre-push `types-and-related-unit-tests` and
`design-system` hooks pass.
- **Deferred:** I didn't run the browser journeys locally; hosted CI
covers them. I haven't looked at the change in the running app.

Signed-off-by: Bradley Axen <[email protected]>
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
kalvinnchau added a commit that referenced this pull request Nov 7, 2026
…2906)

## Problem

Every write to a view key now rescans every saved draft. Since #2853
added draft markers, `draft-places` subscribes to all view writes in the
community. On each write it clears its cache, and every message row
(`useThreadDraft`) re-reads it. The first reader then runs
`listDraftViews`, which:
- walks every localStorage key;
- `JSON.parse`s each draft;
- projects each rich composer document to text.

Writes come from each composer keystroke (the draft save) and from
scroll-place saves, among others. A keystroke in a draft that already
has text never changes where drafts wait, so all of that work is thrown
away. The Drafts & sent list (`useDrafts`, mounted for the sidebar
badge) also re-reads every draft on writes to keys that are not drafts.

## Change

- `subscribeView` listeners receive the key that changed. `writeView`
passes it, as does `clearView` with one key and the cross-window
`storage` event. Scope-wide clears, focus and attachment changes still
pass nothing, which means "anything may have changed".
- `draft-places` skips the rescan, and does not notify, when the changed
key cannot change where drafts wait:
  - a key that is not a draft;
- a draft whose "counts as a draft" answer (same `draftCounts` rule) is
the same as in the cached list.

Anything doubtful falls back to the full rescan: a forgotten,
unreadable, malformed or oversized key.
- `useDrafts` ignores writes to keys that are not drafts.
- The 8 MB draft cutoff is now one exported constant,
`MAX_DRAFT_VIEW_LENGTH`, shared by `listDraftViews` and the new check.

## No behavior change

The cached list is only kept when one key changed and that key's
membership is provably unchanged, so the list equals what a full rescan
would return.
- **Cap:** a draft beyond the 500-entry cap is never in the list, so
writes to it still rescan, as before.
- **Other windows:** the storage event names the key, and the check
reads current shared localStorage, so the result is the same however
events arrive.
- **Drafts & sent:** `readDrafts` depends only on draft keys, attachment
counts and its activity record. The activity record is a separate
localStorage key, not a view key.

New tests in `draft-places.test.tsx`:
- writes to keys that aren't drafts, and keystrokes in a thread draft,
don't rescan;
- empty, clear and refill writes still update the marker;
- a cross-window storage event still marks a draft.

The first test fails without the change.

## Evidence

Main `7817418db` (after #2943 removed the sidebar draft marker) vs this
change. This PR is rebased onto `3884917e5`; the commits in between
don't touch drafts. 4 Linux and 4 macOS boxes each ran 6 interleaved
rounds. The table shows the mean of per-box medians, with the per-box Δ
range in parentheses.

| metric | Linux: main | Linux: this PR | Linux Δ % | macOS: main |
macOS: this PR | macOS Δ % |
|---|---|---|---|---|---|---|
| typing, 100 saved drafts: script | 386.2 ms | 350.3 ms | −9.3%
(−11.5..−4.5) | 297.8 ms | 269.2 ms | −9.6% (−12.3..−7.9) |
| typing, 100 saved drafts: busy | 755.9 ms | 725.0 ms | −4.1%
(−5.3..−1.6) | 546.1 ms | 509.1 ms | −6.8% (−8.6..−4.9) |
| typing, 100 saved drafts: key-to-paint | 36.8 ms | 36.3 ms | −1.2%
(−2.8..+0.4) | 24.2 ms | 21.3 ms | −11.9% (−14.8..−8.6) |
| typing, no saved drafts: script | 298.3 ms | 297.0 ms | −0.4% (noise)
| 232.9 ms | 231.7 ms | −0.5% (noise) |
| mention typing, 100 saved drafts: script | 2384.8 ms | 2358.7 ms |
−1.1% | 1460.6 ms | 1439.4 ms | −1.5% |
| channel switch: script | 505.3 ms | 510.0 ms | +0.9% (noise) | 295.3
ms | 296.2 ms | +0.3% (noise) |

The seeded drafts add about 30% script to plain typing on main (298.3 →
386.2 ms on Linux, 232.9 → 297.8 ms on macOS); this change removes about
a third of that. Most of the rest is the Drafts & sent list re-reading
every draft on each draft keystroke, which this PR leaves unchanged.

Lower is better. The typing benchmark (an Event Timing harness on the
browser fixture) runs a production build in Chromium at 4× CPU
throttling and types 12 keys in the channel composer per phase, 2 phases
per run. Before typing, 100 copies of the composer's own saved draft are
seeded into other channels. Key-to-paint is the Event Timing keydown
duration averaged over every key; keys under 16 ms count as 0. Builds
are interleaved per round; each box's median is taken, then the mean of
box medians per OS. With no saved drafts the change is within noise, as
expected.

Checks:
- vitest for `draft-places`, `view-state` and `drafts-sent` passes (89
tests).
- `tsc` and `biome` pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

2 participants