# Files changed · PR \#23124 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23124/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[perf\(web\): visit marks during a reply no longer re-render ChatView](https://git.cafe/pingdotgg/t3code/pull/23124.md)

Version: 23

Head commit: 9cdd2360b269fa2045b8e29816d144a69402d2fe

Base commit: 0c8b6515df8d597e318ee8f4f3d30da07d84eea7

Comparison base: 0c8b6515df8d597e318ee8f4f3d30da07d84eea7

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/ChatView.logic.ts

Status: modified; +7; −2

```diff
@@ -1629,14 +1629,18 @@
 
 /**
  * A thread shell built from its loaded detail, for direct links that hydrate the detail first.
- * `updatedAt` is passed live: the thread structure leaves it out.
+ * `updatedAt` and `lastVisitedAt` are passed live: the thread structure leaves them out.
  */
 export function presentThreadShellFromProjection(
   environmentId: EnvironmentId,
   threadId: ThreadId,
   projection: ThreadStructure,
+  live: {
+    readonly updatedAt: ThreadProjection["updatedAt"];
+    readonly lastVisitedAt: ThreadProjection["thread"]["lastVisitedAt"];
-  updatedAt: ThreadProjection["updatedAt"],
+  },
 ) {
+  const { updatedAt, lastVisitedAt } = live;
   const latestRun = deriveLatestThreadRun(projection);
   const runtime = deriveThreadRuntime({ ...projection, updatedAt });
   const pendingRuntimeRequest =
@@ -1645,6 +1649,7 @@
   return {
     ...presentThreadShell(environmentId, {
       ...projection.thread,
+      lastVisitedAt,
       latestRunId: latestRun?.runId ?? null,
       activeRunId: runtime?.activeRunId ?? null,
       status: runtime?.status ?? "idle",
```

## apps/web/src/components/ChatView.tsx

Status: modified; +9; −10

```diff
@@ -236,6 +236,7 @@
   useProject,
   useQuietThreadProjection,
   useThreadUpdatedAt,
+  useThreadLastVisitedAt,
   useQuietThreadVisibleTurnItems,
   useThreadStatus,
   useThreadHistory,
@@ -464,19 +465,17 @@
   });
   const serverThreadProjection = useQuietThreadProjection(routeThreadDetailRef);
   const serverProjection = serverThreadProjection?.projection ?? null;
-  // Only the shell-less fallback needs it; with a shell, the shell carries it.
-  const fallbackUpdatedAt = useThreadUpdatedAt(
-    serverThreadShell === null ? routeThreadDetailRef : null,
-  );
+  // Only the shell-less fallback needs these; with a shell, the shell carries them.
+  const fallbackDetailRef = serverThreadShell === null ? routeThreadDetailRef : null;
+  const fallbackUpdatedAt = useThreadUpdatedAt(fallbackDetailRef);
+  const fallbackLastVisitedAt = useThreadLastVisitedAt(fallbackDetailRef);
   const serverThread =
     serverThreadShell !== null || serverProjection === null || fallbackUpdatedAt === null
       ? serverThreadShell
+      : presentThreadShellFromProjection(environmentId, threadId, serverProjection, {
+          updatedAt: fallbackUpdatedAt,
+          lastVisitedAt: fallbackLastVisitedAt,
+        });
-      : presentThreadShellFromProjection(
-          environmentId,
-          threadId,
-          serverProjection,
-          fallbackUpdatedAt,
-        );
   const promptRef = useRef("");
   const localComposerRef = useRef<ChatComposerHandle | null>(null);
   const composerRef = useComposerHandleContext() ?? localComposerRef;
```

## apps/web/src/lib/contextWindow.ts

Status: modified; +2; −3

