Skip to content

feat(web): All Threads, a Linear-like list of every thread - #141

Merged
sanchitrk merged 61 commits into
canaryfrom
claude/all-threads-abb106
Nov 6, 2026
Merged

sanchitrk merged 61 commits into
canaryfrom
claude/all-threads-abb106

Conversation

@sanchitrk

@sanchitrk sanchitrk commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the Dashboard widget with All Threads, a Linear-like list of every thread, and turns Projects into a tree page. Stacked on #140 (feat/sync).

All Threads

  • Read: dashboard.list is renamed threads.list (contract types ThreadsBy, ListedThread, ThreadList, AllThreads, readTopics.threads), and each thread now carries its machine. The E2E test and artifacts are renamed with it; the artifact diff is the added machine field only.
  • List:
    • Sections by status, priority, group or none, each with a count.
    • Ordering by manual ordering, last updated, or priority.
    • Filters for status, priority and machine.
    • Display options saved per view in kv.
  • Rows:
    • Status, priority, group and color pickers, built as searchable cmdk lists like Linear's. 1–9 pick, and typing a new group name creates it.
    • Keyboard: arrows or j/k to move, Enter to open, S/P/L/C for the pickers.
    • Drag to reorder within a section or move between sections; a drop on a header lands at the top.
    • Groups are created, renamed, recolored, reordered and deleted from their headers.
  • Where it opens: a full page at /threads, from the sidebar or the palette's Navigation group.

Performance (following t3code's sidebar)

  • Memoized rows with one stable actions object.
  • Each read keeps the previous object for every thread and group that didn't change.
  • One shared picker instead of four popovers per row.
  • The run mark reads only its own thread's slice of sentAtom.
  • Cached date formatters, and drag work found once per section.
  • Measured at 1,004 threads: a key press or a drag-over went from about 640 ms to one frame, and a status change's refresh no longer blocks.

Also

  • Projects: a page under Navigation instead of a widget, rendered with @pierre/trees. Projects are section bands, then sessions, then forks; a main thread or fork opens in a workspace.
  • Sidebar: New, Search and All Threads as one list, and session titles in regular weight.
  • Palette: a chosen theme now sticks (closing used to restore the old one), and a highlighted theme previews at once.
  • Sessions: switching opens at the end without the up-down jump. The list stays hidden until it has scrolled, and the composer height carries over between conversations.
  • cn: knows the theme's own type sizes, so text-ui replaces a component's text-sm.

React Compiler and render fixes (latest)

  • React Compiler turned on for apps/web; it skips only two components, both on purpose.

  • Render fixes, measured with 80 split frames and 80 concurrent real openrouter runs:

    Before After
    Main thread blocked 27% 7%
    Worst freeze 2.5 s 0.5 s
    Frames over 50 ms 112 12

    During one run, sidebar rows render 12 times instead of 6,596, and conversations 28 times instead of 993.

    • The fork handler keeps one identity, via useStableCallback ported from t3code.
    • The sidebar's read keeps unchanged sessions (shared kernel/keep.ts), and rows get plain values instead of a fresh spread.
    • The session title source keeps its previous value when nothing changed.
    • Each frame builds its shell once and renders its widget through a memoized body.
    • Sent state is one atom per thread.
  • Frame headers: a session frame shows the run's spinner while working, as the sidebar does.

  • Fix: renaming a group from its menu works.

  • Clock and spinners: a shared minute/second clock drives relative times and elapsed timers; spinners pause when off-screen. Memo hooks the compiler makes redundant are removed.

  • Projects tree:

    • forks nest under the thread they were cut from, at any depth (threads.list now returns originThreadId);
    • fork rows are marked "fork";
    • bands show each project's folder, muted;
    • the sticky band stays opaque.

Verification

  • vp check, vp run typecheck and the apps/server suite (26/26) pass.
  • The 80-frame split test ran on real openrouter / stealth/crouton-alpha calls, with 1,000 seeded threads, 1,000 seeded sessions and a 400-turn mock conversation in app.db.
  • Working spinners checked live in the sidebar, frame headers, the workspace tab and All Threads.
  • Checked in the Electron app with real gpt-6-luna calls, driven over CDP:
    • list, grouping, ordering, filters, pickers;
    • drags, each writing one row in app.db;
    • groups, keyboard, Enter, live run marks, display options across a reload;
    • the Projects tree, theme switching, session switching.

Known follow-ups

  • Add Project: a path typed after the prefilled ~/ gets appended to it, and a missing folder is created silently.
  • Error boundary: a widget that throws takes down the whole window; there's no per-widget boundary.
  • Refresh during streaming: the list re-reads only after a 50 ms pause in events. Not measured during a long run.

🤖 Generated with Claude Code

sanchitrk and others added 30 commits November 5, 2026 18:38
session.llm.text.delta and session.llm.thinking.delta gain `offset`, the
part's text length (JS string length) before the delta, so a client can
splice a delta onto its draft or notice a gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The execution lifecycle events and retry.scheduled are durable, and the
session projector writes the thread's run state (run_state, its times,
outcome and error) from each event's own timestamp. At startup, before the
server listens, the harness ends every run a dead process left open with
execution.interrupted (crash, or shutdown when the run was closed); a run
recovery resumes says so with execution.started { resumed: true }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single-row store table holds the database's id and its last revision.
Project, space, session, thread, entry and inbox rows carry the rev of the
write transaction that last touched them, enforced by insert and update
triggers. Store.write runs a write transaction (or joins the enclosing one),
Store.rev takes its revision on first use, and a commit that took one rings
a store-wide doorbell (Store.wake). Every write path goes through it: event
projectors, plain-SQL thread, session, project and space writes. Part writes
stamp their entry, document writes stamp the threads they configure, and
deleting a session leaves a removed row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run the loop ends itself (crashloop, interrupted, truncated) publishes
session.run.failed and returns normally, so the coordinator also published
session.execution.succeeded and the thread row lost the failure reason.
Runner.run and abandon now return an outcome; the execution's settled hook
publishes no terminal after a run that already ended itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion and thread rows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entries carry their size, the bytes of their parts as stored, computed in
SQL. Thread.linePage reads the newest page of a thread's line within a byte
budget and a row count, past an optional seq. Sync.open reads a thread's
first page and pending inbox; Sync.thread reads its own entries and inbox
rows by revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread's newest page within a budget and its pending inbox on first open,
or its rows changed since a revision on resume, then the same live after
each commit, with the thread's text and thinking deltas merged in.
HistoryEntry carries size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table

