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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ T3 Code is a minimal web GUI for using coding agents like Codex and Claude.

This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.

This is a fork of [`pingdotgg/t3code`](https://github.com/pingdotgg/t3code). See
[`UPSTREAM_DIVERGENCE.md`](./UPSTREAM_DIVERGENCE.md) for the running log of which upstream
changes we've pulled in, which we've deliberately skipped, and why. Update it whenever you
review or merge upstream commits. Standing policy: **do not pull Android / native-mobile
changes** — we are not investing in the mobile app on this fork.

## Core Priorities

1. Performance first.
Expand Down
81 changes: 81 additions & 0 deletions UPSTREAM_DIVERGENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Upstream Divergence Log

This fork (`DanielGGordon/t3code`, tracked as `origin`) is based on
[`pingdotgg/t3code`](https://github.com/pingdotgg/t3code) (`upstream`). We periodically
review upstream commits and pull in the ones we want. This file is the running record of
**what we pulled, what we skipped, and why** — so future syncs don't re-litigate decisions
already made, and so anyone reading the fork understands how it diverges.

## How to use this log

- When reviewing a batch of upstream commits, add a dated section below.
- Record the upstream review point (last upstream commit considered), what was pulled,
and — just as important — what was **deliberately skipped** and the reasoning.
- Keep decisions durable: if we skip something now but might revisit it, say so explicitly.

## Standing policy

- **Android / native mobile: do not pull.** We are not investing in the mobile app on this
fork. Skip all upstream Android and native-mobile changes (the app scaffolding, native
Kotlin modules, mobile persistence layers, mobile UI polish) unless they are a prerequisite
for a web/server change we actually want. This supersedes any of our own earlier hand-rolled
Android commits — we are not maintaining them going forward either.

---

## 2026-07-12 — Review of upstream through `c1ec1915f`

**Upstream review point:** `c1ec1915f` (2026-07-12) — 15 upstream commits ahead of our last
sync base `dad0889` (2026-07-07).

### Pulled (cherry-picked with `-x`)

- `3201e00ad` — [codex] Preserve worktree metadata during branch sync (#3822). **The priority
pull.** Adds an `expectedBranch` optimistic-concurrency guard so a stale git-status sync can't
regress a freshly-generated branch back to a temporary worktree branch; stops needlessly
rewriting `worktreePath` during branch reconcile. **Conflict in
`apps/web/src/components/GitActionsControl.tsx`:** our fork's `persistThreadBranchSync` calls
`updateThreadMetadata({ input: { threadId, branch, worktreePath } })`, so upstream's switch to
`resolveThreadBranchMetadataPatch` collided. Resolved by keeping our fork's
`updateThreadMetadata` structure but spreading
`resolveThreadBranchMetadataPatch(branch, activeServerThread.branch)` into the input (so we gain
the `expectedBranch` guard) while still writing `worktreePath` back. `decider.ts`,
`contracts/orchestration.ts`, and both test files auto-merged cleanly.
- `619b0ece9` — fix(marketing): platform-appropriate commit shortcut on the website (#3644).
Clean.
- `ef943a26a` — Fix truncated chat error alert layout (#3899). Applied against current main
(which already carries the `<Tooltip>`-wrapped banner), so this landed as the full upstream fix:
the container layout `mx-auto w-fit max-w-[min(48rem,calc(100%-2rem))]` that stops truncation,
keeping the existing Tooltip.

### Skipped — Android / native mobile (per standing policy)

- `c1ec1915f` — Add Android mobile support (#3579). Full official Android port (native Ghostty
terminal, native review-diff view, Android dialogs/menus, embedded fonts). We don't want
Android work.
- `843cf176e` — fix(mobile): embed fonts and render project favicons reliably (#3823). Mobile-only.
- `2250e3ee7` — feat(client): persist offline environment data and mobile preferences (#3795).
Primarily a mobile persistence/preferences layer; touches `client-runtime` but not worth the
merge cost for our web/server focus right now. Revisit only if we want the offline state model.
- `8619ef22e` — Show compact PR number badges in mobile thread rows (#3827). Mobile-only.
- `f61fa9499` — Expose mobile PR indicator labels to accessibility (#3828). Mobile-only.
- `7778a1cea` — Use rounded depth logo for production splash screen (#3780). Mobile splash asset
(`apps/mobile/assets/splash-icon-prod.png`) — our fork deleted it, so it came in as a
modify/delete conflict. Dropped per the no-mobile policy.

### Skipped — depends on newer Codex schema (revisit after a Codex bump)

- `ca1e08b5a` — [codex] Label max and ultra reasoning (#3824). Cherry-picked cleanly but **fails
typecheck** on our fork: upstream types `REASONING_EFFORT_LABELS` as `Record<string, string>`,
whereas our fork tightened it to `Record<V2ModelListResponse__ReasoningEffort, string>`, and our
vendored generated schema (`packages/effect-codex-app-server/.../schema.gen.ts`) only goes up to
`xhigh` — no `max`/`ultra`. Those effort levels don't exist in the Codex app-server version we
vendor, so the labels would be dead code anyway. Dropped; pull when we next regenerate/bump the
Codex schema.

### Skipped — for now (revisit)

- `e9127658a` (#3821) + `e775bc622` (#3785) — Clerk stack upgrade. Deferred; take as a block
when we next touch auth/toolchain so it doesn't drift too far.
- `18a41388e`, `0c6656585`, `03ac1f0cd` — desktop / electron-builder + pnpm-11 asar packaging
fixes. Only relevant if we ship the desktop build; pull alongside the Clerk block.
4 changes: 3 additions & 1 deletion apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ const mobileEndorsementRows = [
<button class="pr-button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.2"/><circle cx="6" cy="18" r="2.2"/><circle cx="18" cy="6" r="2.2"/><path d="M6 8v8M18 8v5a3 3 0 01-3 3h-3M13 13l-2 3 2 3"/></svg>
Commit &amp; push
<span class="pr-button-kbd">⌘⏎</span>
<span id="pr-button-kbd" class="pr-button-kbd">⌘⏎</span>
</button>
</div>
</div>
Expand Down Expand Up @@ -403,12 +403,14 @@ const mobileEndorsementRows = [
const label = document.getElementById("download-label");
const ctaBtn = document.getElementById("cta-download-btn") as HTMLAnchorElement | null;
const ctaLabel = document.getElementById("cta-download-label");
const kbd = document.getElementById("pr-button-kbd");

const platform = detectPlatform();
if (!platform) return;
document.documentElement.dataset.platform = platform.os;
if (label) label.textContent = platform.label;
if (ctaLabel) ctaLabel.textContent = platform.label;
if (kbd && platform.os !== "mac") kbd.textContent = "Ctrl ⏎";

try {
const release = await fetchLatestRelease();
Expand Down
106 changes: 106 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,112 @@ describe("OrchestrationEngine", () => {
await system.dispose();
});

it("does not regress a generated branch to a stale temporary worktree branch", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
const createdAt = now();

await system.run(
engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-branch-race-project-create"),
projectId: asProjectId("project-branch-race"),
title: "Branch Race Project",
workspaceRoot: "/tmp/project-branch-race",
defaultModelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-branch-race-thread-create"),
threadId: ThreadId.make("thread-branch-race"),
projectId: asProjectId("project-branch-race"),
title: "Branch Race Thread",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
branch: "t3code/generated-branch-name",
worktreePath: "/tmp/project-branch-race-worktree",
createdAt,
}),
);

await system.run(
engine.dispatch({
type: "thread.meta.update",
commandId: CommandId.make("cmd-stale-temporary-branch-sync"),
threadId: ThreadId.make("thread-branch-race"),
branch: "t3code/1234abcd",
expectedBranch: "t3code/1234abcd",
}),
);

const snapshot = await system.readModel();
expect(snapshot.threads[0]?.branch).toBe("t3code/generated-branch-name");
await system.dispose();
});

it("allows authoritative worktree bootstrap to assign a temporary branch", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
const createdAt = now();

await system.run(
engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-worktree-bootstrap-project-create"),
projectId: asProjectId("project-worktree-bootstrap"),
title: "Worktree Bootstrap Project",
workspaceRoot: "/tmp/project-worktree-bootstrap",
defaultModelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-worktree-bootstrap-thread-create"),
threadId: ThreadId.make("thread-worktree-bootstrap"),
projectId: asProjectId("project-worktree-bootstrap"),
title: "Worktree Bootstrap Thread",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
branch: "main",
worktreePath: null,
createdAt,
}),
);
await system.run(
engine.dispatch({
type: "thread.meta.update",
commandId: CommandId.make("cmd-authoritative-worktree-bootstrap"),
threadId: ThreadId.make("thread-worktree-bootstrap"),
branch: "t3code/1234abcd",
worktreePath: "/tmp/project-worktree-bootstrap-worktree",
}),
);

const snapshot = await system.readModel();
expect(snapshot.threads[0]?.branch).toBe("t3code/1234abcd");
expect(snapshot.threads[0]?.worktreePath).toBe("/tmp/project-worktree-bootstrap-worktree");
await system.dispose();
});

it("records command ack duration using the first committed event type", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
}

case "thread.meta.update": {
yield* requireThread({
const thread = yield* requireThread({
readModel,
command,
threadId: command.threadId,
});
const branch =
command.branch !== undefined &&
command.expectedBranch !== undefined &&
thread.branch !== command.expectedBranch
? thread.branch
: command.branch;
const occurredAt = yield* nowIso;
return {
...(yield* withEventBase({
Expand All @@ -333,7 +339,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.modelSelection !== undefined
? { modelSelection: command.modelSelection }
: {}),
...(command.branch !== undefined ? { branch: command.branch } : {}),
...(branch !== undefined ? { branch } : {}),
...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),
updatedAt: occurredAt,
},
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
resolveLiveThreadBranchUpdate,
resolveQuickAction,
resolveThreadBranchUpdate,
resolveThreadBranchMetadataPatch,
} from "./GitActionsControl.logic";

function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {
Expand Down Expand Up @@ -1111,6 +1112,18 @@ describe("resolveLiveThreadBranchUpdate", () => {
});
});

describe("resolveThreadBranchMetadataPatch", () => {
it("does not overwrite worktree metadata while reconciling a branch", () => {
assert.deepEqual(
resolveThreadBranchMetadataPatch("feature/current-ref", "feature/previous-ref"),
{
branch: "feature/current-ref",
expectedBranch: "feature/previous-ref",
},
);
});
});

describe("resolveAutoFeatureBranchName", () => {
it("uses semantic preferred ref names when available", () => {
const ref = resolveAutoFeatureBranchName(["main", "feature/other"], "fix toast copy");
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ export function resolveThreadBranchUpdate(
};
}

export function resolveThreadBranchMetadataPatch(
branch: string | null,
expectedBranch: string | null,
): {
branch: string | null;
expectedBranch: string | null;
} {
return { branch, expectedBranch };
}

export function resolveLiveThreadBranchUpdate(input: {
threadBranch: string | null;
gitStatus: VcsStatusResult | null;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
requiresDefaultBranchConfirmation,
resolveDefaultBranchActionDialogCopy,
resolveLiveThreadBranchUpdate,
resolveThreadBranchMetadataPatch,
resolveQuickAction,
resolveThreadBranchUpdate,
} from "./GitActionsControl.logic";
Expand Down Expand Up @@ -1038,7 +1039,9 @@ export default function GitActionsControl({
environmentId: activeThreadRef.environmentId,
input: {
threadId: activeThreadRef.threadId,
branch,
// Carry expectedBranch so a stale git-status sync can't regress a
// freshly-generated branch back to a temporary worktree branch (upstream #3822).
...resolveThreadBranchMetadataPatch(branch, activeServerThread.branch),
worktreePath,
},
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ThreadErrorBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({
}) {
if (!error) return null;
return (
<div className="pt-3 mx-auto max-w-3xl">
<div className="mx-auto w-fit max-w-[min(48rem,calc(100%-2rem))] pt-3">
<Alert variant="error">
<CircleAlertIcon />
<Tooltip>
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ const ThreadMetaUpdateCommand = Schema.Struct({
title: Schema.optional(TrimmedNonEmptyString),
modelSelection: Schema.optional(ModelSelection),
branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
});

Expand Down
Loading