```diff
@@ -1,9 +1,8 @@
-import type { ThreadStructureTurnItem } from "@t3tools/client-runtime/state/shell";
+import type { ThreadStructure, ThreadStructureTurnItem } from "@t3tools/client-runtime/state/shell";
 import type {
   OrchestrationV2ProviderTurn,
   OrchestrationV2ProviderTurnTokenUsage,
   OrchestrationV2ProviderThread,
-  OrchestrationV2ThreadProjection,
   ThreadTokenUsageSnapshot,
 } from "@t3tools/contracts";
 import * as DateTime from "effect/DateTime";
@@ -29,7 +28,7 @@
     by provider thread, not time, so pick by timestamp. */
 export function latestProviderTurnTokenUsage(
   turns: ReadonlyArray<Pick<OrchestrationV2ProviderTurn, "tokenUsage">>,
-  projection?: Pick<OrchestrationV2ThreadProjection, "thread" | "contextTransfers"> | null,
+  projection?: Pick<ThreadStructure, "thread" | "contextTransfers"> | null,
   providerThread?: Pick<OrchestrationV2ProviderThread, "contextUsage"> | null,
 ): OrchestrationV2ProviderTurnTokenUsage | null {
   let latest: OrchestrationV2ProviderTurnTokenUsage | null = null;
```

## apps/web/src/state/entities.ts

Status: modified; +10; −0

```diff
@@ -65,6 +65,9 @@
 const EMPTY_CHECKPOINT_SUMMARIES_ATOM = Atom.make<ReadonlyArray<ThreadCheckpointSummary>>(
   Object.freeze([]),
 ).pipe(Atom.withLabel("web-thread-checkpoint-summaries:empty"));
+const EMPTY_LAST_VISITED_AT_ATOM = Atom.make<DateTime.Utc | null>(null).pipe(
+  Atom.withLabel("web-thread-last-visited:empty"),
+);
 const EMPTY_UPDATED_AT_ATOM = Atom.make<DateTime.Utc | null>(null).pipe(
   Atom.withLabel("web-thread-updated-at:empty"),
 );
@@ -171,6 +174,13 @@
   );
 }
 
+/** The detail's live visit mark, which the thread structure leaves out. */
+export function useThreadLastVisitedAt(ref: ScopedThreadRef | null): DateTime.Utc | null {
+  return useAtomValue(
+    ref === null ? EMPTY_LAST_VISITED_AT_ATOM : environmentThreadDetails.lastVisitedAtAtom(ref),
+  );
+}
+
 /** The detail's live `updatedAt`, which the thread structure leaves out. */
 export function useThreadUpdatedAt(ref: ScopedThreadRef | null): DateTime.Utc | null {
   return useAtomValue(
```

## packages/client-runtime/src/state/models.ts

Status: modified; +7; −1

```diff
@@ -58,10 +58,16 @@
  * reader of the quiet thread cannot depend on them and go stale. A full
  * projection is assignable to it.
  */
+export type ThreadStructureThread = Omit<
+  OrchestrationV2ThreadProjection["thread"],
+  "updatedAt" | "lastVisitedAt"
+>;
+
 export type ThreadStructure = Omit<
   OrchestrationV2ThreadProjection,
-  "turnItems" | "visibleTurnItems" | "updatedAt"
+  "thread" | "turnItems" | "visibleTurnItems" | "updatedAt"
 > & {
+  readonly thread: ThreadStructureThread;
   readonly turnItems: ReadonlyArray<ThreadStructureTurnItem>;
   readonly visibleTurnItems: ReadonlyArray<ThreadStructureVisibleTurnItem>;
 };
```

## packages/client-runtime/src/state/threadDetail.test.ts

Status: modified; +39; −0