app.db gets the desktop sync's schema: machine (replacing servers),
organization (seeded "personal"), grouping, project, space, session,
thread, entry, part and inbox, with foreign keys so removing a machine
cascades to everything copied from it. The migrations collapse into one
that creates the schema directly and seeds the built-in local machine.

servers.* read and write the machine table and accept remote machines:
a server is local or remote, and a remote one is added and updated with
its WebSocket URL. Only a local server can be the default or run
codework service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
server.info gains storeId and rev; index.sync, thread.sync and thread.page
join the mirror with the project, space, session, thread summary, run and
live delta shapes they carry. HistoryEntry gains size, so the session
widget's stand-in entries carry one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Sync service keeps app.db a copy of each machine's CodeWork over its own
connection: index.sync from machine.cursor, a full sync for a new or
restored store, and thread.sync for opened threads from thread.cursor.
Each batch applies in one transaction with deferred foreign keys; new
threads get their first-seen status, priority and ordering, and run
changes move status with a sync/status event in the same transaction.
Removed sessions are soft-deleted with their detail kept. Live deltas
that continue a draft are forwarded on the app bus, never stored.

sync.open, sync.close and sync.page join the app contract, and the
Event service gains transact so rows and their bus events commit
together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rder and color

App RPCs set a thread's status, priority, color, group and order, a session's
color, and create, update, move and delete groups. Each write lands with its bus
event in one transaction: status edits on sync/status, the rest on sync/tracking
with a tagged payload. Moves write one row at the midpoint of the neighbours, or
the lowest minus one / highest plus one. Out-of-range values and missing rows are
typed TrackingErrors.

The E2E helpers move out of the sync suite into a shared fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs the CodeWork fixture server (scripted model) on real OrbStack Linux
machines, syncs them all into apps/server on the Mac and misbehaves the
network: first sync under concurrent writes, resume after an iptables
partition, deletes while away, crash sweep and recovery, restored and
replaced stores, a netem-throttled live reply, a fork over a busy source,
one repository on two machines with removal cascade, and revision ordering
with a refused write. The machine set grows from 3 to 5 and shrinks again;
each case and the shuffle leave an artifact under __artifacts__.

