Part VI · Client architectures: shared semantics, platform edgesWeb product surfaces
Chapter 31source checked

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.

apps/web/src/components/Sidebar.tsx:2554–2610 ↗verbatim · tsx · 587d2e08
  // 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);
Read this as: Pinned ordering has a local override with observed keys and explicit release conditions for concurrent or canonical changes.

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.

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.

Figure 31.1 · One canonical thread, six focused projectionschange the durable scenario, then compare what each surface consumes and suppresses
Interactive projection lab · Chapter 31

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

Canonical threadthread_31 / durable fields
threadIdmessagessession.statusactivitiesdraftpinnedAt

A running session and a local draft coexist; the draft is not a durable message yet.

Chat timelineConversation first: lifecycle details remain compact activity affordances.

Consumes

  • messages
  • session.status
  • activities

Intentionally suppresses

  • task.latest
  • review.comments
  • orderedAt
Server-state controlsSend message · interrupt turn
Local-view controlsScroll position · collapsed activity
Static projection map
SurfaceCanonical fields it consumesLocal view state
Chat timelinemessages · session.status · activitiesScroll position · collapsed activity
Composer + optimistic statedraft · messages · session.statusDraft text · optimistic pending row
Quiet work logactivities · task.latest · session.statusExpanded rows · filter
Agents / subagentstask.latest · activities · session.statusRoster grouping · selected agent
Review / diffturn.diff · checkpoint · review.commentsSelected lines · diff mode
Ordered / pinned sidebarthreadId · pinnedAt · orderedAt · messagesSort 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.

Figure 31.1. Chat, composer, quiet work log, Agents, review, and sidebar select different fields and expose different local controls around one environment-scoped thread identity. The lab separates server-state mutations from view-local state and makes intentional suppression visible; a quiet row is therefore not evidence that its underlying task activity disappeared.
T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.