```diff
@@ -76,6 +76,16 @@
   it("sees other fields change on a streaming item", () => {
     const next = withItem({ ...assistant, text: "Hello", title: "Answer" });
     expect(sameThreadStructure(before, next)).toBe(false);
+  });
+
+  it("holds through a visit mark", () => {
+    const thread = { ...before.thread, lastVisitedAt: later, updatedAt: later };
+    expect(sameThreadStructure(before, { ...before, thread })).toBe(true);
+  });
+
+  it("sees other thread changes", () => {
+    const thread = { ...before.thread, title: "Renamed" };
+    expect(sameThreadStructure(before, { ...before, thread })).toBe(false);
   });
 
   it("sees changes outside the turn items", () => {
@@ -83,6 +93,33 @@
   });
 });
 
+describe("lastVisitedAtAtom", () => {
+  it("follows a visit mark the quiet thread holds through", () => {
+    const projection = withItem(assistant);
+    const state = Atom.make(
+      AsyncResult.success({ ...EMPTY_ENVIRONMENT_THREAD_STATE, data: Option.some(projection) }),
+    );
+    const details = createEnvironmentThreadDetailAtoms(() => state);
+    const ref = { environmentId: EnvironmentId.make("env"), threadId: v2ThreadId };
+    const registry = AtomRegistry.make();
+    const unmountQuiet = registry.mount(details.quietThreadAtom(ref));
+    const unmountVisit = registry.mount(details.lastVisitedAtAtom(ref));
+    const quietBefore = registry.get(details.quietThreadAtom(ref));
+
+    const visited = { ...projection, thread: { ...projection.thread, lastVisitedAt: later } };
+    registry.set(
+      state,
+      AsyncResult.success({ ...EMPTY_ENVIRONMENT_THREAD_STATE, data: Option.some(visited) }),
+    );
+
+    expect(registry.get(details.quietThreadAtom(ref))).toBe(quietBefore);
+    expect(registry.get(details.lastVisitedAtAtom(ref))).toBe(later);
+    unmountQuiet();
+    unmountVisit();
+    registry.dispose();
+  });
+});
+
 describe("quietThreadAtom", () => {
   const ref = { environmentId: EnvironmentId.make("environment-1"), threadId: v2ThreadId };
 
@@ -125,6 +162,8 @@
     const thread = setup(withItem(assistant));
     const projection = thread.read()?.projection;
     expect(projection).not.toHaveProperty("updatedAt");
+    expect(projection?.thread).not.toHaveProperty("updatedAt");
+    expect(projection?.thread).not.toHaveProperty("lastVisitedAt");
     expect(projection?.turnItems[0]).not.toHaveProperty("text");
     expect(projection?.turnItems[0]).not.toHaveProperty("updatedAt");
     expect(projection?.visibleTurnItems[0]?.item).not.toHaveProperty("text");
```

## packages/client-runtime/src/state/threadDetail.ts

Status: modified; +57; −9

