diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 526ac86a6c9ad..03f1903ae4b99 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -43,6 +43,7 @@ import ThreadSidebar from "./Sidebar"; import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; import { SidebarBrandWidthProbe, SidebarChromeHeader } from "./sidebar/SidebarChrome"; import { MainAppLocationTracker } from "./sidebar/mainAppLocation"; +import type { SidebarRouteReveal } from "./sidebar/routeReveal"; import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; import { useProjects } from "../state/entities"; import { @@ -330,6 +331,10 @@ function ProjectProjectionRetention() { export function AppSidebarLayout({ children }: { children: ReactNode }) { const navigate = useNavigate(); const legacySidebarEnabled = useLegacySidebarEnabled(); + const [sidebarRouteReveal] = useState(() => ({ + observedKey: null, + pendingKey: null, + })); const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = usePanelAnimationSettings(); useThreadVisitedMigration(); @@ -422,9 +427,9 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { ) : legacySidebarEnabled ? ( - + ) : ( - + )} diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 9076b9c127ce5..35c8cae548ac1 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -223,6 +223,11 @@ import { } from "../sidebarProjectGrouping"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; import { useThreadRouteTarget } from "../hooks/useThreadRouteTarget"; +import { + completeSidebarRouteReveal, + pendingSidebarRouteReveal, + type SidebarRouteReveal, +} from "./sidebar/routeReveal"; const SIDEBAR_SORT_LABELS: Record = { updated_at: "Last user message", created_at: "Created at", @@ -1199,20 +1204,26 @@ function useProjectThreadPanel(input: { (settings) => settings.sidebarThreadPreviewCount, ); const activeThreadKey = activeRouteThreadKey ?? undefined; - const pinnedCollapsedThread = - !activeThreadKey || projectExpanded - ? null - : (visibleProjectThreads.find( - (thread) => - scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === activeThreadKey, - ) ?? null); - const hasOverflowingThreads = visibleProjectThreads.length > sidebarThreadPreviewCount; + const activeThread = activeThreadKey + ? (visibleProjectThreads.find( + (thread) => + scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === activeThreadKey, + ) ?? null) + : null; + const pinnedCollapsedThread = projectExpanded ? null : activeThread; + // The open thread stays listed past the preview cut, as it does in a + // collapsed project, so it alone does not overflow the preview. + const hasOverflowingThreads = visibleProjectThreads.some( + (thread, index) => + index >= sidebarThreadPreviewCount && + scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) !== activeThreadKey, + ); const previewThreads = isThreadListExpanded || !hasOverflowingThreads ? visibleProjectThreads : visibleProjectThreads.slice(0, sidebarThreadPreviewCount); const visibleThreadKeys = new Set( - [...previewThreads, ...(pinnedCollapsedThread ? [pinnedCollapsedThread] : [])].map((thread) => + [...previewThreads, ...(activeThread ? [activeThread] : [])].map((thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), ), ); @@ -3202,10 +3213,17 @@ function useLegacySidebarThreads(input: { isThreadListExpanded || !hasOverflowingThreads ? projectThreads : projectThreads.slice(0, sidebarThreadPreviewCount); - const renderedThreads = pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreads; - return renderedThreads.map((thread) => - scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), - ); + if (pinnedCollapsedThread) { + return [ + scopedThreadKey( + scopeThreadRef(pinnedCollapsedThread.environmentId, pinnedCollapsedThread.id), + ), + ]; + } + return projectThreads.flatMap((thread, index) => { + const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); + return index < previewThreads.length || threadKey === activeThreadKey ? [threadKey] : []; + }); }); const threadJumpCommandByKey = new Map< string, @@ -3234,7 +3252,7 @@ function useLegacySidebarThreads(input: { }; } -export default function LegacySidebar() { +export default function LegacySidebar({ routeReveal }: { routeReveal: SidebarRouteReveal }) { const reorderProjects = useUiStateStore((store) => store.reorderProjects); const navigate = useNavigate(); const sidebarThreadSortOrder = useClientSettings((s) => s.sidebarThreadSortOrder); @@ -3298,6 +3316,25 @@ export default function LegacySidebar() { sidebarThreadPreviewCount, expandedThreadListsByProject, }); + // History and the command palette can open a thread scrolled out of view, + // and a remount starts the list at the top, so each route change and each + // mount scroll to the open thread once. Its row can mount after the route + // does, once its environment's shells arrive, so the reveal waits for it. + const mountRevealOwed = useRef(true); + const routeRowRendered = + routeThreadKey !== null && visibleSidebarThreadKeys.includes(routeThreadKey); + const revealRouteRow = useStableCallback(() => { + const pendingKey = pendingSidebarRouteReveal(routeReveal, routeThreadKey); + if (pendingKey === null && (!mountRevealOwed.current || routeThreadKey === null)) return; + const activeRow = document.querySelector( + '[data-sidebar="content"] [data-thread-item] [data-active="true"]', + ); + if (!activeRow) return; + completeSidebarRouteReveal(routeReveal); + mountRevealOwed.current = false; + activeRow.scrollIntoView({ block: "nearest" }); + }); + useEffect(revealRouteRow, [revealRouteRow, routeThreadKey, routeRowRendered]); const newThreadShortcutLabelOptions = { platform, context: { diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 18197317cd192..0485e93ff74c4 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -44,6 +44,7 @@ import { type Thread, } from "../types"; import { makeThreadFixture, type ThreadFixtureOverrides } from "../test-fixtures"; +import { completeSidebarRouteReveal, pendingSidebarRouteReveal } from "./sidebar/routeReveal"; const localEnvironmentId = EnvironmentId.make("environment-local"); @@ -1216,3 +1217,24 @@ describe("Working shelf (beta)", () => { }); }); }); + +describe("pendingSidebarRouteReveal", () => { + it("keeps a revealed thread settled across a mount that reads no route params", () => { + const reveal = { observedKey: null, pendingKey: null }; + expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBe("env:a1"); + completeSidebarRouteReveal(reveal); + + // Back from Settings: location commits before the thread match does. + expect(pendingSidebarRouteReveal(reveal, null)).toBeNull(); + expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBeNull(); + + expect(pendingSidebarRouteReveal(reveal, "env:b1")).toBe("env:b1"); + }); + + it("keeps a pending reveal through a render without route params", () => { + const reveal = { observedKey: null, pendingKey: null }; + pendingSidebarRouteReveal(reveal, "env:a1"); + expect(pendingSidebarRouteReveal(reveal, null)).toBeNull(); + expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBe("env:a1"); + }); +}); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 9447ce0ba6611..7f03b6c5c7857 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -179,6 +179,11 @@ import { resolveSnoozePresets, snoozeWakeLabel, type SnoozePreset } from "./Side import { ProjectFavicon } from "./ProjectFavicon"; import { deriveProviderEntriesByEnvironment } from "../providerInstances"; import { useStableCallback } from "../hooks/useStableCallback"; +import { + completeSidebarRouteReveal, + pendingSidebarRouteReveal, + type SidebarRouteReveal, +} from "./sidebar/routeReveal"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { Button } from "./ui/button"; import { @@ -741,7 +746,7 @@ const SIDEBAR_DRAG_DISTANCE = 6; const SIDEBAR_DRAG_LABEL_HEIGHT = 24; -export default function Sidebar() { +export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReveal }) { const { projects, serverConfigs, @@ -1061,6 +1066,44 @@ export default function Sidebar() { }); const isSearchingThreads = threadSearchQuery.trim().length > 0; + + // History, the command palette, and links can open a thread the project + // scope hides or the list has scrolled away. A remount starts the list at + // the top, so each mount also scrolls to the open thread once, without + // widening the scope. + const mountRevealOwed = useRef(true); + const revealRouteRow = useStableCallback(() => { + const pendingKey = pendingSidebarRouteReveal(routeReveal, routeRowThreadKey); + // Search results replace the thread list; reveal once the list returns. + if (isSearchingThreads) return; + const revealKey = pendingKey ?? (mountRevealOwed.current ? routeRowThreadKey : null); + if (revealKey === null) return; + const thread = threads.find((candidate) => threadShellKey(candidate) === revealKey); + if (thread === undefined) return; + if ( + pendingKey !== null && + scopedProjectKeys !== null && + filterSidebarV2VisibleThreads([thread], scopedProjectKeys).length === 0 && + filterSidebarV2VisibleThreads([thread], null).length === 1 + ) { + // The row renders under the widened scope; scroll on that pass. + setProjectScopeKey(null); + return; + } + const activeRow = document.querySelector('[data-sidebar="content"] [aria-current="page"]'); + if (!activeRow) return; + completeSidebarRouteReveal(routeReveal); + mountRevealOwed.current = false; + activeRow.scrollIntoView({ block: "nearest" }); + }); + useEffect(revealRouteRow, [ + revealRouteRow, + routeRowThreadKey, + threads, + projectScopeKey, + isSearchingThreads, + ]); + const threadSearchMatchByKey = new Map( threadSearch.matches.map((match) => [threadSearchMatchKey(match), match] as const), ); diff --git a/apps/web/src/components/sidebar/routeReveal.ts b/apps/web/src/components/sidebar/routeReveal.ts new file mode 100644 index 0000000000000..d1be939ee7226 --- /dev/null +++ b/apps/web/src/components/sidebar/routeReveal.ts @@ -0,0 +1,28 @@ +// Owned by AppSidebarLayout because the narrow drawer and Settings unmount the +// thread sidebar without changing the route. Only a route change requests a +// reveal that may widen the project scope, so a scope chosen while one thread +// stays open survives a remount. Scroll position does not survive one. +// +// A null key is not an observation: leaving Settings commits the router's +// location before its matches, so the sidebar's first render can read no +// thread params. Recording that would make the same thread look new. +export interface SidebarRouteReveal { + observedKey: string | null; + pendingKey: string | null; +} + +export function pendingSidebarRouteReveal( + reveal: SidebarRouteReveal, + routeKey: string | null, +): string | null { + if (routeKey === null) return null; + if (reveal.observedKey !== routeKey) { + reveal.observedKey = routeKey; + reveal.pendingKey = routeKey; + } + return reveal.pendingKey; +} + +export function completeSidebarRouteReveal(reveal: SidebarRouteReveal): void { + reveal.pendingKey = null; +} diff --git a/knip.jsonc b/knip.jsonc index 7fbfe38259951..babc22932ca0d 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -22,6 +22,8 @@ // Windows verify script with esbuild through a path knip does not trace, and // electron-builder loads the macOS signer by path. "entry": [ + // These fixture scripts are injected into a browser page by path. + "fixtures/issue-16534/*.js", "knip-schemas.ts", "mobile-native-client.ts", "setup-worktree.ts", diff --git a/scripts/fixtures/issue-16534/README.md b/scripts/fixtures/issue-16534/README.md new file mode 100644 index 0000000000000..0ec2a42943be1 --- /dev/null +++ b/scripts/fixtures/issue-16534/README.md @@ -0,0 +1,32 @@ +These fixtures reconstruct synthetic sidebar projections and client reset inputs. +They never start a provider or read personal application data. Projection-only +fixtures prove rendering/navigation, not event-sourced backend behavior. + +Generate with `python3 scripts/fixtures/issue-16534/seed.py --output /tmp/sidebar-seed +--variant all`. Stop the isolated server before applying reset-threads.sql, +archive-unrelated-threads.sql, hide-v1-threads.sql, hide-unrelated-projects.sql, +projects.sql, threads.sql, messages.sql, turn-items.sql, transcript-runs.sql, +transcript-messages.sql and transcript-turn-items.sql, in that order, with the documented +`t3-sqlite-state.ts exec` helper, once per file. Never target the live T3 home. + +`all` includes two eight-thread projects, forty spacers, child/grandchild/fork +lineage and synthetic markdown navigation messages. Other variants isolate shelf, +archive and lineage states. `fixture.json` records deterministic clock/settings. +The replay option records stopped synthetic shell representations for working, +status and delayed arrival without spawning providers. `replay.js` transforms +only real shell frames before the real RPC decoder/reducer receives them; delayed +shell frames are released explicitly through `__sidebarReplay.release()`. + +Install bootstrap.js and replay.js as identical pre-module scripts in the served +HTML only for capture. Remove both before committing or switching checkouts. +Use a fresh navigation, never document.write over an evaluated module graph. +Configure `sessionStorage['sidebar-capture-config']` with `{storage, replay}`. +Clear only projection caches (shell/thread/vcs-refs in t3code:connection-runtime) +from a same-origin inert document before navigating to the app. Preserve auth. +Timers and performance.now remain native; only Date is frozen. Record the exact +injection externally with the artifacts. + +For grouping, initialize the three synthetic /tmp/triage-16534-{A,B,Spacer} +paths as git repositories with the same synthetic origin, then use repository +grouping and an explicit separate override for Spacer. No network fetch needed. +All navigation links are synthetic t3-thread links rendered by the real client. diff --git a/scripts/fixtures/issue-16534/bootstrap.js b/scripts/fixtures/issue-16534/bootstrap.js new file mode 100644 index 0000000000000..f95bfbb016b66 --- /dev/null +++ b/scripts/fixtures/issue-16534/bootstrap.js @@ -0,0 +1,23 @@ +(() => { + const NativeDate = Date, + epoch = NativeDate.parse("2026-10-22T00:00:00.000Z"); + globalThis.Date = new Proxy(NativeDate, { + apply() { + return new NativeDate(epoch).toString(); + }, + construct(target, args, newTarget) { + return Reflect.construct(target, args.length ? args : [epoch], newTarget); + }, + get(target, property, receiver) { + return property === "now" ? () => epoch : Reflect.get(target, property, receiver); + }, + }); + globalThis.__sidebarCaptureClock = { + epoch, + readyState: document.readyState, + moduleScripts: document.querySelectorAll('script[type="module"]').length, + }; + const config = JSON.parse(sessionStorage.getItem("sidebar-capture-config") || "{}"); + for (const [key, value] of Object.entries(config.storage || {})) + localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value)); +})(); diff --git a/scripts/fixtures/issue-16534/plain.json b/scripts/fixtures/issue-16534/plain.json new file mode 100644 index 0000000000000..c847fcd56de43 --- /dev/null +++ b/scripts/fixtures/issue-16534/plain.json @@ -0,0 +1,483 @@ +{ + "clock": "2026-10-22T00:00:00.000Z", + "settings": { + "legacySidebarEnabled": false, + "sidebarProjectGroupingMode": "separate", + "sidebarProjectSortOrder": "manual", + "sidebarThreadPreviewCount": 6, + "sidebarThreadSortOrder": "updated_at" + }, + "threads": [ + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:01:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-1", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-1" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 1", + "updatedAt": "2026-10-21T20:01:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:02:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-2", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-2" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 2", + "updatedAt": "2026-10-21T20:02:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:03:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-3", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-3" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 3", + "updatedAt": "2026-10-21T20:03:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:04:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-4", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-4" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 4", + "updatedAt": "2026-10-21T20:04:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:05:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-5", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-5" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 5", + "updatedAt": "2026-10-21T20:05:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:06:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-6", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-6" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 6", + "updatedAt": "2026-10-21T20:06:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:07:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-7", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-7" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 7", + "updatedAt": "2026-10-21T20:07:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:08:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-A-8", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-A-8" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-A", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage A thread 8", + "updatedAt": "2026-10-21T20:08:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:01:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-1", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-1" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 1", + "updatedAt": "2026-10-21T20:01:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:02:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-2", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-2" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 2", + "updatedAt": "2026-10-21T20:02:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:03:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-3", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-3" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 3", + "updatedAt": "2026-10-21T20:03:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:04:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-4", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-4" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 4", + "updatedAt": "2026-10-21T20:04:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:05:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-5", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-5" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 5", + "updatedAt": "2026-10-21T20:05:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:06:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-6", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-6" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 6", + "updatedAt": "2026-10-21T20:06:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:07:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-7", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-7" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 7", + "updatedAt": "2026-10-21T20:07:00.000Z", + "worktreePath": null + }, + { + "activeProviderThreadId": null, + "archivedAt": null, + "branch": null, + "createdAt": "2026-10-21T20:08:00.000Z", + "createdBy": "user", + "creationSource": "web", + "deletedAt": null, + "forkedFrom": null, + "id": "triage-B-8", + "interactionMode": "default", + "lastVisitedAt": null, + "lineage": { + "parentThreadId": null, + "relationshipToParent": null, + "rootThreadId": "triage-B-8" + }, + "modelSelection": { "instanceId": "codex", "model": "gpt-6.1-sol" }, + "projectId": "triage-project-B", + "providerInstanceId": "codex", + "runtimeMode": "full-access", + "settledAt": null, + "settledOverride": null, + "snoozedAt": null, + "snoozedUntil": null, + "title": "Triage B thread 8", + "updatedAt": "2026-10-21T20:08:00.000Z", + "worktreePath": null + } + ], + "uiState": { + "projectExpandedById": {}, + "projectOrder": [], + "sidebarProjectScopeKey": null, + "threadLastVisitedAtById": {} + }, + "variant": "plain" +} diff --git a/scripts/fixtures/issue-16534/replay.js b/scripts/fixtures/issue-16534/replay.js new file mode 100644 index 0000000000000..8e1594482ec38 --- /dev/null +++ b/scripts/fixtures/issue-16534/replay.js @@ -0,0 +1,292 @@ +/** Capture-only protocol fixture. Install before modules; never import in product code. + * Pass options through sessionStorage sidebar-capture-config.replay. Real server + * frames still travel through the real RPC decoder and client reducers. + */ +(() => { + const options = JSON.parse(sessionStorage.getItem("sidebar-capture-config") || "{}").replay || {}; + const NativeWebSocket = WebSocket; + const held = []; + globalThis.__sidebarWire = []; + const frames = []; + globalThis.__sidebarReplay = { + frames, + held, + release() { + if (delayedThread && delayedStream) { + releaseThread(delayedStream, delayedThread); + delayedThread = null; + delayedStream = null; + } + for (const run of held.splice(0)) run(); + }, + }; + + let delayedThread = null; + let delayedStream = null; + let latestShellStream = null; + let shellSequence = 0; + const releaseThread = (stream, thread, emit = stream.emit) => { + // Insert one fixture event into this subscription's sequence space; later + // server events must carry the same offset or the reducer drops the next one. + stream.offset++; + emit({ + kind: "thread.updated", + sequence: ++stream.sequence, + location: "active", + thread, + }); + }; + const parseFrame = (data) => { + try { + const value = JSON.parse(data); + const ndjson = data.endsWith("\n") && !Array.isArray(value); + return { value: ndjson ? [value] : value, ndjson }; + } catch { + return { + value: data + .trim() + .split("\n") + .map((line) => JSON.parse(line)), + ndjson: true, + }; + } + }; + const transformSnapshot = ( + snapshot, + hold = (thread) => { + delayedThread = thread; + }, + ) => { + snapshot.threads = snapshot.threads.filter( + (t) => + t.id.startsWith("triage-") && (options.lineage || !/^triage-A-(9|10|11|12)$/.test(t.id)), + ); + if (options.shelf === "settled") { + const source = snapshot.threads.find((t) => t.id === "triage-A-8"); + if (source) + for (let i = 13; i <= 26; i++) + snapshot.threads.push({ + ...source, + id: "triage-A-" + i, + title: "Triage A thread " + i, + latestMessageAt: "2026-10-21T23:" + String(i).padStart(2, "0") + ":00.000Z", + updatedAt: "2026-10-21T23:" + String(i).padStart(2, "0") + ":00.000Z", + }); + } + snapshot.projects = snapshot.projects?.filter((p) => p.id?.startsWith("triage-")) || []; + for (const thread of snapshot.threads) { + if (thread.id === "triage-A-1" && options.shelf) { + Object.assign(thread, { + pinnedAt: null, + snoozedUntil: null, + settledOverride: null, + settledAt: null, + }); + if (options.shelf === "pinned") + Object.assign(thread, { pinnedAt: "2026-10-22T00:00:00.000Z", pinOrderKey: "a0" }); + if (options.shelf === "snoozed") + Object.assign(thread, { + snoozedUntil: "2099-01-01T00:00:00.000Z", + snoozedAt: "2026-10-22T00:00:00.000Z", + }); + if (options.shelf === "working") + Object.assign(thread, { + status: "running", + activeRunId: "triage-run-working", + latestRunId: "triage-run-working", + activityRunStatus: "running", + latestRunRequestedAt: "2026-10-21T23:00:00.000Z", + latestRunStartedAt: "2026-10-21T23:00:00.000Z", + }); + } + if (options.shelf === "settled" && thread.projectId === "triage-project-A") { + Object.assign(thread, { settledOverride: "settled", settledAt: thread.createdAt }); + } + if (options.status && ["triage-A-1", "triage-A-2"].includes(thread.id)) { + Object.assign(thread, { + status: thread.id === "triage-A-1" ? "failed" : "completed", + latestRunId: "triage-run-" + thread.id, + latestRunRequestedAt: "2026-10-21T23:00:00.000Z", + latestRunCompletedAt: "2026-10-21T23:01:00.000Z", + lastVisitedAt: null, + }); + } + } + if (options.delay) { + const target = snapshot.threads.find((t) => t.id === "triage-A-1"); + if (target) hold(target); + snapshot.threads = snapshot.threads.filter((t) => t.id !== "triage-A-1"); + } + return snapshot; + }; + const nativeFetch = fetch; + globalThis.fetch = async (...args) => { + const response = await nativeFetch(...args); + const url = String(args[0]?.url ?? args[0]); + if (!options.enabled || !url.includes("/api/orchestration/shell") || !response.ok) + return response; + const value = await response.clone().json(); + if (!Array.isArray(value.threads)) return response; + frames.push({ + kind: "http.shell", + count: value.threads.length, + sequence: value.snapshotSequence, + }); + shellSequence = value.snapshotSequence; + transformSnapshot(value); + if (delayedThread && latestShellStream) { + delayedStream = latestShellStream; + delayedStream.sequence = Math.max(delayedStream.sequence, shellSequence); + } + return new Response(JSON.stringify(value), { + status: response.status, + headers: { "content-type": "application/json" }, + }); + }; + + globalThis.WebSocket = class extends NativeWebSocket { + send(data) { + if (!options.enabled || typeof data !== "string") return super.send(data); + try { + const { value, ndjson } = parseFrame(data); + let changed = false; + for (const request of Array.isArray(value) ? value : [value]) { + if (request?._tag === "Request" && request.tag === "orchestration.subscribeShell") { + const afterSequence = request.payload?.afterSequence; + // Foreground resumes reuse this socket's client cursor. A new RPC + // session owns a new socket and reloads the authoritative HTTP shell. + const offset = + typeof afterSequence === "number" ? (this.latestShellStream?.offset ?? 0) : 0; + if (offset !== 0) { + request.payload.afterSequence -= offset; + changed = true; + } + const stream = { + sequence: afterSequence ?? shellSequence, + offset, + emit: (node) => + this.replayDeliver({ + _tag: "Chunk", + requestId: request.id, + values: [node], + }), + }; + this.shellStreams.set(request.id, stream); + this.latestShellStream = stream; + latestShellStream = stream; + if (delayedThread && !delayedStream) delayedStream = stream; + } else if ( + request?._tag === "Request" && + request.tag === "orchestration.subscribeThread" + ) { + this.threadRequests.add(request.id); + } + } + if (changed) + data = ndjson + ? value.map((item) => JSON.stringify(item)).join("\n") + "\n" + : JSON.stringify(value); + } catch { + // Non-JSON socket traffic belongs to the native protocol handler. + } + super.send(data); + } + latestShellStream = null; + shellStreams = new Map(); + threadRequests = new Set(); + replayDeliver; + constructor(...args) { + super(...args); + let replaying = false; + this.replayDeliver = (data) => { + replaying = true; + try { + this.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(data) })); + } finally { + replaying = false; + } + }; + this.addEventListener("message", (event) => { + if (replaying) return; + frames.push({ + kind: "frame", + type: typeof event.data, + constructor: event.data?.constructor?.name, + length: event.data?.length, + }); + if (typeof event.data !== "string") return; + let value; + let ndjson; + try { + ({ value, ndjson } = parseFrame(event.data)); + } catch { + frames.push({ kind: "unparsed", length: event.data.length }); + return; + } + globalThis.__sidebarWire.push(value); + frames.push({ + kind: "parsed", + tag: value?._tag, + keys: Object.keys(value || {}), + count: Array.isArray(value) ? value.length : undefined, + }); + let changed = false; + const deliver = (data) => { + replaying = true; + try { + this.dispatchEvent( + new MessageEvent("message", { + data: ndjson + ? data.map((item) => JSON.stringify(item)).join("\n") + "\n" + : JSON.stringify(data), + }), + ); + } finally { + replaying = false; + } + }; + if (options.enabled) { + for (const chunk of Array.isArray(value) ? value : [value]) { + if (chunk?._tag !== "Chunk" || !Array.isArray(chunk.values)) continue; + if (this.threadRequests.has(chunk.requestId)) continue; + let stream = this.shellStreams.get(chunk.requestId); + if (!stream && chunk.values.some((node) => Array.isArray(node?.snapshot?.threads))) { + stream = { sequence: 0, offset: 0 }; + this.shellStreams.set(chunk.requestId, stream); + } + if (!stream) continue; + stream.emit = (node) => { + const update = { ...chunk, values: [node] }; + deliver(Array.isArray(value) ? [update] : update); + }; + for (const node of chunk.values) { + if (typeof node?.sequence === "number") { + node.sequence += stream.offset; + stream.sequence = Math.max(stream.sequence, node.sequence); + changed ||= stream.offset !== 0; + } + if (node?.kind !== "snapshot" || !Array.isArray(node.snapshot?.threads)) continue; + node.snapshot.snapshotSequence += stream.offset; + stream.sequence = Math.max(stream.sequence, node.snapshot.snapshotSequence); + const snapshot = node.snapshot; + frames.push({ + kind: "shell.snapshot", + count: snapshot.threads.length, + sequence: snapshot.snapshotSequence, + }); + transformSnapshot(snapshot, (thread) => { + const emit = stream.emit; + held.push(() => releaseThread(stream, thread, emit)); + }); + changed = true; + } + } + } + if (changed) { + event.stopImmediatePropagation(); + deliver(value); + } + }); + } + }; +})(); diff --git a/scripts/fixtures/issue-16534/seed.py b/scripts/fixtures/issue-16534/seed.py new file mode 100644 index 0000000000000..93aec53c87410 --- /dev/null +++ b/scripts/fixtures/issue-16534/seed.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Generate synthetic sidebar projections without reading personal application data. + +Generation never touches a database. Apply the generated SQL files with the documented SQLite +helper only after the environment owner has stopped the isolated server. These +are projection fixtures, not event-history or provider integration fixtures. +""" +import argparse +import hashlib +import json +from pathlib import Path + +CLOCK = '2026-10-22T00:00:00.000Z' + + +def encoded(value): + return json.dumps(value, sort_keys=True, separators=(',', ':')) + + +def quote(value): + return 'NULL' if value is None else "'" + str(value).replace("'", "''") + "'" + + +def thread(group, number): + key = f'triage-{group}-{number}' + stamp = f'2026-10-21T20:{number:02d}:00.000Z' + return dict(createdBy='user', creationSource='web', id=key, + projectId=f'triage-project-{group}', title=f'Triage {group} thread {number}', + providerInstanceId='codex', modelSelection=dict(instanceId='codex', model='gpt-6.1-sol'), + runtimeMode='full-access', interactionMode='default', branch=None, + worktreePath=None, activeProviderThreadId=None, + lineage=dict(parentThreadId=None, relationshipToParent=None, rootThreadId=key), + forkedFrom=None, createdAt=stamp, updatedAt=stamp, archivedAt=None, + settledOverride=None, settledAt=None, snoozedUntil=None, snoozedAt=None, + lastVisitedAt=None, deletedAt=None) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--variant', choices=['plain', 'spacers', 'pinned', 'snoozed', 'settled', 'lineage', 'archived', 'orphan', 'working', 'status', 'grouped', 'all'], default='plain') + parser.add_argument('--legacy', action='store_true') + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + threads = [thread(group, n) for group in ['A', 'B'] for n in range(1, 9)] + projects = ['A', 'B'] + target = threads[0] + if args.variant in ['spacers', 'all']: + projects.append('Spacer') + threads += [thread('Spacer', n) for n in range(1, 41)] + if args.variant == 'pinned': + target.update(pinnedAt=CLOCK, pinOrderKey='a0') + if args.variant == 'snoozed': + target.update(snoozedAt=CLOCK, snoozedUntil='2099-01-01T00:00:00.000Z') + if args.variant == 'settled': + target.update(settledOverride='settled', settledAt=CLOCK) + for n in range(2, 17): + settled = threads[n-1] if n <= 8 else thread('A', n) + settled.update(settledOverride='settled', settledAt=f'2026-10-21T21:{n:02d}:00.000Z') + if n > 8: + threads.append(settled) + if args.variant == 'archived': + target['archivedAt'] = CLOCK + if args.variant in ['lineage', 'orphan', 'all']: + for n, parent, relationship in [(9, target['id'], 'subagent'), (10, 'triage-A-9', 'subagent'), (11, target['id'], 'fork'), (12, target['id'], 'subagent')]: + child = thread('A', n) + child['lineage'] = dict(parentThreadId=parent, relationshipToParent=relationship, rootThreadId=target['id']) + threads.append(child) + if args.variant == 'orphan': + threads[16]['lineage']['parentThreadId'] = 'triage-missing-root' + threads[16]['lineage']['rootThreadId'] = 'triage-missing-root' + messages = [] + for group in ['A', 'B']: + text = "Synthetic navigation links: " + " | ".join(f"[Open {label}](t3-thread://v1/{target_id})" for label,target_id in [('A1','triage-A-1'),('A8','triage-A-8'),('child','triage-A-9'),('grandchild','triage-A-10'),('sibling','triage-A-12'),('fork','triage-A-11'),('missing','triage-missing')]) + messages.append(dict(id=f'triage-link-{group}',threadId=f'triage-{group}-8',runId=None,nodeId=None,role='assistant',text=text,attachments=[],streaming=False,createdAt=CLOCK,updatedAt=CLOCK,createdBy='agent',creationSource='web')) + message_values = ['(' + ','.join(map(quote,[m['id'],m['threadId'],None,None,m['role'],0,CLOCK,CLOCK,encoded(m)])) + ')' for m in messages] + (args.output/'messages.sql').write_text('INSERT OR REPLACE INTO orchestration_v2_projection_messages (message_id,thread_id,run_id,node_id,role,streaming,created_at,updated_at,payload_json) VALUES ' + ','.join(message_values) + ';\n') + item_values = [] + for m in messages: + item=dict(id=m['id']+'-item',threadId=m['threadId'],runId=None,nodeId=None,providerThreadId=None,providerTurnId=None,nativeItemRef=None,parentItemId=None,ordinal=0,status='completed',title=None,startedAt=CLOCK,completedAt=CLOCK,updatedAt=CLOCK,type='assistant_message',messageId=m['id'],text=m['text'],streaming=False) + item_values.append('(' + ','.join(map(quote,[item['id'],item['threadId'],None,None,None,None,None,0,'assistant_message','completed',CLOCK,encoded(item)])) + ')') + (args.output/'turn-items.sql').write_text('INSERT OR REPLACE INTO orchestration_v2_projection_turn_items (turn_item_id,thread_id,run_id,node_id,provider_thread_id,provider_turn_id,parent_item_id,ordinal,type,status,updated_at,payload_json) VALUES '+','.join(item_values)+';\n') + # One statement, so the state helper can apply it as one atomic transaction. + project_values = [] + for group in projects: + project_values.append('(' + ','.join(map(quote, [f'triage-project-{group}', f'Triage {group}', f'/tmp/triage-16534-{group}', '[]', '2026-10-21T20:00:00.000Z', '2026-10-21T20:00:00.000Z'])) + ')') + project_sql = 'INSERT OR REPLACE INTO projection_projects (project_id,title,workspace_root,scripts_json,created_at,updated_at) VALUES ' + ',\n'.join(project_values) + ';\n' + values = [] + for t in threads: + values.append('(' + ','.join(map(quote, [t['id'],t['projectId'],t['title'],'codex','full-access','default',None,t['createdAt'],t['updatedAt'],t['archivedAt'],None,encoded(t),'codex'])) + ')') + sql = 'INSERT OR REPLACE INTO orchestration_v2_projection_threads (thread_id,project_id,title,default_provider,runtime_mode,interaction_mode,active_provider_thread_id,created_at,updated_at,archived_at,deleted_at,payload_json,provider_instance_id) VALUES ' + ',\n'.join(values) + ';\n' + fixture = dict(clock=CLOCK, variant=args.variant, threads=threads, + settings=dict(legacySidebarEnabled=args.legacy,sidebarThreadSortOrder='updated_at',sidebarProjectSortOrder='manual',sidebarThreadPreviewCount=6,sidebarProjectGroupingMode='separate'), + uiState=dict(projectExpandedById={},projectOrder=[],threadLastVisitedAtById={},sidebarProjectScopeKey=None)) + (args.output/'reset-threads.sql').write_text("DELETE FROM orchestration_v2_projection_threads WHERE thread_id LIKE 'triage-%';\n") + (args.output/'hide-unrelated-projects.sql').write_text("UPDATE projection_projects SET deleted_at=" + quote(CLOCK) + " WHERE project_id NOT IN (" + ','.join(quote('triage-project-' + group) for group in projects) + ");\n") + (args.output/'archive-unrelated-threads.sql').write_text("UPDATE orchestration_v2_projection_threads SET archived_at=" + quote(CLOCK) + ",payload_json=json_set(payload_json,'$.archivedAt'," + quote(CLOCK) + ") WHERE thread_id NOT LIKE 'triage-%';\n") + (args.output/'hide-v1-threads.sql').write_text("UPDATE projection_threads SET deleted_at="+quote(CLOCK)+" WHERE thread_id NOT LIKE 'triage-%';\n") + (args.output/'projects.sql').write_text(project_sql) + (args.output/'threads.sql').write_text(sql) + (args.output/'fixture.json').write_text(encoded(fixture)+'\n') + digest = hashlib.sha256(encoded(fixture).encode()).hexdigest() + (args.output/'sha256.txt').write_text(digest+'\n') + from transcript import write_transcript + write_transcript(args.output) + print(digest) + + +if __name__ == '__main__': + main() diff --git a/scripts/fixtures/issue-16534/transcript.py b/scripts/fixtures/issue-16534/transcript.py new file mode 100644 index 0000000000000..135d5caf37a75 --- /dev/null +++ b/scripts/fixtures/issue-16534/transcript.py @@ -0,0 +1,25 @@ +"""Emit stopped synthetic runs and transcript items for real thread-link rendering.""" +import json +from pathlib import Path + +def write_transcript(output): + clock='2026-10-21T23:00:00.000Z' + def q(v): return 'NULL' if v is None else "'"+str(v).replace("'","''")+"'" + def enc(v):return json.dumps(v,separators=(',',':')) + runs=[];messages=[];items=[] + for group in ['A','B']: + tid=f'triage-{group}-8';rid=f'triage-fixture-run-{group}';uid=rid+'-user';aid=rid+'-assistant' + run=dict(id=rid,threadId=tid,ordinal=1,providerInstanceId='codex',modelSelection=dict(instanceId='codex',model='gpt-6.1-sol'),providerThreadId=None,userMessageId=uid,rootNodeId=None,activeAttemptId=None,status='completed',requestedAt=clock,startedAt=clock,completedAt=clock,checkpointId=None,contextHandoffId=None) + runs.append('('+','.join(map(q,[rid,tid,1,'codex',None,'completed',clock,clock,enc(run),'codex']))+')') + linktext='Synthetic fixture navigation: '+' | '.join(f'[{label}](t3-thread://v1/{target})' for label,target in [('Open A1','triage-A-1'),('Open A8','triage-A-8'),('Open child','triage-A-9'),('Open grandchild','triage-A-10'),('Open sibling','triage-A-12'),('Open fork','triage-A-11'),('Open missing','triage-missing')]) + for ordinal,(mid,role,text) in enumerate([(uid,'user','Use these synthetic navigation links.'),(aid,'assistant',linktext)]): + m=dict(id=mid,threadId=tid,runId=rid,nodeId=None,role=role,text=text,attachments=[],streaming=False,createdAt=clock,updatedAt=clock,createdBy='user' if role=='user' else 'agent',creationSource='web') + messages.append('('+','.join(map(q,[mid,tid,rid,None,role,0,clock,clock,enc(m)]))+')') + item=dict(id=mid+'-item',threadId=tid,runId=rid,nodeId=None,providerThreadId=None,providerTurnId=None,nativeItemRef=None,parentItemId=None,ordinal=ordinal,status='completed',title=None,startedAt=clock,completedAt=clock,updatedAt=clock,type=role+'_message',messageId=mid,text=text,streaming=False) + if role=='user':item.update(createdBy='user',creationSource='web',inputIntent='turn_start',attachments=[]) + items.append('('+','.join(map(q,[item['id'],tid,rid,None,None,None,None,ordinal,item['type'],'completed',clock,enc(item)]))+')') + tables=[('runs','run_id,thread_id,ordinal,provider,provider_thread_id,status,requested_at,completed_at,payload_json,provider_instance_id',runs),('messages','message_id,thread_id,run_id,node_id,role,streaming,created_at,updated_at,payload_json',messages),('turn_items','turn_item_id,thread_id,run_id,node_id,provider_thread_id,provider_turn_id,parent_item_id,ordinal,type,status,updated_at,payload_json',items)] + for table,columns,values in tables:(output/('transcript-'+table.replace('_','-')+'.sql')).write_text(f'INSERT OR REPLACE INTO orchestration_v2_projection_{table} ({columns}) VALUES '+','.join(values)+';\n') +if __name__=='__main__': + import sys + write_transcript(Path(sys.argv[1]))