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 25–27 and 38.
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.
? settled
: active
).push(
optimisticDrop.clearsSnooze
? projected
: { ...projected, snoozedAt: thread.snoozedAt, snoozedUntil: thread.snoozedUntil },
);
} else if (supportsSnooze && effectiveSnoozed(thread, { now: preciseNow })) {
// Snooze outranks settlement and pinning until the thread wakes.
snoozed.push(thread);
} else if (supportsSettlement && thread.settledOverride === "settled") {
settled.push(thread);
} else if (thread.pinnedAt != null) {
pinned.push(thread);
} else {
active.push(thread);
}
}
// One shared rule on every platform (see sortPinnedThreadsByOrderKey):
// user-arranged keys first, keyless threads in creation order below.
// Server capability only gates DRAGGING — it must not influence the
// sort, or mixed-version fleets would render different pinned orders on
// web and mobile from the same data.
const sortedPinned = sortPinnedThreadsForSidebar(pinned);
const sortedActive = sortThreadsForSidebar(active);
return {
pinnedThreads:
optimisticDrop?.section !== "pinned" || optimisticDrop.order === null
? sortedPinned
: orderItemsByPreferredIds({
items: sortedPinned,
preferredIds: optimisticDrop.order,
getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
}),
draggableThreadKeys: draggable,
activeReorderableThreadKeys: activeReorderable,
activeThreads:
optimisticDrop?.section !== "active" || optimisticDrop.order === null
? sortedActive
: orderItemsByPreferredIds({
items: sortedActive,
preferredIds: optimisticDrop.order,
getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
}),
// Soonest wake first: "what comes back next" is the shelf's question.
snoozedThreads: snoozed.toSorted(
(left, right) =>
firstValidTimestampMs(left.snoozedUntil ?? null) -
firstValidTimestampMs(right.snoozedUntil ?? null),
),
settledThreads: sortSettledThreadsForSidebar(settled),
snoozeNow: preciseNow,
};
}, [nowMinute, optimisticDrop, scopedProjectKeys, serverConfigs, snoozeWakeTick, threads]);
const threadSearchInputRef = useRef<HTMLInputElement>(null);
const [threadSearchQuery, setThreadSearchQuery] = useState("");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.