One thread, many deliberate projections
The composer, timeline, Agents panel, review panel, and sidebar do not own competing thread records. They select and project one canonical environment-scoped thread through different interaction contracts, including optimistic local affordances that must yield to canonical state.
What this chapter resolves
- Trace composer content, attachments, provider commands and skills to a durable turn lifecycle without confusing UI insertion with execution.
- Distinguish an optimistic client affordance from accepted durable state and streamed projection.
- Explain quiet work-log and Agents views as different folds of canonical thread activity.
- Compare review modes and sidebar sorting/pinning as surface-specific selections of one scoped thread model.
The web application has many ways to look at a thread. That does not make each panel a source of truth. The canonical thread projection carries its messages, activities, turn/session state, checkpoints, lifecycle overlays, and provider observations. Web surfaces take selections from that material:
- the composer prepares the next intent;
- the timeline favors conversation and quiet grouped work;
- Agents unfolds retained task evidence into a roster;
- review selects one comparison basis;
- the sidebar sorts, shelves, and pins thread shells.
All of those are scoped by environment and thread identity. A title, pin order, or status pill in a sidebar is not a competing copy of the conversation.
The composer composes intent; the server accepts or rejects it
The composer combines prompt text with selected provider/model/mode controls, attachments, terminal and preview context, pending approval/input/review states, and menu affordances. Before a regular turn is sent, web code persists selected model and modes on the thread. The server’s normalizer then owns timestamp, workspace, and attachment normalization before dispatch. A successful dispatch result names the accepted command/sequence boundary; it is stronger than a local button state, but later provider work still crosses the post-commit reactor seam described in Chapters 10–12 and 23.
Attachments are references with a lifecycle
Attachments are not just inline text pasted into a browser control. The server normalizes attachment paths under its attachment root and later collects projected attachment references for cleanup. A rendered attachment token therefore must not be treated as proof that arbitrary files on the browser’s machine are readable, or that an attachment will survive a thread deletion indefinitely.
Commands and skills are prompt affordances, not generic jobs
In the / menu, web combines built-in commands with provider slash commands and
eligible provider skills. Enabled skills join that menu only when the relevant
setting is enabled; a visible skill can suppress a same-named provider slash
command. A $ skill choice replaces prompt text with $skill syntax. Provider
slash command selection similarly inserts /command text. These operations are
useful because the selected provider sees its native prompt convention; they do not
by themselves create a durable task, plan step, or scheduler assignment.
Optimistic state is a temporary projection with a surrender rule
Some interactions must feel immediate before the durable stream reflects them. The sidebar’s pin drag keeps an optimistic ordering snapshot containing the desired order, its observed pin-order keys, and the keys it wrote. It retains that local order until canonical state confirms it—but releases it if membership changes, a foreign concurrent key lands, all local assignments land, or canonical order already agrees. That last set of conditions is the important contract: local optimism may bridge latency, but it must yield to the canonical thread projection.
// and holding it would launder a stale order into later drags.
const pinnedDndSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
);
const [optimisticPinnedOrder, setOptimisticPinnedOrder] = useState<{
readonly order: readonly string[];
/** pinOrderKey per thread as of the drop — the baseline that tells a
concurrent client's write apart from one of our own landing. */
readonly keysAtDrop: ReadonlyMap<string, string | null>;
/** The keys this drop writes (one per planned assignment). The
override holds until all of them appear in canonical state. */
readonly assignedKeys: ReadonlyMap<string, string>;
} | null>(null);
const orderedPinnedThreads = useMemo(() => {
if (optimisticPinnedOrder === null) return pinnedThreads;
return orderItemsByPreferredIds({
items: pinnedThreads,
preferredIds: optimisticPinnedOrder.order,
getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
});
}, [optimisticPinnedOrder, pinnedThreads]);
useEffect(() => {
if (optimisticPinnedOrder === null) return;
const canonical = pinnedThreads.filter((thread) =>
reorderablePinnedKeys.has(scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id))),
);
const canonicalKeys = canonical.map((thread) =>
scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
);
// The override represents one drop against one snapshot of the world.
// Release it when the world moves on: membership changed (pin/unpin/
// snooze/wake — the override can't say where members it never saw
// belong), a key changed to something we did NOT write (a concurrent
// client's reorder that must win), every key we wrote has landed, or
// canonical already matches. Releasing on the FIRST landed key instead
// of the last exposes the half-written order mid-materialization and
// the block visibly reshuffles once per write.
const membershipChanged =
canonicalKeys.length !== optimisticPinnedOrder.order.length ||
canonicalKeys.some((key) => !optimisticPinnedOrder.order.includes(key));
const foreignKeyLanded = canonical.some((thread, index) => {
const threadKey = canonicalKeys[index]!;
const currentKey = thread.pinOrderKey ?? null;
if (currentKey === optimisticPinnedOrder.keysAtDrop.get(threadKey)) return false;
return currentKey !== optimisticPinnedOrder.assignedKeys.get(threadKey);
});
const currentKeyByThreadKey = new Map(
canonical.map((thread, index) => [canonicalKeys[index]!, thread.pinOrderKey ?? null]),
);
const allAssignmentsLanded = [...optimisticPinnedOrder.assignedKeys].every(
([threadKey, orderKey]) => currentKeyByThreadKey.get(threadKey) === orderKey,
);
const orderConfirmed =
!membershipChanged &&
canonicalKeys.every((key, index) => key === optimisticPinnedOrder.order[index]);
if (membershipChanged || foreignKeyLanded || allAssignmentsLanded || orderConfirmed) {
setOptimisticPinnedOrder(null);Quiet log and Agents roster intentionally disagree in granularity
The timeline’s quiet-work policy groups direct agents by spawn turn and workflow
members under their coordinator. It replaces a flood of task lifecycle rows with a
call to action that opens Agents. The client-runtime fold reads retained task.*
and agent-owned tool activity into a source-neutral roster, tolerating missing
starts, late completion, reactivation, and session death. The web Agents panel then
shows rows and aggregate information from that folded model.
Review modes select a comparison, not a different thread
The review panel can select working-tree changes, branch changes, the latest turn, or an earlier turn. A turn selection derives an adjacent checkpoint range; a non-turn selection asks the environment for a working-tree or branch diff. The panel refreshes an applicable branch diff on browser focus and after the newest completed turn changes. Those are different evidence sources and cache scopes, but they remain anchored to the active environment/thread/workspace context.
Sidebar shelves and sorts the same thread shells
The sidebar creates one visible ordering from pinned, active, route-preserved
snoozed, and visible settled threads. Pinning is a lifecycle overlay, not a new
thread type: the durable model provides pinOrderKey, snooze, settlement, and
archive fields; the UI applies shelving and sorting policy. The active route is
special-cased so a snoozed thread reached through a deep link does not disappear
behind a collapsed shelf.
Exercise the projection boundary
The lab keeps one canonical thread scenario on the left and lets each surface choose its own projection. Change the scenario and selected surface, then compare the displayed pin, task, review, and local-view fields to see why a quiet timeline and a detailed Agents panel can honestly look different without holding different canonical threads.
One canonical thread, six product surfaces
Change the durable scenario, then choose a projection to see what it reads, leaves out, and can mutate.
Active turn · Chat timeline
thread_31 / durable fieldsA running session and a local draft coexist; the draft is not a durable message yet.
Consumes
- messages
- session.status
- activities
Intentionally suppresses
- task.latest
- review.comments
- orderedAt
Static projection map
| Surface | Canonical fields it consumes | Local view state |
|---|---|---|
| Chat timeline | messages · session.status · activities | Scroll position · collapsed activity |
| Composer + optimistic state | draft · messages · session.status | Draft text · optimistic pending row |
| Quiet work log | activities · task.latest · session.status | Expanded rows · filter |
| Agents / subagents | task.latest · activities · session.status | Roster grouping · selected agent |
| Review / diff | turn.diff · checkpoint · review.comments | Selected lines · diff mode |
| Ordered / pinned sidebar | threadId · pinnedAt · orderedAt · messages | Sort choice · collapsed groups |
Text equivalent
The interactive lab offers three canonical thread scenarios and six product surfaces. For each surface it lists the canonical fields consumed, fields intentionally suppressed, server-state controls, and local-view controls. Chat emphasizes messages and session activity; composer combines a draft with submission controls; the quiet log condenses activity; Agents expands task evidence; review selects checkpoint and diff data; sidebar projects thread identity, pinning, and ordering. All retain the same thread identity and none becomes a second durable transcript.