Skip to content

sync: absorb upstream v0.0.30 (27 commits) - #33

Merged
NoahHendrickson merged 32 commits into
customfrom
claude/sync-upstream-v0.0.30
Jul 29, 2026
Merged

sync: absorb upstream v0.0.30 (27 commits)#33
NoahHendrickson merged 32 commits into
customfrom
claude/sync-upstream-v0.0.30

Conversation

@NoahHendrickson

@NoahHendrickson NoahHendrickson commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Brings custom up to upstream/main at 694f8d1c6 (v0.0.30) — 27 commits, including the sidebar-v2-by-default rollout, the queued-row rebuild, 33 web UI fixes, websocket/gzip perf work, and the agent-guidance docs overhaul.

Conflict resolutions

  • AGENTS.md — upstream rewrote the whole document. Took upstream's new body wholesale and re-appended the two fork sections at the end: the fork-authored Change Scope guidance and the fenced Fork Workflow section (fork-workflow-docs guard requires the fences). CLAUDE.md symlink blob untouched.
  • AppSidebarLayout.tsx — upstream's new artwork-gated trigger styling only applies while the sidebar is visible, a state the fork's floating control never renders in (fork-sidebar-chrome moved the toggle into the header). Dropped the dead lines and the now-unused useEnvironmentIdentificationMode import.
  • SidebarChrome.tsx — kept the fork header (always-on brand backdrop, inline toggle, APP_BASE_NAME brand) per fork-sidebar-chrome/fork-app-identity intent; the fork's local stage-label hook is replaced by upstream's equivalent new useEnvironmentStageLabel. Took upstream's simplified footer (fork never touched it).
  • SidebarV2.tsx — upstream redesigned its card row in the same region sidebar-v2-card-rows/sidebar-v2-row-action-hit-area own; fork design wins in the fenced spans, upstream's tooltip/dialog/ghost-row refinements outside the fences are absorbed. Upstream's fixedHeader control rows lose to the fork's SidebarV2SearchRow/SidebarV2ProjectScopeRow (the fork list already self-scrolls, so header pinning is preserved).

