Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ These are direct edits to upstream-owned files that exist only because upstream
- **Project identity honors `gh repo set-default`** (register D31; Jackson's 2026-10-11 decision). `apps/server/src/project/RepositoryIdentityResolver.ts` reads `remote.<name>.gh-resolved` beside `git remote -v` and prefers the marked remote (`base`, or `OWNER/REPO` on that remote's host) before upstream's `upstream`, then `origin` order. Without it a fork developed as its own project is identified as its parent, so the `#` picker and the pull request view read the parent's pull requests. The hunks and the two tests in `RepositoryIdentityResolver.test.ts` are a port of pingdotgg/t3code#14291, which upstream closed unmerged; its replacement, pingdotgg/t3code#21574, is open and fixes the same issue (pingdotgg/t3code#15023). The hunks were re-applied on upstream's new cache key at the 2026-10-24 advance. Watch signal: upstream merges #21574, or settles fork remotes another way. Exit: take upstream's file and follow its rule, even if it differs.
- **Storage cleanup reads Git 2.56's broken-gitfile message.** `apps/server/src/storageCleanup.ts` treats "does not point to a valid repository" like "not a git repository" when it checks whether a worktree's repository or branch changed. Git 2.56 reports a worktree whose `.git` file points at a missing directory with the new message, so cleanup reported that worktree as failed instead of kept, and `storageCleanup.test.ts` failed on CI runners with Git 2.56 (#768). Upstream's file has the same check. Watch signal: upstream matches the Git 2.56 message. Exit: remove this hunk and take upstream's.
- **Saved-usage window test pins its clock.** `apps/server/src/usage/UsageService.test.ts` takes upstream's `1280ccd0d2` (pingdotgg/t3code#28603) unchanged: "reports saved usage of a removed directory only for windows it reaches" sets `TestClock` to 2026-08-15 and provides `TestClock.layer()`. The test ran on the wall clock with a transcript dated 2026-08-01, and the service prunes saved usage older than 92 days, so from 2026-11-01 it failed on every J5 PR. Exit: drop this entry at the next upstream advance, which brings the same commit; the file then equals upstream's.
- **The mobile Working section releases a finished thread** (#842). `apps/mobile/src/features/threads/useThreadListV2.ts` takes upstream's own fix ahead of the pin: pingdotgg/t3code#28169 (`be58795d37`, merged 2026-10-31), applied unchanged, so the file equals upstream's at that commit. Upstream's `useMemo` listed the inbox tracker's version without reading it, and the React Compiler memoizes on what a callback reads, so a thread whose exit grace expired stayed under Working until some thread shell changed; Jackson hit it on his iPhone at Checkpoint 1. `apps/mobile/vite.config.ts` adds J5's `useThreadListV2.test.tsx` to `reactTests`, so the test runs under the release compiler. Exit: at the next advance, take upstream's `useThreadListV2.ts`; keep the test and its `reactTests` entry, or drop both if upstream adds its own.

Retired at the 2026-10-08 advance, when upstream's own fix landed or the maintainer ruled the patch out: the Codex generator steps and the protocol drift they absorbed (upstream regenerated its bindings), the migrated Codex replay transcripts (the fixtures are upstream's, unmodified; `scripts/j5/migrate-codex-fixtures.mjs` stays for the old recordings should upstream's tests need it), the SQLite boolean bindings (upstream's `booleanInt`), the remote pairing scopes (pingdotgg/t3code#9785 merged), committed Stop (register D5 and D23, removed by decision 10; its watch signal went with it), and the skip on upstream's V1 cutover test (`orchestration-v2/legacy/LegacyV1Cutover.integration.test.ts` equals upstream's and passes).

Expand Down Expand Up @@ -839,6 +840,8 @@ explicitly marked.
| `apps/server/src/serviceLauncher.ts` | R | H, 50, 85 |
| `apps/server/src/storageCleanup.ts` | N | Git 2.56 gitfile patch |
| `apps/server/src/usage/UsageService.test.ts` | N | saved-usage clock pin |
| `apps/mobile/src/features/threads/useThreadListV2.ts` | N | Working section release patch |
| `apps/mobile/vite.config.ts` | N | Working section release patch |
| `apps/server/src/telemetry/AnalyticsService.ts` | N | B, 63, 86 |
| `apps/server/src/telemetry/FeatureUsage.ts` | N | 96 |
| `apps/server/src/testUtils/shardWeights.json` | N | 65 |
Expand Down
83 changes: 83 additions & 0 deletions apps/mobile/src/features/threads/useThreadListV2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { WORKING_EXIT_GRACE_MS } from "@t3tools/client-runtime/state/thread-inbox";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { expect, it, vi } from "vite-plus/test";

vi.mock("../../state/olderSettledShelf", () => ({
useOlderSettledShelf: () => ({
isListed: () => true,
unloadedCount: () => 0,
loading: false,
failed: false,
}),
useOlderSettledShelfGrowth: () => {},
}));

import { makeThreadShellFixture } from "../../test-fixtures";
import { useThreadListV2 } from "./useThreadListV2";

const NOW = "2026-06-02T00:00:00.000Z";
const running = {
status: "running" as const,
activeRunId: null,
providerInstanceId: ProviderInstanceId.make("codex"),
providerName: "Codex",
lastError: null,
updatedAt: NOW,
};

// Runs under the release React Compiler (vite.config.ts `reactTests`), as the app does.
it("returns a finished thread from Working once its grace ends, with nothing else changing", () => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
const container = document.createElement("div");
const root = createRoot(container);
try {
const working = makeThreadShellFixture({
id: ThreadId.make("new-thread"),
title: "Say hello",
runtime: running,
});
const finished = { ...working, runtime: null };
function Probe({ threads }: { readonly threads: ReadonlyArray<typeof working> }) {
const { layout } = useThreadListV2({
threads,
pendingTasks: [],
environmentId: null,
projectRefs: null,
searchQuery: "",
searchMatches: new Map(),
settlementEnvironmentIds: new Set(),
snoozeEnvironmentIds: new Set(),
pinReorderEnvironmentIds: new Set(),
activeReorderEnvironmentIds: new Set(),
queuedThreadKeys: new Set(),
settledLimit: 10,
now: NOW,
pendingOrder: null,
shelfPreferencesLoaded: true,
workingShelfEnabled: true,
workingShelfExpanded: false,
snoozedShelfExpanded: false,
settledShelfExpanded: false,
selectedThreadKey: null,
});
return <span>{layout.workingCount}</span>;
}
const workingCount = () => Number(container.textContent);
const workingThreads = [working];
const finishedThreads = [finished];
act(() => root.render(<Probe threads={workingThreads} />));
expect(workingCount()).toBe(1);
// The turn ends: the shelf holds the thread for the grace period.
act(() => root.render(<Probe threads={finishedThreads} />));
expect(workingCount()).toBe(1);
// Only the grace timer fires; the thread list itself does not change again.
act(() => vi.advanceTimersByTime(WORKING_EXIT_GRACE_MS));
expect(workingCount()).toBe(0);
} finally {
act(() => root.unmount());
vi.useRealTimers();
}
});
14 changes: 12 additions & 2 deletions apps/mobile/src/features/threads/useThreadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ function useKeySet(newlineSeparatedKeys: string): ReadonlySet<string> {

type LayoutInput = Parameters<typeof buildThreadListV2Items>[0];

/**
* The React Compiler memoizes on the values a callback reads and drops unread entries from a
* dependency list, so the version that marks an expired grace must be read to rebuild the list.
*/
function observeInbox(
threads: Parameters<typeof threadListInboxReturns.observe>[0],
_expiredGraceVersion: number,
) {
return threadListInboxReturns.observe(threads);
}

/**
* Home's and the thread sidebar's thread list. Its result is one value a screen only reads:
* built inline, the list's inputs stayed open to mutation across the screen's other hooks, so
Expand Down Expand Up @@ -62,8 +73,7 @@ export function useThreadListV2(input: {
threadListInboxReturns.getSnapshot,
);
const inbox = useMemo(
() => threadListInboxReturns.observe(workingShelfEnabled ? threads : null),
// oxlint-disable-next-line react/memo-dependencies -- the version signals an expired grace
() => observeInbox(workingShelfEnabled ? threads : null, inboxVersion),
[threads, workingShelfEnabled, inboxVersion],
);
// Up/down menu availability for every card, computed once per section per
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const reactTests = [
"src/state/queries.debounce.test.tsx",
"src/state/use-selected-thread-git-actions.test.tsx",
"src/features/terminal/useTerminalExitNavigation.test.tsx",
"src/features/threads/useThreadListV2.test.tsx",
];

const sourceRoot = `${normalizePath(NodeURL.fileURLToPath(new URL("./src", import.meta.url)))}/`;
Expand Down
Loading