The machine scripts (setup, start, stop, net, drive) live in
test/machines; the fixture server gains a bind host, long replies and a
sweep-only mode. The suite is skipped where orb is not installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The machine's boot (orb start until its server answers from the Mac) ran
from under a second to three minutes and pushed the step's bucket past
its edge, so the shuffle artifact drifted. A step can now time its sync
apart from what it waits on: the artifact buckets that sync time, the
printed timings and the measured file keep both. The wait for the
rebooted machine is raised to 8 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app server serves two reads over app.db alone: `sidebar.list` (an
organization's active projects with their sessions and main threads, on
the default machine or every machine) and `threads.line` (one keyset page
of a thread's line, inherited entries included, with parts, the pending
inbox and an untitled fork's ancestor title). `readTopics` names the bus
topics each read refreshes from.

The sidebar and switcher read run state from the synced rows; the working
set, `projects.list` and its `thread.list` pass-through are gone. The
session backend opens a thread's sync on follow, pages from app.db
(fetching below its floor with `sync.page`), forwards the sync's notices
and deltas on the thread's topic, and sends commands to the machine the
session is on. It no longer keeps kv snapshots or subscribes to CodeWork's
global event feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st delta, end a follow on removal

The switcher reads the run state of the thread each frame shows. The view starts a part from a delta at offset 0 and holds deltas for a reply whose row has not landed, merging them once it does. A follow waiting for a thread ends as soon as its session is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dashboard.list splits an organization's threads by status, priority or
group, each list in ordering and every list present, with the groups in
their ordering; a thread's color falls back to its session's. Clients
refresh it from sync/changed, sync/status and sync/tracking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard lists threads by status, priority or group, with controls
for each, group management, and drag to reorder or move between lists.
A prompt not yet in app.db shows pending and a stop not yet idle shows
stopping, drawn after 200 ms and held 300 ms, in the session view, the
workspace tab and the sidebar row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread missing from app.db is waited for only until its machine's index reaches the store head read at the follow (a row in flight, such as a session just created); still missing then, it is not found, as thread.info reported it. A deleted session or thread fails at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… off the dashboard

A session's place in the sidebar is the newest change to any of its threads,
forks included, not its main thread's alone. Subagent threads are not listed
on the dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y display reads idle

The switcher reads each thread's run_state rather than a running flag, so a
starting run reads as starting. A stop no longer clears when the first display
reads the run idle: the session view hears it before the sidebar list refreshes,
so the tab drew the run again until its own copy caught up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sanchitrk and others added 24 commits November 6, 2026 01:40
The sidebar's All Threads opens `/threads`, a full page over the mounted
desktop as settings is; a thread picked there opens in a workspace as the
sidebar opens sessions. The palette still opens the widget in a workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linear's list has no free-text filter; status, priority and machine remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r does

Each row picker is a searchable list: the field names what it changes and
shows its key, the current option is checked, and 1-9 pick while the field is
empty. A group name no group has offers to create it and puts the thread in
it. Colors read by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… size

Row pickers use the stock Command input, group and items with digits as
shortcuts; filter values are plain items with the mark first and a check
when on, not checkbox items with an empty indicator column; the display
options sit in a spaced grid. `cn` now knows the theme's own type sizes, so
`text-ui` replaces a component's `text-sm` instead of sitting beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Menu text drops to 12px and the picker's group heading to 11px; spacing stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pickers open from a key many times a day, so they show at once without the
popover's zoom. The sidebar's current session no longer turns medium, which
reflowed its title; background and ink mark it. The color handle is wider and
shows a faint bar on hover when unset, and the section chevron honors reduced
motion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows sit 28px apart in a 240px popover; the selects are borderless with a
faint fill and the switch is the small one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following t3code's sidebar: rows are memoized and get values plus one actions
object whose methods reach the latest handlers through a ref; each read keeps
the previous object for every thread and group that did not change; one
picker anchored to the open row's button replaces four popovers and five
mutation hooks per row; a run's mark reads only its own threads' part of
what was sent; the drag index is found once per section and the whole order
is sorted only on a drop; dates use formatters built once. At 1004 threads a
key press or a drag over a row goes from about 640ms to one frame, and a
status change's read no longer blocks the main thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching sessions remounts the transcript, whose list drew one frame at the
top before its initial scroll, then scrolled once more when the composer's
height reached the footer. The list stays hidden until it has loaded and
scrolled, and a conversation starts with the last composer height, so it
appears at its end and stays there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion and View

Choosing a theme applied it and then closing put the saved one back, so the
screen kept the old theme while the palette marked the new one. Closing after
an action no longer restores, and a highlighted theme previews at once
instead of after a pause. The palette opens on Navigation (All Threads, the
page) and View (Projects, in the workspace).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Projects view renders with @pierre/trees: each project folds open to its
sessions in the sidebar's order, and a session with forks folds open to them.
A main thread or a fork opens its thread beside the tree (Cmd-click or a
middle click in a new frame, Enter from the keyboard); a project only folds.
Rows show a chat mark and the last activity, and the tree's colors come from
the theme's tokens, so it follows light and dark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s put

The page had its own header, a few pixels off a frame's, so its title jumped
when switching from a workspace. The frame's header is shared, and a page
frame draws it with the desktop's surface and inset but no controls: the
title and icon land exactly where a lone widget's do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tle stays put"

This reverts commit 384a90c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Projects leaves the widgets and opens as its own page, as All Threads does,
from the palette's Navigation group, which now holds both. The two pages
share one page shell: the header, a widget's host, and opening a thread in a
workspace as the sidebar opens sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A toolbar counts the projects, and each project's row is a full-width
section band, sticky while its sessions scroll under it, with its session
count on the right; rows take All Threads' type size. A click still folds the
project and opens a thread as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enable `react({ compiler: true })` with oxc-transform-react, as t3code
does, and lint apps/web with the rules that map to compiler bailouts.

Rewrite what the compiler skipped: a hook to keep a value's last defined
state instead of a ref written in render (notice slot, folder listing),
destructure useFollow's result so its callback ref does not mark the
whole object a ref, scroll the live phase with scrollTo, and drop a
try/finally, a destructured for..of and a default arrow parameter.
Switcher and a desktop resize check read refs in render by design and
opt out with "use no memo".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build useRunDisplay's run-state map from its text key, so the compiler
keeps the memo, and read the transcript's settled state from the
destructured useFollow result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing Rename closed the menu, which took focus back as the name field
mounted, so typing went nowhere and the old name was saved. The menu no
longer restores focus, and the field focuses and selects its text a frame
after it mounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a frame's thread is starting, running or being stopped, its header
icon is the run's mark as the sidebar draws it, in place of the project's
badge, so it is plain which of several split frames is working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured with 80 frames open and 80 runs at once: main-thread blocking falls
from 27% to 7% of the run, the worst freeze from 2.5s to 0.5s, and frames
over 50ms from 112 to 12. During one run, sidebar rows render 12 times
instead of 6,596 and conversations 28 instead of 993.

- The transcript's fork handler keeps one identity (useStableCallback, as
  t3code's), so turns no longer render for every send or desktop render.
- The sidebar's read keeps unchanged sessions' objects, and rows take the
  project's id, name and machine as values instead of a fresh spread, so
  memoized rows hold. kernel/keep.ts shares this with All Threads' read.
- What the sidebar says about a session (a frame's title before load)
  returns its previous object when unchanged, so conversations stop
  rendering on every sidebar read.
- Each frame builds its shell once and renders its widget through a
  memoized body, so drags, focus and title changes leave widgets alone.
- What a window sent is one atom per thread, read through a derived atom
  per set of keys, so a send touches only its thread's readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lder

The tree's background was transparent, and sticky project bands are drawn
in an overlay painted with it, so rows scrolling under a band showed through
it. The tree now uses the page's own surface. Each band also shows its
project's folder, muted and shortened (home as ~, past three folders only the
last two), before its session count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A band's weight never changes, so selecting one cannot reflow its name; the
selection background marks the active band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ects tree

`threads.list` returns each thread's `originThreadId`, so the tree nests a
fork under its source at any depth: a fork of a fork sits under that fork,
not beside it. A fork whose source is not listed (a subagent) hangs off its
session's main thread. Fork rows read "fork" before their time, since a fork
often carries its source's title. The server's threads artifacts gain the
field and nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-redundant memos

- One shared second and minute timer behind useSyncExternalStore
  (hooks/now.ts over kernel/clock.ts). The sidebar's relative time is an
  <Ago> leaf on the minute tick, so memoized rows no longer go stale; the
  session's live "working for" label reads the second tick instead of a
  per-instance interval; the Projects tree redraws its decorations each
  minute.
- PixelLoader observes itself through one shared IntersectionObserver
  plus tab visibility, and .pixel-loader-cell pauses through
  --visible-animation-state while off screen, collapsed or hidden.
- With the React Compiler on, plain values replace the useMemo and
  useCallback calls it already memoizes. Ref-backed stable actions stay;
  the markdown plugin instance moves to useState.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feat/sync to canary November 6, 2026 07:35
sanchitrk and others added 3 commits November 6, 2026 13:12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The check now runs in two passes like a dead-files pass over the whole
monorepo and a dead-exports pass over the app workspaces. The files and
the dependency it found had no importers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sanchitrk
sanchitrk merged commit b9c111b into canary Nov 6, 2026
1 check passed
@sanchitrk
sanchitrk deleted the claude/all-threads-abb106 branch November 6, 2026 07:46
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