```diff
@@ -1,4 +1,5 @@
 import {
+  OrchestrationV2AppThread,
   OrchestrationV2TurnItem,
   type OrchestrationV2ThreadProjection,
   type ScopedThreadRef,
@@ -21,6 +22,7 @@
   EnvironmentThread,
   EnvironmentThreadStructure,
   ThreadStructure,
+  ThreadStructureThread,
   ThreadStructureTurnItem,
   ThreadStructureVisibleTurnItem,
 } from "./models.ts";
@@ -41,6 +43,7 @@
 
 type TurnItem = OrchestrationV2ThreadProjection["turnItems"][number];
 type VisibleTurnItem = OrchestrationV2ThreadProjection["visibleTurnItems"][number];
+type AppThread = OrchestrationV2ThreadProjection["thread"];
 
 // Items are stripped once each: unchanged items keep their identity across
 // structure changes, which the incremental timeline relies on.
@@ -76,35 +79,55 @@
   return row.item.type !== "assistant_message" && row.item.type !== "reasoning";
 }
 
+// The open thread is marked visited several times while a reply streams. Visit
+// state is read from the shell, or from lastVisitedAtAtom while the thread is
+// not in the shell index yet, so a mark is not structure.
+function toThreadStructureThread(thread: AppThread): ThreadStructureThread {
+  const { updatedAt: _updatedAt, lastVisitedAt: _lastVisitedAt, ...structure } = thread;
+  return structure;
+}
+
 function toThreadStructure(projection: OrchestrationV2ThreadProjection): ThreadStructure {
   const { updatedAt: _updatedAt, ...structure } = projection;
   return {
     ...structure,
+    thread: toThreadStructureThread(projection.thread),
     turnItems: projection.turnItems.map(toThreadStructureTurnItem),
     visibleTurnItems: projection.visibleTurnItems.map(toThreadStructureVisibleTurnItem),
   };
 }
 
 const sameTurnItem = Schema.toEquivalence(OrchestrationV2TurnItem);
+const sameAppThread = Schema.toEquivalence(OrchestrationV2AppThread);
 
+// Overlays previous's structure on next, so the fields the strip functions
+// remove are exactly the ones ignored here.
+function sameApartFromStripped<T extends object>(
+  same: (left: T, right: T) => boolean,
-// Compares through toThreadStructureTurnItem, so the fields it strips are
-// exactly the ones ignored here.
+  previous: T,
+  previousStructure: object,
+  next: T,
+): boolean {
+  return (
+    Object.keys(previous).length === Object.keys(next).length &&
+    same(Object.assign({}, next, previousStructure), next)
+  );
+}
+
 function sameTurnItemStructure(previous: TurnItem, next: TurnItem): boolean {
   if (previous === next) return true;
   const structure = toThreadStructureTurnItem(previous);
   if (previous.id !== next.id || previous.type !== next.type || structure === previous) {
     return false;
   }
-  return (
-    Object.keys(previous).length === Object.keys(next).length &&
-    sameTurnItem(Object.assign({}, next, structure), next)
-  );
+  return sameApartFromStripped(sameTurnItem, previous, structure, next);
 }
 
 /**
  * Whether `previous` and `next` have the same ThreadStructure: they differ
+ * only by text streamed into existing assistant and reasoning items or by a
+ * visit mark. The same items exist, with the same status, and every other
- * only by text streamed into existing assistant and reasoning items. The same
+ * field is unchanged.
- * items exist, with the same status, and every other field is unchanged.
  */
 export function sameThreadStructure(
   previous: OrchestrationV2ThreadProjection,
@@ -112,10 +135,28 @@
 ): boolean {
   if (previous === next) return true;
   for (const key of Object.keys(next) as Array<keyof OrchestrationV2ThreadProjection>) {
+    if (
+      key === "thread" ||
+      key === "turnItems" ||
-    if (key === "turnItems" || key === "visibleTurnItems" || key === "updatedAt") continue;
+      key === "visibleTurnItems" ||
+      key === "updatedAt"
+    ) {
+      continue;
+    }
     if (previous[key] !== next[key]) return false;
   }
   if (
+    previous.thread !== next.thread &&
+    !sameApartFromStripped(
+      sameAppThread,
+      previous.thread,
+      toThreadStructureThread(previous.thread),
+      next.thread,
+    )
+  ) {
+    return false;
+  }
+  if (
     previous.turnItems.length !== next.turnItems.length ||
     previous.visibleTurnItems.length !== next.visibleTurnItems.length
   ) {
@@ -304,6 +345,12 @@
     }).pipe(Atom.setIdleTTL(0), Atom.withLabel(`environment-thread-checkpoint-summaries:${key}`));
   });
 
+  const lastVisitedAtAtomFamily = Atom.family((key: string) =>
+    Atom.make(
+      (get) =>
+        Option.getOrNull(get(threadStateValueAtomFamily(key)).data)?.thread.lastVisitedAt ?? null,
+    ).pipe(Atom.setIdleTTL(0), Atom.withLabel(`environment-thread-last-visited:${key}`)),
+  );
   const updatedAtAtomFamily = Atom.family((key: string) =>
     Atom.make(
       (get) => Option.getOrNull(get(threadStateValueAtomFamily(key)).data)?.updatedAt ?? null,
@@ -372,6 +419,7 @@
   return {
     worktreePathAtom: (ref: ScopedThreadRef) => worktreePathAtomFamily(threadKey(ref)),
     updatedAtAtom: (ref: ScopedThreadRef) => updatedAtAtomFamily(threadKey(ref)),
+    lastVisitedAtAtom: (ref: ScopedThreadRef) => lastVisitedAtAtomFamily(threadKey(ref)),
     pendingRequestsAtom: (ref: ScopedThreadRef) => pendingRequestsAtomFamily(threadKey(ref)),
     queueWorkflowAtom: (ref: ScopedThreadRef) => queueWorkflowAtomFamily(threadKey(ref)),
     queuedCountAtom: (ref: ScopedThreadRef) => queuedCountAtomFamily(threadKey(ref)),
```
