Repository navigation
feat(web): All Threads, a Linear-like list of every thread - #141
Merged
Merged
Conversation
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>
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
dashboard.listis renamedthreads.list(contract typesThreadsBy,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 addedmachinefield only.ordering, last updated, or priority./threads, from the sidebar or the palette's Navigation group.Performance (following t3code's sidebar)
sentAtom.Also
@pierre/trees. Projects are section bands, then sessions, then forks; a main thread or fork opens in a workspace.cn: knows the theme's own type sizes, sotext-uireplaces a component'stext-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
openrouterruns:During one run, sidebar rows render 12 times instead of 6,596, and conversations 28 times instead of 993.
useStableCallbackported from t3code.kernel/keep.ts), and rows get plain values instead of a fresh spread.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:
threads.listnow returnsoriginThreadId);Verification
vp check,vp run typecheckand theapps/serversuite (26/26) pass.openrouter/stealth/crouton-alphacalls, with 1,000 seeded threads, 1,000 seeded sessions and a 400-turn mock conversation inapp.db.gpt-6-lunacalls, driven over CDP:app.db;Known follow-ups
~/gets appended to it, and a missing folder is created silently.🤖 Generated with Claude Code