Follow-up fixes in the sync

  • PaletteIcon added to the lucide→Phosphor shim (upstream's new appearance settings category imports it — caught by the shim guard).
  • FolderIcon re-imported in SidebarV2.tsx for upstream's redesigned project-actions dialog.

Behavioral note

Upstream's new environment identification setting (artwork/pill) does not affect the fork's sidebar header — that art is always-on brand chrome per fork-sidebar-chrome. The setting still governs upstream's other art surfaces (composer send button, auth screen).

Verification

  • Fork guard suite: 22 files / 175 tests pass (172 at review time; +3 guards added for the review feedback). An earlier draft of this section said "846/846" — that run was launched from the repo root, where vp test run apps/web/src/__fork_guards__ path-matches the stale .claude/worktrees/* review checkouts and counts their copies of the suite too. Run it from apps/web (vp test run src/__fork_guards__) to reproduce the real number.
  • apps/web typecheck: clean
  • .fork/lint-owned.mjs: 52 files, no warnings; .fork/detect-drift.mjs report reviewed — every flagged customization's guard is green
  • Focused tests for touched watch files (branding, SidebarStageBackdrop, DesktopAppIdentity, ComposerPrimaryActions): all pass
  • Live browser pass: fork sidebar renders seeded threads; environment-identification modes each verified (artwork / pill / none); chrome-row icon tint confirmed restored; search/scope rows confirmed pinned while the list scrolls

🤖 Generated with Claude Code

t3dotgg and others added 29 commits July 27, 2026 21:41
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…g#4705)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…4787)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
PaletteIcon joins the lucide->Phosphor shim (upstream's new appearance
settings category imports it), and SidebarV2 re-imports FolderIcon for
upstream's redesigned project-actions dialog — its previous import
arrived with the upstream scope-menu code the fork replaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Jul 29, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thermo-nuclear code quality review — not approved.

Fork conflict absorption for the named surfaces (AppSidebarLayout, SidebarChrome, AGENTS.md, PaletteIcon shim) is mostly clean. The approval bar fails on structural regressions this sync pulls in and on a permanent dual-path the fork absorb locks in.

Ordered findings

  1. Structural regression — GitVcsDriverCore listRefs cache lattice (2581 → 2796 LOC; test 976 → 1445, crosses 1k). Dual epoch + generation Maps, dual Effect caches, and a while (true) coherence spin is storm-correct and design-wrong. Extract a single GitRefsSnapshotCache (get/invalidate, one monotonic version) instead of absorbing another lattice into a 2.7k driver.

  2. Missed code-judo / dual-path debt — environment identification. Fork header is always-on brand chrome; composer/auth still honor the new setting; Appearance copy claims the setting chooses how Dev/Nightly are identified. That is three policies for one concept. Collapse behind one surface-aware resolver (sidebarHeader: always-artwork) and make the settings copy tell the truth for this fork.

  3. Spaghetti / god-file growth — Appearance panel appended to SettingsPanels (1765 → 1839). New category belongs in its own module; the route is already a one-liner.

  4. Boundary / dual resolve — ChatMarkdown + markdown-links. Prefetch map with runtime ?? fallback keeps policy split across a 1.6k component and the new module. Resolve once; delete the fallback path.

  5. 1k-line rule — GitVcsDriverCore.test.ts crossed 1000 (+469). Split by cache/invalidation concern before this file becomes the next dumping ground.

  6. Maintainability residue — SidebarV2 stays a fence nuclear zone (~2976 LOC, dozens of fork:begins; unbalanced sidebar-v2-card-rows fence still present). Sync preserved intent but did not reduce concept count. Prefer custom/ / overrides/ ownership over more leaf fences on the next touch.

Credit

Prompt-stash provider-scope removal, dead artwork-gated trigger drop in AppSidebarLayout, and the single useSidebarV2Enabled door are real simplifications.

Approval bar

Blocked: unjustified god-file / 1k growth, absorb of a dual-cache lattice that should have been extracted, and a fork dual-path that makes the new identification setting partially lie. Fix or clearly justify those before this should merge on maintainability grounds.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

Comment on lines +2290 to +2371
const listRefsEpochByCommonDir = new Map<string, number>();
let listRefsEpochSequence = 0;
const bumpListRefsEpoch = (gitCommonDir: string): number => {
const nextEpoch = ++listRefsEpochSequence;
listRefsEpochByCommonDir.delete(gitCommonDir);
listRefsEpochByCommonDir.set(gitCommonDir, nextEpoch);
if (listRefsEpochByCommonDir.size > LIST_REFS_SNAPSHOT_CACHE_CAPACITY) {
const oldestKey = listRefsEpochByCommonDir.keys().next().value;
if (oldestKey !== undefined) {
listRefsEpochByCommonDir.delete(oldestKey);
}
if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
yield* Effect.logWarning(
`GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
);
}
return nextEpoch;
};
const listRefsGenerationByCommonDir = new Map<string, number>();
let listRefsGenerationSequence = 0;
const setListRefsGeneration = (gitCommonDir: string, generation: number): number => {
listRefsGenerationByCommonDir.delete(gitCommonDir);
listRefsGenerationByCommonDir.set(gitCommonDir, generation);
if (listRefsGenerationByCommonDir.size > LIST_REFS_SNAPSHOT_CACHE_CAPACITY) {
const oldestKey = listRefsGenerationByCommonDir.keys().next().value;
if (oldestKey !== undefined) {
listRefsGenerationByCommonDir.delete(oldestKey);
}

const defaultBranch =
defaultRef.exitCode === 0
? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
: null;

const worktreeMap = new Map<string, string>();
if (worktreeList.exitCode === 0) {
let currentPath: string | null = null;
for (const line of worktreeList.stdout.split("\n")) {
if (line.startsWith("worktree ")) {
const candidatePath = line.slice("worktree ".length);
const exists = yield* fileSystem.stat(candidatePath).pipe(
Effect.map(() => true),
Effect.orElseSucceed(() => false),
}
return generation;
};
const currentListRefsGeneration = (gitCommonDir: string): number => {
const current = listRefsGenerationByCommonDir.get(gitCommonDir);
return current === undefined
? setListRefsGeneration(gitCommonDir, ++listRefsGenerationSequence)
: setListRefsGeneration(gitCommonDir, current);
};
const bumpListRefsGeneration = (gitCommonDir: string): number =>
setListRefsGeneration(gitCommonDir, ++listRefsGenerationSequence);
const listRefsSnapshotCache = yield* Cache.makeWith(
(cacheKey: GitRefsSnapshotCacheKey) => readGitRefsSnapshot(cacheKey.gitCommonDir),
{
capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_REFS_SNAPSHOT_CACHE_TTL : Duration.zero),
},
);
const listRefsRefreshSnapshotCache = yield* Cache.makeWith(
(cacheKey: GitRefsRefreshCacheKey) =>
Effect.suspend(() => {
const epoch = bumpListRefsEpoch(cacheKey.gitCommonDir);
return Cache.get(
listRefsSnapshotCache,
new GitRefsSnapshotCacheKey({ gitCommonDir: cacheKey.gitCommonDir, epoch }),
);
}),
{
capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
timeToLive: (exit) =>
Exit.isSuccess(exit) ? LIST_REFS_REFRESH_COALESCE_TTL : LIST_REFS_REFRESH_FAILURE_COOLDOWN,
},
);
const resolveListRefsSnapshot = Effect.fn("resolveListRefsSnapshot")(function* (
gitCommonDir: string,
refresh: boolean,
) {
while (true) {
const generation = currentListRefsGeneration(gitCommonDir);
const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);
const snapshot =
refresh || currentEpoch === undefined
? // The refresh cache owns the complete snapshot read, rather than only the
// epoch bump. Slow repositories therefore remain singleflight for the
// entire Git scan even when more refresh requests arrive after the
// coalescing TTL would otherwise have elapsed.
yield* Cache.get(
listRefsRefreshSnapshotCache,
new GitRefsRefreshCacheKey({ gitCommonDir, generation }),
)
: yield* Cache.get(
listRefsSnapshotCache,
new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),
);
currentPath = exists ? candidatePath : null;
} else if (line.startsWith("branch refs/heads/") && currentPath) {
worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
} else if (line === "") {
currentPath = null;
}
}
if (currentListRefsGeneration(gitCommonDir) === generation) {
return snapshot;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structural regression / missed code-judo. This absorb lands a second cache lattice (epoch Map + generation Map + snapshot cache + refresh cache + while (true) coherence spin) inside an already ~2.8k driver.

Storm-coalescing is the right goal. This shape is not. Epoch and generation are doing overlapping jobs, and the spin loop is the tell that the model is fighting itself.

Extract something like GitRefsSnapshotCache with get(commonDir, { refresh }) / invalidate(cwd) and one monotonic version. Delete either epoch or generation — not both. Same treatment as the repository-paths dual cache above it. Absorbing upstream without that extraction makes the driver harder to reason about every sync.

Comment on lines 38 to 44
{/* Always, not only on a non-prod build: in the fork this is brand
chrome rather than a channel cue. Upstream's other two art surfaces
(composer send button, auth screen) still gate on the variant and are
untouched, so they keep signalling Dev. */}
chrome rather than a channel cue, so it also ignores the environment
identification setting upstream added for its own header art — that
setting still governs upstream's other two art surfaces (composer
send button, auth screen), which are untouched and keep signalling
Dev. */}
<ForkSidebarHeaderBackdrop stageLabel={stageLabel} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dual-path debt from the absorb. Fork header artwork is always-on brand chrome and intentionally ignores the new environment-identification setting, while composer/auth still honor it (ComposerPrimaryActions via useEnvironmentIdentificationMode). Appearance settings then describe the setting as choosing how Dev/Nightly are identified — which is false for this surface.

