History

fix(orchestration): stabilize Codex turn mapping and settlement

- Preserve Codex turn identity while suppressing duplicate diff notifications - Optimize settlement projections and isolate thread visit handling - Add concurrency and regression coverage across server and mobile

Julius Marminge committed 2 months ago

Commit 6502e221 parent 25d2531167 files · +189949 −140Browse files

Showing the changes to apps/web/src (6 of 167 files). Show all

Unified · Split

6 files
  1. apps/web/src/components/CommandPalette.tsx +9 −1
  2. apps/web/src/components/ThreadStatusIndicators.subscriptions.test.tsx +180 −0
  3. apps/web/src/components/ThreadStatusIndicators.test.ts +19 −0
  4. apps/web/src/components/ThreadStatusIndicators.tsx +59 −17
  5. apps/web/src/connection/desktopLocal.test.ts +62 −1
  6. apps/web/src/connection/desktopLocal.ts +19 −2
apps/web/src/components/CommandPalette.tsx+9−1
@@ -1186,7 +1186,14 @@ function OpenCommandPaletteDialog(props: { · 1185 unmodified lines
11861186 projectTitleById,
11871187 sortOrder: clientSettings.sidebarThreadSortOrder,
11881188 icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
1189− renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
1189+ renderLeadingContent: (thread) => (
1190+ <ThreadRowLeadingStatus
1191+ thread={thread}
1192+ snapshot={changeRequestSnapshotByKey.get(
1193+ scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
1194+ )}
1195+ />
1196+ ),
11901197 renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />,
11911198 renderDescription: (thread, { projectTitle }) => {
11921199 const modelInstanceId =
@@ -1238,6 +1245,7 @@ function OpenCommandPaletteDialog(props: { · 45 unmodified lines
12381245 }),
12391246 [
12401247 activeThreadId,
1248+ changeRequestSnapshotByKey,
12411249 clientSettings.sidebarThreadSortOrder,
12421250 navigate,
12431251 projectCwdById,
apps/web/src/components/ThreadStatusIndicators.subscriptions.test.tsx+180−0
@@ -0,0 +1,180 @@
1+import { ProjectId, ThreadId, type VcsStatusResult } from "@t3tools/contracts";
2+import { act, type ComponentProps, type ReactNode } from "react";
3+import { create, type ReactTestRenderer } from "react-test-renderer";
4+import { afterEach, expect, it, vi } from "vite-plus/test";
5+
6+import { AppAtomRegistryProvider } from "../rpc/atomRegistry";
7+import { makeThreadFixture } from "../test-fixtures";
8+
9+const state = vi.hoisted(() => ({ queries: [] as string[] }));
10+
11+vi.mock("../state/entities", () => ({ useProject: () => null }));
12+vi.mock("../uiStateStore", () => ({ useUiStateStore: () => undefined }));
13+vi.mock("../state/vcs", async () => {
14+ const { AsyncResult, Atom } = await import("effect/unstable/reactivity");
15+ const statuses = Atom.family((cwd: string) =>
16+ Atom.make(() => {
17+ state.queries.push(`vcs:${cwd}`);
18+ return AsyncResult.success({
19+ isRepo: true,
20+ hasPrimaryRemote: true,
21+ isDefaultRef: false,
22+ refName: "feature/test",
23+ hasWorkingTreeChanges: false,
24+ workingTree: { files: [], insertions: 0, deletions: 0 },
25+ hasUpstream: true,
26+ aheadCount: 0,
27+ behindCount: 0,
28+ pr: {
29+ number: 42,
30+ title: "Observed worktree PR",
31+ url: "https://github.com/example/repo/pull/42",
32+ baseRef: "main",
33+ headRef: "feature/test",
34+ state: "open",
35+ },
36+ } satisfies VcsStatusResult);
37+ }),
38+ );
39+ return {
40+ vcsEnvironment: { status: ({ input }: { input: { cwd: string } }) => statuses(input.cwd) },
41+ };
42+});
43+vi.mock("../state/pullRequests", async (importOriginal) => {
44+ const original = await importOriginal<typeof import("../state/pullRequests")>();
45+ const { AsyncResult, Atom } = await import("effect/unstable/reactivity");
46+ const { ProjectId } = await import("@t3tools/contracts");
47+ const summaries = Atom.family((number: number) =>
48+ Atom.make(() => {
49+ state.queries.push(`pr:${number}`);
50+ return AsyncResult.success({
51+ provider: "github" as const,
52+ projectId: ProjectId.make("project-test"),
53+ repository: "example/repo",
54+ number,
55+ title: "Observed linked PR",
56+ url: `https://github.com/example/repo/pull/${number}`,
57+ state: "open" as const,
58+ headBranch: "feature/test",
59+ baseBranch: "main",
60+ updatedAt: "2026-09-04T00:00:00.000Z",
61+ });
62+ }),
63+ );
64+ return {
65+ ...original,
66+ linkedPullRequestDetailAtom: ({ input }: { input: { number: number } }) =>
67+ summaries(input.number),
68+ };
69+});
70+vi.mock("./ui/tooltip", async () => {
71+ const { cloneElement, isValidElement } = await import("react");
72+ return {
73+ Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
74+ TooltipTrigger({
75+ render,
76+ children,
77+ }: ComponentProps<typeof import("./ui/tooltip").TooltipTrigger>) {
78+ return isValidElement(render) ? cloneElement(render, undefined, children) : children;
79+ },
80+ TooltipPopup: () => null,
81+ };
82+});
83+
84+import { ThreadRowLeadingStatus } from "./ThreadStatusIndicators";
85+
86+afterEach(() => {
87+ vi.unstubAllGlobals();
88+});
89+
90+it("leases only visible palette results while retaining observed and cached PR badges", async () => {
91+ const observers: Array<{ setVisible: (visible: boolean) => void }> = [];
92+ const scrollRoot = {};
93+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
94+ vi.stubGlobal(
95+ "IntersectionObserver",
96+ class {
97+ constructor(callback: (entries: ReadonlyArray<{ isIntersecting: boolean }>) => void) {
98+ observers.push({ setVisible: (visible) => callback([{ isIntersecting: visible }]) });
99+ }
100+ observe() {}
101+ disconnect() {}
102+ },
103+ );
104+ const threads = Array.from({ length: 128 }, (_, index) =>
105+ makeThreadFixture({
106+ id: ThreadId.make(`palette-thread-${index}`),
107+ branch: "feature/test",
108+ worktreePath: `/test/worktree-${index}`,
109+ linkedPullRequest:
110+ index % 2 === 0
111+ ? null
112+ : {
113+ projectId: ProjectId.make("project-test"),
114+ repository: "example/repo",
115+ number: index,
116+ url: `https://github.com/example/repo/pull/${index}`,
117+ },
118+ }),
119+ );
120+ let renderer: ReactTestRenderer | undefined;
121+ state.queries.length = 0;
122+ try {
123+ await act(async () => {
124+ renderer = create(
125+ <AppAtomRegistryProvider>
126+ {threads.map((thread, index) => (
127+ <span key={thread.id}>
128+ <ThreadRowLeadingStatus
129+ thread={thread}
130+ snapshot={
131+ index === 126
132+ ? {
133+ branch: "feature/test",
134+ sourceControlProvider: undefined,
135+ pr: {
136+ number: 126,
137+ title: "Cached badge",
138+ url: "https://github.com/example/repo/pull/126",
139+ baseRef: "main",
140+ headRef: "feature/test",
141+ state: "open",
142+ },
143+ }
144+ : undefined
145+ }
146+ />
147+ {thread.title}
148+ </span>
149+ ))}
150+ </AppAtomRegistryProvider>,
151+ { createNodeMock: () => ({ parentElement: { closest: () => scrollRoot } }) },
152+ );
153+ });
154+ const mounted = renderer!;
155+ const badgeCount = () =>
156+ mounted.root.findAll(
157+ (node) => node.type === "span" && String(node.props["aria-label"] ?? "").startsWith("PR #"),
158+ ).length;
159+ expect(observers).toHaveLength(128);
160+ expect(state.queries).toEqual([]);
161+ expect(badgeCount()).toBe(1);
162+
163+ await act(async () => {
164+ for (const observer of observers.slice(0, 8)) observer.setVisible(true);
165+ });
166+ expect(state.queries).toHaveLength(8);
167+ expect(state.queries.filter((key) => key.startsWith("vcs:"))).toHaveLength(4);
168+ expect(state.queries.filter((key) => key.startsWith("pr:"))).toHaveLength(4);
169+ expect(badgeCount()).toBe(9);
170+
171+ await act(async () => {
172+ for (const observer of observers.slice(0, 8)) observer.setVisible(false);
173+ for (const observer of observers.slice(8, 16)) observer.setVisible(true);
174+ });
175+ expect(state.queries).toHaveLength(16);
176+ expect(badgeCount()).toBe(17);
177+ } finally {
178+ await act(async () => renderer?.unmount());
179+ }
180+});
apps/web/src/components/ThreadStatusIndicators.test.ts+19−0
@@ -554,6 +554,25 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { · 553 unmodified lines
554554 ).toEqual(mergedPr);
555555 });
556556
557+ it("shows the last matching PR snapshot while its live status lease is released", () => {
558+ const openPr = { ...mergedPr, state: "open" as const };
559+ const input = {
560+ threadBranch: featureBranch,
561+ gitStatus: null,
562+ snapshot: snapshotFor(featureBranch, openPr, provider),
563+ retainTerminalOnBranchMismatch: false,
564+ };
565+ expect(resolveDisplayedThreadPr(input)).toEqual(openPr);
566+ expect(resolveDisplayedThreadPrProvider(input)).toEqual(provider);
567+ expect(resolveDisplayedThreadPr({ ...input, threadBranch: "feature/other" })).toBeNull();
568+ expect(
569+ resolveDisplayedThreadPr({
570+ ...input,
571+ snapshot: { ...input.snapshot, linkedPullRequest },
572+ }),
573+ ).toBeNull();
574+ });
575+
557576 it("retains a merged PR after a main checkout", () => {
558577 const matchingStatus = status({
559578 refName: featureBranch,
apps/web/src/components/ThreadStatusIndicators.tsx+59−17
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
1212 } from "@t3tools/contracts";
1313 import { Atom } from "effect/unstable/reactivity";
1414 import { FolderGit2Icon, TerminalIcon } from "lucide-react";
15−import { useMemo } from "react";
15+import { useCallback, useMemo } from "react";
1616 import { appAtomRegistry } from "../rpc/atomRegistry";
1717 import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
1818 import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
@@ -27,6 +27,8 @@ import { · 8 unmodified lines
2727 resolveThreadLastVisitedAt,
2828 resolveThreadStatusPill,
2929 type ThreadStatusPill,
30+ useRetainedValue,
31+ useSidebarRowSubscriptionLease,
3032 } from "./Sidebar.logic";
3133 import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation";
3234 import type { SidebarThreadSummary } from "../types";
@@ -58,9 +60,10 @@ export interface LinkedThreadPullRequestStatus { · 25 unmodified lines
5860 export function useLinkedThreadPullRequest(
5961 environmentId: EnvironmentId | null,
6062 linkedPullRequest: ThreadLinkedPullRequest | null | undefined,
63+ enabled = true,
6164 ): LinkedThreadPullRequestStatus | null {
6265 const queried = useEnvironmentQuery(
63− environmentId === null || linkedPullRequest == null
66+ !enabled || environmentId === null || linkedPullRequest == null
6467 ? null
6568 : linkedPullRequestDetailAtom({
6669 environmentId,
@@ -353,7 +356,7 @@ export function nextThreadChangeRequestSnapshot(input: { · 286 unmodified lines
353356 * checkouts only, a cached merged/closed PR for the thread. Local thread
354357 * metadata follows the shared checkout, so the cached branch intentionally
355358 * survives that metadata changing to the newly checked-out branch. Open PRs
356− * are never retained — their state can still change.
359+ * are retained only while live status is absent and the branch still matches.
357360 */
358361 export function resolveDisplayedThreadPr(input: {
359362 threadBranch: string | null;
@@ -388,6 +391,15 @@ export function resolveDisplayedThreadPr(input: { · 28 unmodified lines
388391 return gitStatus.pr;
389392 }
390393
394+ if (
395+ gitStatus === null &&
396+ threadBranch !== null &&
397+ snapshot?.branch === threadBranch &&
398+ snapshot.linkedPullRequest === undefined
399+ ) {
400+ return snapshot.pr;
401+ }
402+
391403 if (
392404 threadBranch !== null &&
393405 retainTerminalOnBranchMismatch &&
@@ -434,6 +446,15 @@ export function resolveDisplayedThreadPrProvider(input: { · 40 unmodified lines
434446 return gitStatus.sourceControlProvider;
435447 }
436448
449+ if (
450+ gitStatus === null &&
451+ threadBranch !== null &&
452+ snapshot?.branch === threadBranch &&
453+ snapshot.linkedPullRequest === undefined
454+ ) {
455+ return snapshot.sourceControlProvider;
456+ }
457+
437458 if (
438459 threadBranch !== null &&
439460 retainTerminalOnBranchMismatch &&
@@ -550,7 +571,19 @@ export function ThreadStatusLabel({ · 110 unmodified lines
550571 * like the command palette. Shows the change request state icon (if present) and the
551572 * thread status dot, matching the sidebar's leading indicators.
552573 */
553−export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummary }) {
574+export function ThreadRowLeadingStatus({
575+ thread,
576+ snapshot,
577+}: {
578+ thread: SidebarThreadSummary;
579+ snapshot?: ThreadChangeRequestSnapshot | undefined;
580+}) {
581+ const { leaseLiveStatus, rowRef } = useSidebarRowSubscriptionLease(false);
582+ // Observe the containing title even when this thread has no badge yet.
583+ const statusRef = useCallback(
584+ (node: HTMLSpanElement | null) => rowRef(node?.parentElement ?? null),
585+ [rowRef],
586+ );
554587 const threadRef = scopeThreadRef(thread.environmentId, thread.id);
555588 const localLastVisitedAt = useUiStateStore(
556589 (state) => state.threadLastVisitedAtById[scopedThreadKey(threadRef)],
@@ -567,9 +600,11 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 10 unmodified lines
567600 const linkedPullRequest = useLinkedThreadPullRequest(
568601 thread.environmentId,
569602 thread.linkedPullRequest,
603+ leaseLiveStatus,
570604 );
571605 const gitStatus = useEnvironmentQuery(
572− thread.linkedPullRequest == null &&
606+ leaseLiveStatus &&
607+ thread.linkedPullRequest == null &&
573608 (thread.branch != null || thread.worktreePath !== null) &&
574609 gitCwd !== null
575610 ? vcsEnvironment.status({
@@ -578,14 +613,20 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 2 unmodified lines
578613 })
579614 : null,
580615 );
581− const pr =
582− thread.linkedPullRequest == null
583− ? resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data })
584− : (linkedPullRequest?.pr ?? null);
585− const prStatus = prStatusIndicator(
586− pr,
587− linkedPullRequest?.sourceControlProvider ?? gitStatus.data?.sourceControlProvider,
616+ const visibleGitStatus = useRetainedValue(
617+ JSON.stringify([thread.environmentId, gitCwd]),
618+ gitStatus.data,
588619 );
620+ const displayedPrInput = {
621+ threadBranch: thread.branch,
622+ gitStatus: visibleGitStatus,
623+ snapshot,
624+ retainTerminalOnBranchMismatch: thread.worktreePath === null,
625+ linkedPullRequest: thread.linkedPullRequest,
626+ linkedPullRequestStatus: linkedPullRequest,
627+ };
628+ const pr = resolveDisplayedThreadPr(displayedPrInput);
629+ const prStatus = prStatusIndicator(pr, resolveDisplayedThreadPrProvider(displayedPrInput));
589630 const threadStatus = resolveThreadStatusPill({
590631 thread: {
591632 ...thread,
@@ -593,12 +634,13 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 1 unmodified line
593634 },
594635 });
595636
596− if (!prStatus && !threadStatus) {
597− return null;
598− }
599−
600637 return (
601− <span className="inline-flex shrink-0 items-center gap-1.5">
638+ <span
639+ ref={statusRef}
640+ className={
641+ prStatus || threadStatus ? "inline-flex shrink-0 items-center gap-1.5" : "contents"
642+ }
643+ >
602644 {prStatus && pr ? (
603645 <Tooltip>
604646 <TooltipTrigger
apps/web/src/connection/desktopLocal.test.ts+62−1
@@ -2,7 +2,11 @@ import { · 1 unmodified line
22 BearerConnectionTarget,
33 PrimaryConnectionTarget,
44 } from "@t3tools/client-runtime/connection";
5−import { EnvironmentId, PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
5+import {
6+ type DesktopEnvironmentBootstrap,
7+ EnvironmentId,
8+ PRIMARY_LOCAL_ENVIRONMENT_ID,
9+} from "@t3tools/contracts";
610 import { describe, expect, it } from "vite-plus/test";
711
812 import {
@@ -38,6 +42,63 @@ describe("desktop local connection identity", () => { · 29 unmodified lines
3842 });
3943
4044 describe("desktop local topology reads", () => {
45+ it("reuses snapshots when polling returns fresh objects for the same topology", () => {
46+ const secondary: DesktopEnvironmentBootstrap = {
47+ id: "wsl:default",
48+ label: "WSL",
49+ runningDistro: "Ubuntu",
50+ httpBaseUrl: "http://127.0.0.1:4000",
51+ wsBaseUrl: "ws://127.0.0.1:4000",
52+ bootstrapToken: "bootstrap-1",
53+ };
54+ let entries = [secondary];
55+ const reader = createDesktopSecondaryBootstrapsReader(() => ({
56+ getLocalEnvironmentBootstraps: () => entries.map((entry) => ({ ...entry })),
57+ }));
58+
59+ const connected = reader.readSnapshot();
60+ expect(reader.readSnapshot()).toBe(connected);
61+ expect(reader.readResult()).toEqual({ _tag: "Success", bootstraps: connected });
62+ expect(reader.readSnapshot()).toBe(connected);
63+
64+ entries = [];
65+ const empty = reader.readSnapshot();
66+ expect(empty).toEqual([]);
67+ expect(reader.readSnapshot()).toBe(empty);
68+ });
69+
70+ it.each<Partial<DesktopEnvironmentBootstrap>>([
71+ { id: "wsl:Debian" },
72+ { label: "Renamed backend" },
73+ { runningDistro: "Debian" },
74+ { httpBaseUrl: "http://127.0.0.1:4001" },
75+ { wsBaseUrl: "ws://127.0.0.1:4001" },
76+ { bootstrapToken: "bootstrap-2" },
77+ ])("publishes bootstrap changes: %j", (change) => {
78+ let secondary: DesktopEnvironmentBootstrap = {
79+ id: "wsl:default",
80+ label: "WSL",
81+ runningDistro: "Ubuntu",
82+ httpBaseUrl: "http://127.0.0.1:4000",
83+ wsBaseUrl: "ws://127.0.0.1:4000",
84+ bootstrapToken: "bootstrap-1",
85+ };
86+ const reader = createDesktopSecondaryBootstrapsReader(() => ({
87+ getLocalEnvironmentBootstraps: () => [{ ...secondary }],
88+ }));
89+ const before = reader.readSnapshot();
90+ secondary = { ...secondary, ...change };
91+ const after = reader.readSnapshot();
92+ expect(after).not.toBe(before);
93+ expect(after).toEqual([secondary]);
94+ expect(reader.readSnapshot()).toBe(after);
95+ });
96+
97+ it("keeps the empty snapshot stable without a desktop bridge", () => {
98+ const reader = createDesktopSecondaryBootstrapsReader(() => undefined);
99+ expect(reader.readSnapshot()).toBe(reader.readSnapshot());
100+ });
101+
41102 it("distinguishes a successful empty topology from a read failure", () => {
42103 let readBootstraps = () => [];
43104 const reader = createDesktopSecondaryBootstrapsReader(() => ({
apps/web/src/connection/desktopLocal.ts+19−2
@@ -67,13 +67,30 @@ export function createDesktopSecondaryBootstrapsReader( · 66 unmodified lines
6767 const readResult = (): DesktopSecondaryBootstrapsRead => {
6868 const bridge = resolveBridge();
6969 if (bridge === undefined) {
70− snapshot = [];
70+ if (snapshot.length > 0) snapshot = [];
7171 return { _tag: "Success", bootstraps: snapshot };
7272 }
7373 try {
74− snapshot = bridge
74+ const next = bridge
7575 .getLocalEnvironmentBootstraps()
7676 .filter((entry) => entry.id !== PRIMARY_LOCAL_ENVIRONMENT_ID);
77+ if (
78+ next.length !== snapshot.length ||
79+ next.some((entry, index) => {
80+ const previous = snapshot[index];
81+ return (
82+ previous === undefined ||
83+ entry.id !== previous.id ||
84+ entry.label !== previous.label ||
85+ entry.runningDistro !== previous.runningDistro ||
86+ entry.httpBaseUrl !== previous.httpBaseUrl ||
87+ entry.wsBaseUrl !== previous.wsBaseUrl ||
88+ entry.bootstrapToken !== previous.bootstrapToken
89+ );
90+ })
91+ ) {
92+ snapshot = next;
93+ }
7794 return { _tag: "Success", bootstraps: snapshot };
7895 } catch (cause) {
7996 return { _tag: "Failure", cause };

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t