Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
688ee52
fix(release): include pnpm hook in Vercel uploads (#22813)
t3dotgg Oct 21, 2026
1688f96
fix(release): keep Vercel warnings out of deployment URLs (#22817)
t3dotgg Oct 21, 2026
96f09c6
fix(queue): send selected held messages when a thread is stopped (#22…
Yash-Singh1 Oct 21, 2026
066c8fc
fix(server): delegated Claude children follow your subagent cache TTL…
t3dotgg Oct 21, 2026
448b8a9
fix(server): t3 pair in a worktree no longer falls back to the live i…
t3dotgg Oct 21, 2026
370b9ae
fix(server): avoid expensive diff minimization in status refreshes (#…
voltcrash Oct 21, 2026
aacd1ee
fix(codex): keep every path of a multi-file patch (#22541)
only21mil Oct 21, 2026
588150e
fix(gitlab): merge request diffs page 30 files at a time so GitLab 15…
Mnigos Oct 21, 2026
2d40894
fix(server): keep Antigravity question options and no-custom-answer f…
fixfon Oct 21, 2026
1d9060b
fix(server): commit prompts no longer repeat CLAUDE.md when it matche…
t3dotgg Oct 21, 2026
b260a7c
fix(mcp): point a bare thread uuid at its mcp: thread in thread_not_f…
fixfon Oct 21, 2026
2f021ee
fix(opencode): out-of-funds failures show OpenCode's message instead …
fixfon Oct 21, 2026
35e5d62
fix(shared): accept a pasted ChatGPT callback URL that is missing htt…
Project516 Oct 21, 2026
a7c22f1
fix(server): the background service picks up JAVA_HOME from the login…
t3dotgg Oct 21, 2026
e822ba7
fix(server): answered questions say "already answered" instead of "no…
t3dotgg Oct 21, 2026
810e8f6
fix(desktop): an abandoned browser sign-in no longer blocks T3 Connec…
t3dotgg Oct 21, 2026
680152e
fix(server): agents get a short, current model list instead of a 55k-…
t3dotgg Oct 21, 2026
3f24d45
fix(fork): merge upstream 680152e into patch/home
fireboltdude1357 Oct 21, 2026
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
87 changes: 87 additions & 0 deletions .github/scripts/vercel-deployment-output.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const assert = require("node:assert/strict");
const { spawnSync } = require("node:child_process");
const { mkdtempSync, readFileSync, rmSync, existsSync } = require("node:fs");
const { tmpdir } = require("node:os");
const { join } = require("node:path");
const { test } = require("node:test");

const release = readFileSync(join(__dirname, "../workflows/release.yml"), "utf8");
const preview = readFileSync(join(__dirname, "../workflows/web-preview.yml"), "utf8");
const stepPattern =
/ name: (?:Build staged deployment|Deploy preview)\n shell: bash\n run: \|\n((?: .*\n|\n)+)/g;
const steps = [
...Array.from(release.matchAll(stepPattern), (match, index) => [
index === 0 ? "hosted web" : "marketing",
match[1],
]),
...Array.from(preview.matchAll(stepPattern), (match) => ["web preview", match[1]]),
];
assert.equal(steps.length, 3, "Expected both release steps and the preview step");

const url = "https://t3code-fixture-pinglabs.vercel.app";
const warning = 'Error while parsing config file: "/runner/t3code/pnpm-lock.yaml"';

function runStep(script, stdout, exitCode = 0) {
const directory = mkdtempSync(join(tmpdir(), "t3-vercel-output-"));
const outputFile = join(directory, "output");
try {
const result = spawnSync(
"bash",
[
"-c",
'vp() { printf "%s\\n" "$FIXTURE_STDOUT"; return "$FIXTURE_EXIT"; }\n' +
'curl() { printf \'{"id":"fixture-project"}\\n\'; }\n' +
script.replace(/^ /gm, "").replace(/\$\{\{.*?\}\}/g, "fixture"),
],
{
encoding: "utf8",
env: {
PATH: process.env.PATH,
VERCEL_TOKEN: "fixture-token",
VERCEL_ORG_ID: "fixture-org",
VERCEL_PROJECT_ID: "fixture-project",
GITHUB_OUTPUT: outputFile,
FIXTURE_STDOUT: stdout,
FIXTURE_EXIT: String(exitCode),
},
},
);
assert.ifError(result.error);
return {
...result,
output: existsSync(outputFile) ? readFileSync(outputFile, "utf8") : "",
};
} finally {
rmSync(directory, { recursive: true, force: true });
}
}

for (const [name, script] of steps) {
for (const [label, stdout] of [
["plain URL", url],
["lockfile warning", `${warning}\n${url}`],
["trailing diagnostic", `${url}\nDone`],
]) {
test(`${name}: saves one deployment URL after ${label}`, () => {
const result = runStep(script, stdout);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(
result.output.split("\n").filter((line) => !line.startsWith("project_id=")),
[`deployment_url=${url}`, ""],
);
});
}

for (const [label, stdout, exitCode] of [
["missing URL", warning, 0],
["invalid URL", "https://example.invalid/deployment", 0],
["multiple URLs", `${url}\nhttps://other.vercel.app`, 0],
["failed deployment", url, 1],
]) {
test(`${name}: rejects ${label} without saving output`, () => {
const result = runStep(script, stdout, exitCode);
assert.notEqual(result.status, 0);
assert.equal(result.output, "");
});
}
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ jobs:
run: python3 -B .github/scripts/stage-preview-bundle.test.py

- name: Test release checks
run: node --test .github/scripts/check-nightly-release.test.cjs
run: node --test .github/scripts/check-nightly-release.test.cjs .github/scripts/vercel-deployment-output.test.cjs

- name: Finish installing browser secret helper build libraries
timeout-minutes: 5
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ jobs:
fi

echo "Building hosted web app for $channel_name channel."
deployment_url="$(
deployment_output="$(
vp dlx vercel@53.1.1 deploy \
--archive=tgz \
--prod \
Expand All @@ -1091,6 +1091,13 @@ jobs:
--build-env "VITE_HOSTED_APP_URL=$router_url" \
--build-env "VITE_HOSTED_APP_CHANNEL=$channel_name"
)"
printf '%s\n' "$deployment_output"
# Vercel can print lockfile warnings to stdout before the deployment URL.
deployment_url="$(printf '%s\n' "$deployment_output" | grep -E '^https://[[:alnum:].-]+\.vercel\.app$')"
if [[ "$deployment_url" == *$'\n'* ]]; then
echo "Vercel returned multiple deployment URLs." >&2
exit 1
fi
echo "Staged $deployment_url."
echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -1219,7 +1226,7 @@ jobs:
)"
export VERCEL_PROJECT_ID

deployment_url="$(
deployment_output="$(
vp dlx vercel@53.1.1 deploy \
--archive=tgz \
--prod \
Expand All @@ -1228,6 +1235,13 @@ jobs:
--token "$VERCEL_TOKEN" \
--scope "${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}"
)"
printf '%s\n' "$deployment_output"
# Vercel can print lockfile warnings to stdout before the deployment URL.
deployment_url="$(printf '%s\n' "$deployment_output" | grep -E '^https://[[:alnum:].-]+\.vercel\.app$')"
if [[ "$deployment_url" == *$'\n'* ]]; then
echo "Vercel returned multiple deployment URLs." >&2
exit 1
fi
echo "Staged $deployment_url."
echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT"
echo "project_id=$VERCEL_PROJECT_ID" >> "$GITHUB_OUTPUT"
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/web-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,20 @@ jobs:
vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}"
deployment_url="$(
deployment_output="$(
vp dlx vercel@53.1.1 deploy \
--archive=tgz \
--yes \
--token "$VERCEL_TOKEN" \
--scope "$vercel_scope"
)"
printf '%s\n' "$deployment_output"
# Vercel can print lockfile warnings to stdout before the deployment URL.
deployment_url="$(printf '%s\n' "$deployment_output" | grep -E '^https://[[:alnum:].-]+\.vercel\.app$')"
if [[ "$deployment_url" == *$'\n'* ]]; then
echo "Vercel returned multiple deployment URLs." >&2
exit 1
fi
echo "Deployed $deployment_url"
echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT"
Expand Down
2 changes: 2 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Vercel's default .pnp* rule also excludes pnpm's hook, whose checksum is in the lockfile.
!/.pnpmfile.cjs
58 changes: 58 additions & 0 deletions apps/desktop/src/clerkOAuthCancel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { assert, describe, it } from "@effect/vitest";

import { createClerkOAuthCancel } from "./clerkOAuthCancel.ts";

const deferredOpen = () => {
const resolvers: Array<(value: { callbackUrl: string }) => void> = [];
const opened: Array<Promise<{ callbackUrl: string }>> = [];
const open = (_url: string) => {
const promise = new Promise<{ callbackUrl: string }>((resolve) => {
resolvers.push(resolve);
});
opened.push(promise);
return promise;
};
return { open, resolvers, opened };
};

const outcome = (promise: Promise<{ callbackUrl: string }>) =>
promise.then(
({ callbackUrl }) => `resolved ${callbackUrl}`,
(error: Error) => `rejected ${error.message}`,
);

describe("createClerkOAuthCancel", () => {
it("cancel rejects the pending flow and reports it is no longer pending", async () => {
const oauth = createClerkOAuthCancel();
const changes: boolean[] = [];
oauth.onPendingChange((pending) => changes.push(pending));
const { open } = deferredOpen();

const flow = outcome(oauth.wrapOpen(open)("https://accounts.example/a"));
assert.isTrue(oauth.isPending());
oauth.cancel();

assert.strictEqual(await flow, "rejected Clerk: OAuth flow was cancelled.");
assert.isFalse(oauth.isPending());
assert.deepStrictEqual(changes, [true, false]);
});

it("a new attempt replaces the pending one, and the old result cannot clear it", async () => {
const oauth = createClerkOAuthCancel();
const { open, resolvers, opened } = deferredOpen();
const wrapped = oauth.wrapOpen(open);

const first = outcome(wrapped("https://accounts.example/a"));
const second = outcome(wrapped("https://accounts.example/b"));
assert.strictEqual(await first, "rejected Clerk: OAuth flow was cancelled.");

resolvers[0]?.({ callbackUrl: "t3code://app/?a" });
// Two hops, so the wrapper's own `.then().finally()` on this result has run.
await opened[0]?.then(() => undefined).then(() => undefined);
assert.isTrue(oauth.isPending());

resolvers[1]?.({ callbackUrl: "t3code://app/?b" });
assert.strictEqual(await second, "resolved t3code://app/?b");
assert.isFalse(oauth.isPending());
});
});
46 changes: 46 additions & 0 deletions apps/desktop/src/clerkOAuthCancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
type OAuthOpen = (url: string) => Promise<{ callbackUrl: string }>;

/**
* Lets the app cancel the Clerk OAuth flow the renderer is waiting on. The
* system browser never reports a closed tab, so an abandoned sign-in would
* otherwise keep Clerk's sheet spinning until Clerk's 180s timeout.
*
* Pass `wrapOpen` to `exposeClerkBridge` as `wrapOAuthOpen`. Cancelling only
* rejects the renderer's promise; the patched main process replaces the stale
* flow when the next attempt starts.
*/
export function createClerkOAuthCancel() {
let cancelPending: (() => void) | null = null;
const listeners = new Set<(pending: boolean) => void>();
const setCancelPending = (cancel: (() => void) | null) => {
cancelPending = cancel;
for (const listener of listeners) listener(cancel !== null);
};

return {
wrapOpen:
(open: OAuthOpen): OAuthOpen =>
(url) => {
cancelPending?.();
return new Promise((resolve, reject) => {
const settle = () => {
if (cancelPending === cancel) setCancelPending(null);
};
const cancel = () => {
settle();
reject(new Error("Clerk: OAuth flow was cancelled."));
};
setCancelPending(cancel);
open(url).then(resolve, reject).finally(settle);
});
},
isPending: () => cancelPending !== null,
onPendingChange: (listener: (pending: boolean) => void) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
cancel: () => cancelPending?.(),
};
}
7 changes: 6 additions & 1 deletion apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { exposeClerkBridge } from "@clerk/electron/preload";
import { contextBridge, ipcRenderer, webFrame, webUtils } from "electron";

import { createClerkOAuthCancel } from "./clerkOAuthCancel.ts";
import * as IpcChannels from "./ipc/channels.ts";
import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts";

Expand All @@ -30,7 +31,8 @@ function isSnapShotEvent(value: unknown): value is DesktopSnapShotEvent {
);
}

