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
1186 projectTitleById,1186 projectTitleById,
1187 sortOrder: clientSettings.sidebarThreadSortOrder,1187 sortOrder: clientSettings.sidebarThreadSortOrder,
1188 icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,1188 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 ),
1190 renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />,1197 renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />,
1191 renderDescription: (thread, { projectTitle }) => {1198 renderDescription: (thread, { projectTitle }) => {
1192 const modelInstanceId =1199 const modelInstanceId =
@@ -1238,6 +1245,7 @@ function OpenCommandPaletteDialog(props: { · 45 unmodified lines
1238 }),1245 }),
1239 [1246 [
1240 activeThreadId,1247 activeThreadId,
1248 changeRequestSnapshotByKey,
1241 clientSettings.sidebarThreadSortOrder,1249 clientSettings.sidebarThreadSortOrder,
1242 navigate,1250 navigate,
1243 projectCwdById,1251 projectCwdById,
apps/web/src/components/ThreadStatusIndicators.subscriptions.test.tsx+180−0
@@ -0,0 +1,180 @@
1import { ProjectId, ThreadId, type VcsStatusResult } from "@t3tools/contracts";
2import { act, type ComponentProps, type ReactNode } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, expect, it, vi } from "vite-plus/test";
5
6import { AppAtomRegistryProvider } from "../rpc/atomRegistry";
7import { makeThreadFixture } from "../test-fixtures";
8
9const state = vi.hoisted(() => ({ queries: [] as string[] }));
10
11vi.mock("../state/entities", () => ({ useProject: () => null }));
12vi.mock("../uiStateStore", () => ({ useUiStateStore: () => undefined }));
13vi.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});
43vi.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});
70vi.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
84import { ThreadRowLeadingStatus } from "./ThreadStatusIndicators";
85
86afterEach(() => {
87 vi.unstubAllGlobals();
88});
89
90it("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
554 ).toEqual(mergedPr);554 ).toEqual(mergedPr);
555 });555 });
556 556
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
557 it("retains a merged PR after a main checkout", () => {576 it("retains a merged PR after a main checkout", () => {
558 const matchingStatus = status({577 const matchingStatus = status({
559 refName: featureBranch,578 refName: featureBranch,
apps/web/src/components/ThreadStatusIndicators.tsx+59−17
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
12} from "@t3tools/contracts";12} from "@t3tools/contracts";
13import { Atom } from "effect/unstable/reactivity";13import { Atom } from "effect/unstable/reactivity";
14import { FolderGit2Icon, TerminalIcon } from "lucide-react";14import { FolderGit2Icon, TerminalIcon } from "lucide-react";
15import { useMemo } from "react";15import { useCallback, useMemo } from "react";
16import { appAtomRegistry } from "../rpc/atomRegistry";16import { appAtomRegistry } from "../rpc/atomRegistry";
17import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";17import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
18import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";18import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
@@ -27,6 +27,8 @@ import { · 8 unmodified lines
27 resolveThreadLastVisitedAt,27 resolveThreadLastVisitedAt,
28 resolveThreadStatusPill,28 resolveThreadStatusPill,
29 type ThreadStatusPill,29 type ThreadStatusPill,
30 useRetainedValue,
31 useSidebarRowSubscriptionLease,
30} from "./Sidebar.logic";32} from "./Sidebar.logic";
31import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation";33import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation";
32import type { SidebarThreadSummary } from "../types";34import type { SidebarThreadSummary } from "../types";
@@ -58,9 +60,10 @@ export interface LinkedThreadPullRequestStatus { · 25 unmodified lines
58export function useLinkedThreadPullRequest(60export function useLinkedThreadPullRequest(
59 environmentId: EnvironmentId | null,61 environmentId: EnvironmentId | null,
60 linkedPullRequest: ThreadLinkedPullRequest | null | undefined,62 linkedPullRequest: ThreadLinkedPullRequest | null | undefined,
63 enabled = true,
61): LinkedThreadPullRequestStatus | null {64): LinkedThreadPullRequestStatus | null {
62 const queried = useEnvironmentQuery(65 const queried = useEnvironmentQuery(
63 environmentId === null || linkedPullRequest == null66 !enabled || environmentId === null || linkedPullRequest == null
64 ? null67 ? null
65 : linkedPullRequestDetailAtom({68 : linkedPullRequestDetailAtom({
66 environmentId,69 environmentId,
@@ -353,7 +356,7 @@ export function nextThreadChangeRequestSnapshot(input: { · 286 unmodified lines
353 * checkouts only, a cached merged/closed PR for the thread. Local thread356 * checkouts only, a cached merged/closed PR for the thread. Local thread
354 * metadata follows the shared checkout, so the cached branch intentionally357 * metadata follows the shared checkout, so the cached branch intentionally
355 * survives that metadata changing to the newly checked-out branch. Open PRs358 * 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.
357 */360 */
358export function resolveDisplayedThreadPr(input: {361export function resolveDisplayedThreadPr(input: {
359 threadBranch: string | null;362 threadBranch: string | null;
@@ -388,6 +391,15 @@ export function resolveDisplayedThreadPr(input: { · 28 unmodified lines
388 return gitStatus.pr;391 return gitStatus.pr;
389 }392 }
390 393
394 if (
395 gitStatus === null &&
396 threadBranch !== null &&
397 snapshot?.branch === threadBranch &&
398 snapshot.linkedPullRequest === undefined
399 ) {
400 return snapshot.pr;
401 }
402
391 if (403 if (
392 threadBranch !== null &&404 threadBranch !== null &&
393 retainTerminalOnBranchMismatch &&405 retainTerminalOnBranchMismatch &&
@@ -434,6 +446,15 @@ export function resolveDisplayedThreadPrProvider(input: { · 40 unmodified lines
434 return gitStatus.sourceControlProvider;446 return gitStatus.sourceControlProvider;
435 }447 }
436 448
449 if (
450 gitStatus === null &&
451 threadBranch !== null &&
452 snapshot?.branch === threadBranch &&
453 snapshot.linkedPullRequest === undefined
454 ) {
455 return snapshot.sourceControlProvider;
456 }
457
437 if (458 if (
438 threadBranch !== null &&459 threadBranch !== null &&
439 retainTerminalOnBranchMismatch &&460 retainTerminalOnBranchMismatch &&
@@ -550,7 +571,19 @@ export function ThreadStatusLabel({ · 110 unmodified lines
550 * like the command palette. Shows the change request state icon (if present) and the571 * like the command palette. Shows the change request state icon (if present) and the
551 * thread status dot, matching the sidebar's leading indicators.572 * thread status dot, matching the sidebar's leading indicators.
552 */573 */
553export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummary }) {574export 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 );
554 const threadRef = scopeThreadRef(thread.environmentId, thread.id);587 const threadRef = scopeThreadRef(thread.environmentId, thread.id);
555 const localLastVisitedAt = useUiStateStore(588 const localLastVisitedAt = useUiStateStore(
556 (state) => state.threadLastVisitedAtById[scopedThreadKey(threadRef)],589 (state) => state.threadLastVisitedAtById[scopedThreadKey(threadRef)],
@@ -567,9 +600,11 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 10 unmodified lines
567 const linkedPullRequest = useLinkedThreadPullRequest(600 const linkedPullRequest = useLinkedThreadPullRequest(
568 thread.environmentId,601 thread.environmentId,
569 thread.linkedPullRequest,602 thread.linkedPullRequest,
603 leaseLiveStatus,
570 );604 );
571 const gitStatus = useEnvironmentQuery(605 const gitStatus = useEnvironmentQuery(
572 thread.linkedPullRequest == null &&606 leaseLiveStatus &&
607 thread.linkedPullRequest == null &&
573 (thread.branch != null || thread.worktreePath !== null) &&608 (thread.branch != null || thread.worktreePath !== null) &&
574 gitCwd !== null609 gitCwd !== null
575 ? vcsEnvironment.status({610 ? vcsEnvironment.status({
@@ -578,14 +613,20 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 2 unmodified lines
578 })613 })
579 : null,614 : null,
580 );615 );
581 const pr =616 const visibleGitStatus = useRetainedValue(
582 thread.linkedPullRequest == null617 JSON.stringify([thread.environmentId, gitCwd]),
583 ? resolveThreadPr({ threadBranch: thread.branch, gitStatus: gitStatus.data })618 gitStatus.data,
584 : (linkedPullRequest?.pr ?? null);
585 const prStatus = prStatusIndicator(
586 pr,
587 linkedPullRequest?.sourceControlProvider ?? gitStatus.data?.sourceControlProvider,
588 );619 );
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));
589 const threadStatus = resolveThreadStatusPill({630 const threadStatus = resolveThreadStatusPill({
590 thread: {631 thread: {
591 ...thread,632 ...thread,
@@ -593,12 +634,13 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar · 1 unmodified line
593 },634 },
594 });635 });
595 636
596 if (!prStatus && !threadStatus) {
597 return null;
598 }
599
600 return (637 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 >
602 {prStatus && pr ? (644 {prStatus && pr ? (
603 <Tooltip>645 <Tooltip>
604 <TooltipTrigger646 <TooltipTrigger
apps/web/src/connection/desktopLocal.test.ts+62−1
@@ -2,7 +2,11 @@ import { · 1 unmodified line
2 BearerConnectionTarget,2 BearerConnectionTarget,
3 PrimaryConnectionTarget,3 PrimaryConnectionTarget,
4} from "@t3tools/client-runtime/connection";4} from "@t3tools/client-runtime/connection";
5import { EnvironmentId, PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";5import {
6 type DesktopEnvironmentBootstrap,
7 EnvironmentId,
8 PRIMARY_LOCAL_ENVIRONMENT_ID,
9} from "@t3tools/contracts";
6import { describe, expect, it } from "vite-plus/test";10import { describe, expect, it } from "vite-plus/test";
7 11
8import {12import {
@@ -38,6 +42,63 @@ describe("desktop local connection identity", () => { · 29 unmodified lines
38});42});
39 43
40describe("desktop local topology reads", () => {44describe("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
41 it("distinguishes a successful empty topology from a read failure", () => {102 it("distinguishes a successful empty topology from a read failure", () => {
42 let readBootstraps = () => [];103 let readBootstraps = () => [];
43 const reader = createDesktopSecondaryBootstrapsReader(() => ({104 const reader = createDesktopSecondaryBootstrapsReader(() => ({
apps/web/src/connection/desktopLocal.ts+19−2
@@ -67,13 +67,30 @@ export function createDesktopSecondaryBootstrapsReader( · 66 unmodified lines
67 const readResult = (): DesktopSecondaryBootstrapsRead => {67 const readResult = (): DesktopSecondaryBootstrapsRead => {
68 const bridge = resolveBridge();68 const bridge = resolveBridge();
69 if (bridge === undefined) {69 if (bridge === undefined) {
70 snapshot = [];70 if (snapshot.length > 0) snapshot = [];
71 return { _tag: "Success", bootstraps: snapshot };71 return { _tag: "Success", bootstraps: snapshot };
72 }72 }
73 try {73 try {
74 snapshot = bridge74 const next = bridge
75 .getLocalEnvironmentBootstraps()75 .getLocalEnvironmentBootstraps()
76 .filter((entry) => entry.id !== PRIMARY_LOCAL_ENVIRONMENT_ID);76 .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 }
77 return { _tag: "Success", bootstraps: snapshot };94 return { _tag: "Success", bootstraps: snapshot };
78 } catch (cause) {95 } catch (cause) {
79 return { _tag: "Failure", cause };96 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