That is three policies for one concept, glued together with comments. Collapse behind one fork resolver, e.g. resolveEnvironmentIdentification({ surface }) with an explicit sidebarHeader: 'always-artwork' policy, and point header / composer / auth / settings copy at it. Otherwise every future upstream art change will re-open this conflict.

}

export function GeneralSettingsPanel() {
export function AppearanceSettingsPanel() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File-size / decomposition. AppearanceSettingsPanel is a new settings category appended into an already ~1.8k god file (1765 → 1839). The route (settings.appearance.tsx) is already a thin shell — the panel should live in its own module now, not after the next appearance feature forces a split under pressure.

Also: the Environment identification row copy below claims users "Choose how Dev and Nightly environments are identified," but the fork sidebar header ignores that setting. Either move this panel out and make the description surface-accurate, or the settings UI is lying.

Comment on lines +1574 to +1576
const fileLinkMeta =
inlineCodeFileLinkMetaByText.get(codeText.trim()) ??
resolveInlineCodeFileLinkMeta(codeText, cwd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boundary leak / dual resolve. Prefetch map + runtime ?? resolveInlineCodeFileLinkMeta(...) means inline-code link policy still has two call sites — one "optimized" path and a silent fallback inside a 1.6k component — while markdown-links.ts was supposed to own the model.

Resolve once in markdown-links (or guarantee the map is complete for every span the renderer can see) and delete this fallback. If the map can miss, that is an invariant bug, not a reason to keep a second resolver in the leaf.

Comment on lines +54 to +56
return input.configuredByUser || input.enabled
? input.enabled
: resolveSidebarV2Default(input.stageLabel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ghost state in the hot path. configuredByUser || enabled ? enabled : default preserves a legacy third state ("stored true without the companion flag") on every resolve. The migration intent is real; keeping it as permanent runtime branching is not.

Code-judo: one-shot normalize on settings load (enabled && !configuredByUser → set configuredByUser), then the runtime path is just configuredByUser ? enabled : default(stage). Delete the ghost branch so sidebar-v2 resolution stops carrying forever-debt for a schema that no longer needs it.

const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);
const currentView = viewStack.at(-1) ?? null;
const [browseGeneration, setBrowseGeneration] = useState(0);
const browseNavigationRef = useRef<ReturnType<typeof createBrowseNavigationCoordinator> | null>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

God-component growth (2094 → 2192). The new shared filesystem browse coordinator is the right idea — initializing/owning it inside CommandPalette.tsx is not. Pull browse orchestration into something like useCommandPaletteBrowse so the palette stops accumulating another independent subsystem. Next sync will otherwise keep paying the 2k+ LOC tax here.

listEmpty
);

if (threadListV2Enabled) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dual tree about to cross 1k (983 → 998). if (threadListV2Enabled) swapping entire list implementations inside HomeScreen is the spaghetti form of a feature flag. Split HomeThreadListV1 / HomeThreadListV2 (or delete v1 now that v2 is default) before this file tips over 1000 with the next mobile polish pass.

Copy link
Copy Markdown
Owner Author

Review: upstream v0.0.30 sync

I reviewed this as a conflict-resolution PR, not as 27 upstream commits — the fork's actual authored surface here is the merge resolution in 13 files plus 2a6d5f1c. I re-ran everything locally on 2a6d5f1c after a fresh pnpm install --frozen-lockfile.

The resolutions are good. I checked each of the four documented ones against both parents and they hold up — I've listed the verification at the bottom. What I want to push on is what this sync didn't write down, plus one visual regression the merge structurally cannot see.


1. The environment-identification setting is now partly inert in the fork, and nothing records that

Upstream pingdotgg#4652 added environmentIdentificationMode: "artwork" | "pill" | "none", described in Settings as "Choose how Dev and Nightly environments are identified." The resolution dropped upstream's pill from SidebarChromeHeader and left ForkSidebarHeaderBackdrop unconditional.

Net effect on a fork Dev build — the only build where the row is even shown (showEnvironmentIdentification gates on the stage label):

Setting Sidebar header Pill Composer / auth
Artwork fork art art
Version pill fork art (unchanged) nothing, anywhere plain
None fork art (unchanged) plain

Two of the three options do nothing to the surface the setting is named after, and "Version pill" shows no pill at all — upstream only ever rendered it in SidebarChromeHeader, which the fork replaced. The setting's own description is false in the fork.

The "Behavioral note" in the PR body is the right call, but a PR body doesn't survive the merge. Nothing else catches this either: upstream ships no render test on the pill, and forkSidebarChrome.test.ts asserts the art is always-on and that the send button/auth screen stay gated — it never asserts the header ignores the mode. A future sync can re-add the pill, or gate the art on the setting, and everything stays green.

Pick one and land it with the sync: honor pill/none in the fork header, or keep today's behavior and give the setting copy the fork can honestly own. Either way it needs intent on .fork/customizations.yaml#fork-sidebar-chrome and a guard.

2. Zero manifest or guard changes in a sync that made four design decisions

git diff custom...HEAD -- .fork apps/web/src/__fork_guards__ is empty. Across 147 changed files the only fork-owned edit is two lines in the icon shim.

But this sync decided: drop upstream's fixedHeader control rows; drop the pill; decline upstream's status-slot rework in favor of the fork's grid cell; swap the fork's stage-label hook for upstream's. .fork/AGENTS.md is unambiguous that every customization carries manifest intent plus a guard asserting the outcome, "so an upstream sync that silently drops the customization turns CI red instead of passing quietly." These are customizations in everything but name, and right now the only record of the reasoning is this PR description. Next sync re-litigates all four from scratch.

3. Likely visual regression: sidebarMenuButtonVariants restyle lands on fork-owned chrome

ui/sidebar.tsx merged cleanly — no conflict, so it got no attention. Its base class gained:

[&>svg]:text-sidebar-muted-foreground [&>svg]:opacity-60
hover:[&>svg]:text-sidebar-foreground hover:[&>svg]:opacity-100 …

custom/SidebarV2ChromeRows.tsx renders SearchIcon, PlusCircleIcon, FolderOpenIcon, ChevronsUpDownIcon as direct children of SidebarMenuButton, each carrying text-sidebar-muted-foreground/80. .[&>svg]:…>svg is specificity (0,1,1); the icon's own class is (0,1,0) — the parent wins. So the fork's deliberate /80 tint is now dead, and those icons render at 60% opacity, compounding with Phosphor duotone's own secondary-layer alpha. That's precisely the case the shim's weight choices were tuned around.

cn/twMerge can't help — the conflicting classes are on different elements. Typecheck is clean and every guard passes, because none of them look at rendered opacity. Please eyeball the search/scope rows; if it reads washed out, [&>svg]:opacity-100 on TRAILING_BUTTON and the search trigger's class fixes it.

The same base change also drops the footer Settings icon from 18px to 16px and dims it. The fork never customized the footer so taking upstream is right — flagging only so it isn't a surprise.

4. ## Change Scope in AGENTS.md is unfenced, unmanifested, unguarded — and this PR moved it

It was fork-authored at the top of the doc, immediately after # AGENTS.md and ahead of Task Completion Requirements. It now sits at the bottom, below upstream's entire rewritten body. For a file that gets loaded into every agent's context, position is the whole point of the section.

Unlike the Fork Workflow section right below it, it carries no fork:begin/fork:end, appears in no watch: list, and no guard mentions the string. Upstream's next AGENTS.md rewrite deletes it outright and CI stays green — the exact failure mode .fork/AGENTS.md §"Every customization requires" exists to prevent. Fence it, add it to fork-workflow-docs, and assert its position ahead of upstream's first heading.

5. Pre-existing, but this sync is the moment to fix it: unterminated fence in SidebarV2.tsx

fork:begin sidebar-v2-card-rows at apps/web/src/components/SidebarV2.tsx:986 has no matching fork:end. The file is 38 begins / 37 ends (identical on custom, so not introduced here); the fork:end at 1141 closes the fence opened at 1137, leaving the card-row padding hunk — px-3 py-2.5, gap-[7px], the contain-intrinsic-size hints — with an open marker and no boundary.

Nothing catches it. customizationsManifest.test.ts only does content.matchAll(/fork:begin ([a-z0-9-]+)/gu) to collect ids; neither detect-drift.mjs nor lint-owned.mjs looks at pairing at all. A fence-balance assertion in the manifest guard is a few lines and would have failed today. Given this sync rewrote code on both sides of that exact region, it's worth closing now rather than carrying into the next one.

6. The verification numbers don't reproduce

On 2a6d5f1c, fresh install:

  • vp test run apps/web/src/__fork_guards__22 files / 172 tests passed. The description says "846/846". Everything is green, so this isn't a correctness problem — but 846 isn't the guard suite's test count, and the next reviewer can't reproduce it. Worth stating what it counts, or dropping it.
  • .fork/lint-owned.mjs52 fork-owned files, no warnings — exact match ✅
  • apps/web typecheck → clean ✅
  • branding / SidebarStageBackdrop / ComposerPrimaryActions / Sidebar.logic / settings → 11 files / 145 passed ✅

What I verified and found correct

  • AGENTS.md — zero upstream lines lost, both fork sections present. CLAUDE.md is still the 9-byte no-newline symlink blob fork-workflow-docs requires.
  • SidebarChrome — the resolveSidebarStageBadgeLabeluseEnvironmentStageLabel swap is genuinely equivalent; the former is a one-line passthrough to resolveServerBackedAppStageLabel. Note its only remaining caller is its own test — a dead export, but upstream's to remove.
  • Upstream Prevent sidebar row labels from truncating pingdotgg/t3code#4789 (row truncation) — the font-mono half landed on WorkingDuration. The layout half is correctly superseded: the fork's grid cell is zero-width at rest and widens on hover/focus, so the title re-truncates against the visible state, which is the same outcome upstream's min-w-8 + absolute rework was reaching for.
  • Skipping fixedHeader — right call. The fork's rows sit above a min-h-0 flex-1 overflow-y-auto SidebarGroup, and Sidebar.tsx (v1) still passes fixedHeader, so the prop isn't dead.
  • Stale overrides — none. apps/web/src/overrides/ holds only README.md, so no shadowed file went quietly out of date behind upstream's changes.
  • ChatComposer.tsx / index.css — both sides fully applied, result-vs-main equals the fork's own delta exactly. Fence counts are byte-identical pre/post in every watch file (checked with grep -a, per the NUL-byte warning in .fork/AGENTS.md).
  • Layering — the result diverges from upstream in 105 files, and not one is under apps/server, packages/contracts, packages/shared, packages/client-runtime, or apps/mobile. The fork stayed exactly where it says it lives.

One question

SidebarContent wraps its children in a ScrollArea whose inner content div is auto-height, and the fork hangs min-h-0 flex-1 overflow-y-auto off a child SidebarGroupflex-1 against a parent with no definite height. "The fork list already self-scrolls, so header pinning is preserved" is the load-bearing justification for declining upstream's fixedHeader, and it's unchanged from custom so it isn't a regression either way. It's still worth 30 seconds in a browser to confirm the search and scope rows actually stay put when the thread list is long, rather than scrolling away with it.


Verdict: the merge itself is careful work and I'd take it. §1 and §4 should land with it rather than after — both are cases where a real decision exists only in prose that the next sync won't read. §3 is worth a look before merge. §2 and §5 can be a follow-up if you'd rather keep this PR to one concern.


Generated by Claude Code

Honor upstream's environment-identification pill in the fork header:
the brand art never gates on the setting, but 'Version pill' now renders
upstream's badge (white-on-art) so every option does something visible;
guarded, and the decision is recorded in the fork-sidebar-chrome intent.

Restore the fork chrome rows' icon tint: sidebarMenuButtonVariants'
new parent-level [&>svg] pair (muted at opacity-60) outweighed the
icons' own /80 class; the fork buttons now re-assert it via twMerge.

Move the fork-authored Change Scope section back ahead of upstream's
first heading in AGENTS.md and fence it as fork-change-scope, with a
position guard under fork-workflow-docs.

Close the unterminated sidebar-v2-card-rows fence and add a repo-wide
fence-balance assertion to the manifest guard.

Record the sync's four resolution decisions (pill, fixedHeader decline,
status-slot decline, stage-label hook swap) in manifest intents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NoahHendrickson