exposeClerkBridge({ passkeys: true });
const clerkOAuthCancel = createClerkOAuthCancel();
exposeClerkBridge({ passkeys: true, wrapOAuthOpen: clerkOAuthCancel.wrapOpen });

// Runs before any app script reads localStorage. See DesktopLegacyLocalStorage.
try {
Expand Down Expand Up @@ -253,6 +255,9 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.removeListener(IpcChannels.QUIT_SHORTCUT_CHANNEL, wrappedListener);
};
},
isClerkOAuthPending: clerkOAuthCancel.isPending,
onClerkOAuthPendingChange: clerkOAuthCancel.onPendingChange,
cancelClerkOAuth: clerkOAuthCancel.cancel,
getWindowFullscreenState: () =>
ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true,
getWindowZoomFactor: () => webFrame.getZoomFactor(),
Expand Down
21 changes: 14 additions & 7 deletions apps/mobile/src/features/threads/ThreadQueueControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export function ThreadQueueSheet({ route }: StaticScreenProps<QueueTarget>) {
void Haptics.selectionAsync();
if (action === "remove") {
await cancel(buildCancelQueuedRunCommand({ ...target, runId }));
} else if (workflow?.canSendNow) {
await resume({ ...target, input: { threadId: target.threadId, runId } });
} else if (workflow?.activeRun && workflow.canPromoteToSteer) {
await promote({
...target,
Expand Down Expand Up @@ -221,6 +223,7 @@ export function ThreadQueueSheet({ route }: StaticScreenProps<QueueTarget>) {
const controls = resolveThreadQueueRowControls({
busy: busyRunId !== null || draggedRunId !== null,
canPromoteToSteer: workflow?.canPromoteToSteer ?? false,
canSendNow: workflow?.canSendNow ?? false,
canReorder: workflow?.canReorder ?? false,
index,
isEditing: editing?.runId === run.id,
Expand Down Expand Up @@ -326,12 +329,12 @@ export function ThreadQueueSheet({ route }: StaticScreenProps<QueueTarget>) {
accessibilityLabel={`Actions for queued message ${index + 1}`}
shouldOpenOnLongPress
actions={[
...(workflow?.canPromoteToSteer
...(workflow?.canPromoteToSteer || workflow?.canSendNow
? [
{
id: "steer",
title: "Steer now",
attributes: { disabled: !controls.canSteer },
title: workflow?.canSendNow ? "Send now" : "Steer now",
attributes: { disabled: !controls.canSend },
image: Platform.OS === "ios" ? "arrow.turn.left.up" : "arrow_upward",
},
]
Expand Down Expand Up @@ -385,16 +388,20 @@ export function ThreadQueueSheet({ route }: StaticScreenProps<QueueTarget>) {
Editing
</Text>
) : null}
{workflow?.canPromoteToSteer ? (
{workflow?.canPromoteToSteer || workflow?.canSendNow ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Steer with message ${index + 1} now`}
disabled={!controls.canSteer}
accessibilityLabel={
workflow?.canSendNow
? `Send message ${index + 1} now`
: `Steer with message ${index + 1} now`
}
disabled={!controls.canSend}
onPress={() => void act(run.id, "steer")}
className="h-8 shrink-0 justify-center rounded-full bg-primary px-3 active:opacity-70 disabled:opacity-40"
>
<Text className="font-t3-medium text-xs text-primary-foreground">
Steer
{workflow?.canSendNow ? "Send now" : "Steer"}
</Text>
</Pressable>
) : null}
Expand Down
Loading