Copy link
Copy Markdown
Owner Author

All feedback addressed in 755e8c7, verified live in a dev environment. Point by point:

§1 — environment identification. Landed the first of your two options, with a twist that keeps the fork intent intact: the header art still never consults the setting (brand chrome), but "Version pill" now renders upstream's badge in the fork header, white-on-art since it always sits on the artwork. That makes all three options distinct and true: artwork gates the composer/auth surfaces, pill shows a badge here, none shows no explicit identification. Verified all three modes in the browser. The decision is recorded in the fork-sidebar-chrome intent, and a new guard asserts both halves — the header never matches === "artwork" / never wraps the backdrop in a conditional, and the pill renders off === "pill" with the white-on-art treatment.

§2 — manifest record of the sync decisions. Landed with the sync rather than as a follow-up, since §1 forced the manifest open anyway. fork-sidebar-chrome now records the pill decision, the fixedHeader decline (and why a sync must not re-adopt it to shrink a conflict), and the useEnvironmentStageLabel swap with the equivalence argument. sidebar-v2-row-action-hit-area records declining upstream's status-slot rework and the "Settle" text label, plus the [&>svg] displacement from §3.

§3 — confirmed regression, fixed. Measured before the fix: search, new-thread, and new-project icons at computed opacity: 0.6 with the base's [&>svg]:text-sidebar-muted-foreground beating the fork's /80 tint on color too (the scope row's chevron escaped only because its trigger isn't a SidebarMenuButton). Fixed where you suggested — TRAILING_BUTTON and the search trigger's class re-assert [&>svg]:text-sidebar-muted-foreground/80 [&>svg]:opacity-100; twMerge drops the base's conflicting pair. Measured after: opacity: 1, color alpha 0.8. One deliberate delta from pre-sync behavior: the icons now brighten on hover via upstream's hover:[&>svg] rules, which matches the button text's existing hover and reads as an improvement. Footer left on upstream, as you said.

§4 — Change Scope. Moved ahead of upstream's first ## heading (after the H1 + intro paragraph), fenced as fork-change-scope under fork-workflow-docs, intent updated to state the position requirement, and the guard now asserts fences + position — with a comment noting this sync's bottom-of-file placement as the exact failure it catches.

§5 — fence closed and now guarded. fork:end sidebar-v2-card-rows added at the padding hunk's boundary (38 begins / 38 ends in SidebarV2.tsx). The manifest guard gained a repo-wide per-file fence-balance assertion over all tracked files; .fork/ and the guards are excluded because both quote fence markers as prose (the three .fork/notes/*.md handoffs each mention a begin without an end).

§6 — explained and corrected in the PR body. 846 was real but meaningless: run from the repo root, vp test run apps/web/src/__fork_guards__ path-matches the stale .claude/worktrees/* review checkouts and counts their copies of the suite. From apps/web it's 172 (175 with the three new guards). The body now says so. Those stale worktrees are worth pruning, and .claude may deserve the same vite.config.ts exclusion .t3 has — left out of this PR as a separate concern.

Your question — header pinning: confirmed in a real browser. Two checks: the scroller's box is bounded by the flex chain in normal layout (clientHeight 572 with the group's bottom at 704 in an 800px viewport — SidebarContent's flex column gives the flex-1 group a definite height; the ScrollArea inner div isn't in that chain in the fork's arrangement), and with the list forced to overflow (scrollHeight − clientHeight = 372) the first card scrolled from y=144 to y=−6 while the search row held y=58 and the scope row y=94 exactly.

@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Re the Cursor automation review: findings 2 and 6 overlap the human review and are addressed in 755e8c7 (the identification setting now behaves honestly in the fork — pill honored, decision recorded in the manifest; the unbalanced sidebar-v2-card-rows fence is closed and a fence-balance guard added).

The remaining findings (1, 3, 4, 5 — GitVcsDriverCore cache design, SettingsPanels growth, ChatMarkdown prefetch fallback, test-file size) all critique upstream-authored code arriving through the sync. This fork deliberately does not refactor upstream internals: restructuring GitVcsDriverCore or splitting SettingsPanels here would fork those files permanently and turn every future sync into a conflict, which is the exact failure mode .fork/AGENTS.md's placement ladder exists to prevent. Those critiques belong upstream at pingdotgg/t3code if anywhere. Not actioning them in this PR.

@NoahHendrickson NoahHendrickson left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: upstream v0.0.30 sync

High-effort multi-agent review of the full diff (152 files): 8 finder angles → 27 candidates → dedup to 20 → independent verification of each. 14 confirmed, 3 plausible, 3 refuted. Top 10 below, ranked most-severe first. Verdict legend: CONFIRMED = demonstrated from the code; PLAUSIBLE = realistic but not fully demonstrated.

A note on scope: findings 1, 2, 6, 7, 9, and 10 are upstream-authored behavior the sync carries — the ask there is a deliberate keep/counter decision (and a manifest intent where countered), not necessarily a code change in this PR. Findings 3, 4, 5, and 8 are fork-side and directly actionable here.


1. Branch list can be minutes stale after out-of-band git — CONFIRMED (upstream)

apps/server/src/vcs/GitVcsDriverCore.ts:2393

listRefs now serves a 2-minute-cached ref snapshot (LIST_REFS_SNAPSHOT_CACHE_TTL, line 61) with a 10-minute-cached current-branch fallback (REPOSITORY_PATHS_CACHE_TTL, line 57). Invalidation only wraps driver-initiated mutations (withListRefsInvalidation, line 2743), no production client ever sends the new refresh: true escape hatch (grep: only vcs.test.ts), and this diff also removed the branch picker's on-open branchRefState.refresh(). Run git checkout -b feature-x in a terminal and the picker misses the new branch for up to 2 minutes; when the snapshot's worktree-path match misses, the current-branch checkmark is wrong for up to 10. Pre-merge, every open re-ran git and was always fresh.

2. v1 prompt stash deleted at startup without migration — CONFIRMED (upstream, data loss)

apps/web/src/promptStashStore.ts:274

Module init unconditionally runs removeItem(LEGACY_PROMPT_STASH_STORAGE_KEY) and readPersistedEntries (line 177) reads only the v2 key. The v1 key shipped on custom pre-sync, so a fork user who stashed prompts before upgrading loses them (and their image attachments) unrecoverably on first launch. Upstream documents this as deliberate (lines 8–13, localStorage quota rationale) — but the sync silently accepts the data loss for existing fork users. Worth a conscious decision, and a release-note line if kept.

3. Fork header divergence sits above its own fork:begin fence — CONFIRMED (fork-side)

apps/web/src/components/sidebar/SidebarChrome.tsx:54

The fork's central divergence in this header — the always-rendered ForkSidebarHeaderBackdrop (line 54), its rationale comment (48–53), the px-0 header padding rewrite (40–46), and the ~/custom import (line 13) — all sit above the fork:begin fork-sidebar-chrome fence, which opens at line 55. Fence-based sync audits can't see those lines; the string-grep guards backstop only the backdrop render, not the padding rewrite. The next conflict here can be resolved to upstream's side straight through the unfenced lines. Fix: open the fence before line 13/40 so the contested lines live inside the mechanism the sync workflow actually honors.

4. [&>svg] counter-override duplicated in two literals, no guard — CONFIRMED (fork-side)

apps/web/src/custom/SidebarV2ChromeRows.tsx:85, 109

The [&>svg]:text-sidebar-muted-foreground/80 [&>svg]:opacity-100 pair that displaces upstream's new icon dim is pasted into both TRAILING_BUTTON and the search-row className, coupled only by a "mirrors TRAILING_BUTTON's" comment. No guard covers it (grep of __fork_guards__ for opacity-100/&>svg: zero hits). Next upstream restyle of that slot — or a selector-shape change ([&_svg], data-slot CSS, ! modifier) that twMerge no longer displaces — double-fades one row's duotone icons with every test green: the exact regression this pair was added to fix. Fix: hoist into one shared const, and add a guard asserting the merged cn(sidebarMenuButtonVariants(...), TRAILING_BUTTON) outcome drops opacity-60.

5. Fence-balance guard scans all ~15.6k tracked files — CONFIRMED (fork-side)

apps/web/src/__fork_guards__/customizationsManifest.test.ts:109

The new balance test runs its own git ls-files + readFileSync walk over every tracked file (~195 MB including ~13k vendored .repos/ files; measured ~3.8s warm per run), duplicating the sibling test's walk at line 77 with already-diverged filters (extension whitelist vs. everything; .fork/ excluded in only one), and compiles fresh RegExps per file per kind. Only ~43 tracked files contain fence markers. Cheaper and convergent: one shared git grep -lE "fork:(begin|end)" candidate pass (~0.7s measured) feeding both tests, one precompiled /fork:(begin|end) ([a-z0-9-]+)/gu matchAll.

6. Thread error tooltip hardcodes "Error occurred" — CONFIRMED (upstream)

apps/web/src/components/SidebarV2.tsx:359

custom previously rendered {thread.session.lastError} in the tooltip (old line 352); upstream's restyle (dd5ea32) replaces it with the literal string Error occurred, and grep shows the real message now renders nowhere in the sidebar — only inside the opened chat view. Diagnosing a failed session from the sidebar is no longer possible. Candidate for a small fenced fork restore.

7. Mobile thread-list opt-out ignored until prefs load — CONFIRMED (upstream)

apps/mobile/src/features/threads/threadListV2.ts:37

resolveThreadListV2Enabled returns the default (true) whenever preferences aren't successfully loaded. A device with threadListV2Enabled: false persisted mounts the v2 list every launch and remounts to v1 a tick later (losing scroll/expanded-group state); if both the SQLite and secure-storage reads fail (mobile-preferences.ts:228), loaded stays false forever and the opt-out is ignored all session. The startup window is documented as a tradeoff (lines 28–31); the persistent-failure path isn't.

8. Comment + manifest wrongly claim the setting gates auth-screen art — CONFIRMED (fork-side, docs)

apps/web/src/components/sidebar/SidebarChrome.tsx:48

The new comment ("that setting still governs upstream's other two art surfaces (composer send button, auth screen)", and "none produces neither pill nor composer/auth art") and the matching .fork/customizations.yaml text are factually wrong about the auth half: AuthSurfaceShell.tsx:11 resolves stage art with enabled defaulting true and never reads environmentIdentificationMode — upstream never gated that surface either (the mode gates only AppSidebarLayout, ComposerPrimaryActions, SidebarChrome). The manifest is the fork's sync-resolution contract; wrong text invites a wrong conflict resolution or a bogus bug hunt next sync. Fix the comment + manifest wording (or actually gate the auth surface).

9. Stored sidebarV2Enabled: false flips to v2 on Dev/Nightly — PLAUSIBLE (upstream)

apps/web/src/hooks/useSettings.ts:251

The old schema recorded the Beta toggle as plain false with no configuredByUser companion, so a pre-sync explicit opt-out decodes as {enabled: false, configuredByUser: false} and resolveSidebarV2Enabled (branding.logic.ts:53–57) applies the stage default — Dev/Nightly users who turned v2 off come up with v2 on after upgrading, plus one documented v1→v2 remount after async hydration. Upstream knowingly accepted the ambiguity (settings.ts:124–128) as part of the beta rollout; flagging because pre-sync opt-outs are silently overridden.

10. acceptsGzip misses gzip ; q=1 — coding token not trimmed — CONFIRMED (upstream, minor)

apps/server/src/http.ts:54

entry.trim().toLowerCase().split(";") trims the entry but never the coding token after the split (parameters do get .trim() at line 56), so the RFC-9110-legal OWS-before-; form yields the key "gzip " and accepted.get("gzip") misses — that client silently gets uncompressed responses. One-word fix: trim the coding after the split.


Refuted during verification (for the record): header art ignoring the "None" identification mode is a documented, guard-tested fork decision, not a defect; the fork-change-scope fence-id/anchor shape follows the pre-existing fork-workflow convention with manifest + guard coverage; the declined "Settle" text label is already enforced by sidebarV2RowActionHitArea.test.ts. Seven further low-severity confirmed items (mostly fragile string-window assertions in the new guard tests) fell below the 10-finding cap.

🤖 Generated with Claude Code

NoahHendrickson and others added 2 commits July 29, 2026 02:10
Fence the whole fork-divergent header in SidebarChrome — the always-on
backdrop, px-0 rewrite, hooks, and the ~/custom import sat above the
fork:begin, outside what a sync conflict resolution honors.

Hoist the [&>svg] icon-tint counter-override into one shared
CHROME_ROW_ICON_TINT const and guard the merged outcome, so a base
selector reshape that stops displacing upstream's dim turns CI red.

Share one git-grep candidate pass across both manifest fence tests
instead of two full-tree walks (test phase 3.6s -> 0.25s).

Correct the comment and manifest claim that the identification setting
gates the auth screen — upstream gates that art on the build channel
alone; the setting's fork-visible effects are the composer button and
the header pill.

Restore the session's actual last error in the thread tooltip, which
upstream's restyle flattened to a literal 'Error occurred'; new
sidebar-v2-error-tooltip customization with manifest intent and guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…se note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NoahHendrickson

Copy link
Copy Markdown
Owner Author

All ten findings triaged; fork-side fixes landed in 458acfc (+ ac649cb for the release note), guards 178/178, typecheck and fork-lint clean.

Fork-side, fixed:

#3 — fence coverage. The fork:begin fork-sidebar-chrome now opens at the top of SidebarChromeHeader's body — covering the hooks, the px-0 padding rewrite, the backdrop render and its rationale — and the ~/custom import got its own fence pair. Everything the sync workflow needs to protect in that header now sits inside the mechanism it honors.

#4 — icon tint hoisted and guarded. One exported CHROME_ROW_ICON_TINT const, referenced by both buttons. The new guard asserts the merged outcome: it extracts sidebarMenuButtonVariants' base string from the cva call (upstream doesn't export it, and importing the component would drag the sidebar module graph into the test), runs it through cn with the tint, and asserts no svg]:opacity-60 survives — the pattern is selector-shape-agnostic, so an upstream move to [&_svg] that breaks twMerge's conflict grouping reds instead of double-fading silently. Plus a one-spelling check on the literal.

#5 — guard perf. Both fence tests now share one module-level git grep --text -lE candidate pass (~40 files instead of ~15k; --text because ChatComposer.tsx's NUL bytes would otherwise get the file classified binary — the same trap .fork/AGENTS.md documents for plain grep) and one precompiled fork:(begin|end) regex. Suite test phase went from ~3.6s to ~0.25s. The claims test keeps its extension whitelist deliberately — markdown fence ids (fork-workflow, fork-change-scope) are anchors into the manifest, not manifest ids.

#8 — docs corrected. You're right: AuthSurfaceShell resolves stage art with enabled defaulting true and never reads the mode. The header comment, pill comment, and the manifest paragraph now say the setting's fork-visible effects are the composer send button and the header pill, with the auth screen channel-gated only. Chose to fix the text rather than gate the surface — gating auth art on an appearance setting wasn't upstream's behavior either, and inventing it here would be a new divergence.

Upstream-carried, decided:

#6 — countered. The tooltip renders thread.session.lastError again, wrapping rather than truncating (an error's tail is the useful half, and the tooltip is already the row's overflow surface — same treatment as the branch-mismatch line). Landed as a proper customization: sidebar-v2-error-tooltip manifest entry with intent + a guard that reds if a future sync takes upstream's literal back.

#2 — kept, with a durable record. Upstream's migration-less v1 stash cleanup is deliberate on their side and not worth a permanent storage-layer divergence. The user-facing consequence is now written into .fork/notes/RELEASE-NOTES-PENDING.md so the next fork release's notes carry the "copy your stashes before upgrading" line — a mechanism that survives this PR body, per your earlier point.

#1, #7, #9, #10 — kept, no fork divergence. All four live in apps/server, apps/mobile, or upstream settings logic, where this fork deliberately carries no delta: a fenced counter-fix in any of them buys a conflict on every future sync for behavior upstream owns and may fix. #1 (stale ref cache) and #10 (the gzip ; q=1 OWS trim, genuinely a one-worder) are the two worth reporting to pingdotgg/t3code; #9's opt-out override is upstream's documented beta-rollout choice and only touches Dev/Nightly builds; #7's persistent-failure path is real but mobile-internal. Happy to file the two upstream issues if you want them written up.

@NoahHendrickson
NoahHendrickson merged commit 530b90d into custom Jul 29, 2026
10 checks passed
@NoahHendrickson
NoahHendrickson deleted the claude/sync-upstream-v0.0.30 branch July 29, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants