Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
96a330c
fix(web): remote delegates show their harness icon, a parent row, and…
t3dotgg Oct 28, 2026
1abf368
fix(build): builds no longer fail when GitHub's CDN times out on lice…
t3dotgg Oct 28, 2026
0160c44
feat(server): agents list snoozed threads across all projects in one …
t3dotgg Oct 28, 2026
c280e64
fix(server): searching delegate models by provider name no longer sho…
t3dotgg Oct 28, 2026
eb751fa
fix(web): chat tables open at the right size instead of collapsed and…
t3dotgg Oct 28, 2026
6bc87b9
ci(release): desktop builds stop hitting the npm registry for staged …
t3dotgg Oct 28, 2026
5211acf
feat(web): boring sidebar can show folder threads on two lines (#26463)
t3dotgg Oct 28, 2026
125b619
fix(server): release completed pull request sweep data (#25177)
juliusmarminge Oct 28, 2026
d5f1d0a
test(evals): device manager cases for mcp-tool-use (#25850)
gabrielelpidio Oct 28, 2026
d07c0a7
fix(server): agents drive simulators through the device tools instead…
gabrielelpidio Oct 28, 2026
30eb7e7
fix(mobile): overlay sidebar header blurs content scrolling under it …
juliusmarminge Oct 28, 2026
fec83a1
fix(web): numbered composer lists nest and lift correctly (#17138)
michal-billtech Oct 28, 2026
612a8e5
feat(mobile): add dynamic workflows to the Agents sheet (#18642)
Bil0000 Oct 28, 2026
237498c
fix(ci): Android merges stop cutting a new Play Store build every tim…
juliusmarminge Oct 28, 2026
886d8f6
fix(mobile): dictation confirm button no longer hugs the small compos…
juliusmarminge Oct 28, 2026
be6ff17
feat(devices): lease simulators and emulators from a Simlock pool (al…
juliusmarminge Oct 28, 2026
ba18706
fix(mobile): use a native split button for Android merges (#26373)
PixPMusic Oct 28, 2026
9e09926
docs: settings paths name the Environments and Sharing & access pages…
juliusmarminge Oct 28, 2026
9826dbb
docs(agents): agents verify in a browser or simulator without asking …
juliusmarminge Oct 28, 2026
5b89a53
feat(pull-requests): manage GitHub maintainer edits (#25588)
Bil0000 Oct 28, 2026
d464631
fix(web): image chips drop the pill and match other chips (#26379)
flamboh Oct 28, 2026
e34067e
fix(mobile): align settled and active thread rows (#26622)
PixPMusic Oct 28, 2026
e677e97
fix(server): publish local git status before remote lookup (#21046)
JustYannicc Oct 29, 2026
26caea2
fix(server): outside MCP clients get thread links they can open (#26642)
t3dotgg Oct 29, 2026
725573f
fix(web): double-click selects the whole inline code span (#26639)
t3dotgg Oct 29, 2026
b287102
fix(acp): ID-only command approvals keep the tracked command and gran…
juliusmarminge Oct 29, 2026
37a9ca0
fix(cursor): subagent ls, lint and MCP results no longer fail the par…
juliusmarminge Oct 29, 2026
29f33af
perf(web): long-thread timeline rebuilds 45 → 9 ms, tool updates 12 →…
juliusmarminge Oct 29, 2026
a42bdd4
fix(mobile): a thread opened while reconnecting no longer turns into …
shivamhwp Oct 29, 2026
221610f
fix(web): keep settled drop hint inside the sidebar viewport (#26736)
Yash-Singh1 Oct 29, 2026
be7d86f
feat(devices): Simlock setup uses each machine's agent-device token, …
juliusmarminge Oct 29, 2026
a5448f4
fix(web): pull request diff follows the file tree, with a hide-tests …
juliusmarminge Oct 29, 2026
8ff9ae1
fix(mobile): delivered messages no longer reappear at the bottom of l…
juliusmarminge Oct 29, 2026
d8f4980
ci(mobile): fork PRs now get the Native Change label (#26755)
juliusmarminge Oct 29, 2026
5eebf77
fix(claude): command approvals show the command, not Claude's descrip…
maria-rcks Oct 29, 2026
7b87466
docs: add nixpkgs installation instructions (#26750)
0x4bs3nt Oct 29, 2026
861a51a
feat(server,web,mobile): fall back when the default provider is out o…
maria-rcks Oct 29, 2026
783581d
fix(ci): fork PRs now resolve in workflow_run PR lookups (#26779)
juliusmarminge Oct 29, 2026
8298664
fix(server,cursor,opencode): refresh provider usage when a turn hits …
maria-rcks Oct 29, 2026
fe219c4
fix(web): keep tool groups collapsed after a usage-limit pause (#24937)
shivamhwp Oct 29, 2026
7632c3d
fix(mobile): photo-library PNGs no longer fail after resizing (#26522)
shivamhwp Oct 29, 2026
ab1e221
fix(server): guest links no longer return file diffs (#26538)
nexxeln Oct 29, 2026
eb565e1
fix(claude): Open diff shows Claude edits as a diff instead of raw JS…
nexxeln Oct 29, 2026
dac8b11
chore: vouch nexxeln (#26812)
t3dotgg Oct 29, 2026
f855f01
chore(release): prepare v0.1.4
t3-code-bot[bot] Oct 29, 2026
310c128
feat(web): Labs toggle keeps 5h, weekly, and context limits in view (…
t3dotgg Oct 29, 2026
6d82a22
fix(desktop): bump Electron to 44.10.0 for the Node.js Brotli fix (CV…
nabettu Oct 29, 2026
da1eab0
chore: make agent mistakes fail early (#26167)
t3dotgg Oct 29, 2026
ccaa3ad
feat(marketing): hero shows four agents running at once (#26875)
t3dotgg Oct 29, 2026
35b298a
fix(web): undo notice moves to the sidebar footer so it never covers …
t3dotgg Oct 29, 2026
58e60bf
feat: turn a No project thread into a project (#26767)
t3dotgg Oct 29, 2026
4432fbe
feat(antigravity): Claude and GPT models, thinking levels, and diff e…
t3dotgg Oct 29, 2026
d7935f1
feat(web): choose what the thread details card shows (#26904)
t3dotgg Oct 29, 2026
f2943ee
feat(updates): switch your whole fleet between Stable and Nightly in …
t3dotgg Oct 29, 2026
c1876b5
fix(antigravity): file reads no longer show unrelated errors (#26910)
t3dotgg Oct 29, 2026
b5fc7be
feat(release): release notes and the update popover open with a short…
t3dotgg Oct 29, 2026
48247dd
fix(cli): t3 starts on a fresh Ubuntu server without libatomic (#26912)
t3dotgg Oct 29, 2026
e6ca170
fix(web): update everything no longer waits on npm for service hosts …
t3dotgg Oct 29, 2026
3748c5c
test(web): pin the npm wait floor for update everything (#26921)
t3dotgg Oct 29, 2026
7f55281
fix(antigravity): served reads stop spinning and duplicate model name…
t3dotgg Oct 29, 2026
9acb769
fix(server): queue Copilot follow-ups without killing background agen…
shivamhwp Oct 29, 2026
4453f53
fix(web): Antigravity effort no longer shows a speedometer in the com…
t3dotgg Oct 29, 2026
6d450a4
fix(release): Linux CLI archives build on runners without package doc…
t3dotgg Oct 29, 2026
2ba9ef7
Merge upstream/main 6d450a4284 into coder-only
henrychu04 Oct 29, 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ pnpm build
```

After dependencies are installed, license-notice generation uses the checked-in SPDX texts in
`licenses/spdx`; builds do not need access to `raw.githubusercontent.com`. Maintainers adding a new
license can run `pnpm licenses:sync` with network access and commit the downloaded source text.
`third-party-licenses/spdx`; builds do not need access to `raw.githubusercontent.com`. Maintainers
adding a new license can run `pnpm licenses:sync` with network access and commit the downloaded
source text.

Contributor and agent rules live in [AGENTS.md](./AGENTS.md).
3 changes: 2 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "t3",
"version": "0.1.3",
"version": "0.1.4",
"license": "MIT",
"type": "module",
"scripts": {
Expand All @@ -19,6 +19,7 @@
"@t3tools/source-control-definitions": "workspace:*",
"@t3tools/source-control-gitlab": "workspace:*",
"character-entities": "2.0.2",
"diff": "8.0.3",
"effect": "catalog:",
"node-pty": "1.2.0-beta.15",
"saxes": "6.0.0",
Expand Down
16 changes: 16 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const omitInitializeModelState = process.env.T3_ACP_OMIT_INITIALIZE_MODEL_STATE
const completeFirstPromptOnCancel = process.env.T3_ACP_COMPLETE_FIRST_PROMPT_ON_CANCEL === "1";
const floodStderr = process.env.T3_ACP_FLOOD_STDERR === "1";
const hangPromptForever = process.env.T3_ACP_HANG_PROMPT_FOREVER === "1";
const emitTrackedToolCallKindThenHang = process.env.T3_ACP_EMIT_TRACKED_TOOL_CALL_KIND_THEN_HANG;
const clientFsProbePath = process.env.T3_ACP_CLIENT_FS_PROBE_PATH;
const clientFsProbeLogPath = process.env.T3_ACP_CLIENT_FS_PROBE_LOG_PATH;
const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
Expand Down Expand Up @@ -1294,6 +1295,21 @@ const program = Effect.gen(function* () {
return yield* AcpError.AcpRequestError.internalError("Mock prompt failure");
}

if (emitTrackedToolCallKindThenHang !== undefined) {
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "tracked-tool-call",
title: "git status",
kind: emitTrackedToolCallKindThenHang as AcpSchema.ToolKind,
status: "pending",
rawInput: { command: "git status" },
},
});
return yield* Effect.never;
}

if (hangPromptForever || (emitEmptySuccessfulBashThenHang && promptCount === 2)) {
return yield* Effect.never;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/coderEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const CODER_ENVIRONMENT_CAPABILITIES = {
threadStop: true,
environmentIcon: true,
projectCloneTracking: true,
threadProjectFromScratch: true,
// Links are the gateway's workspace link rules; calls between workspaces go through its hub.
peerLinks: true,
linkFence: true,
Expand All @@ -89,6 +90,7 @@ export const CODER_OMITTED_CAPABILITIES = {
serverUpdateThreadContinuation: "No server self-update.",
agentActivityPublishing: "No relay, push notifications, or Live Activities.",
desktopAppUpdate: "No desktop app.",
desktopAppChannelSwitch: "No desktop app.",
serverBrowser: "No browser preview.",
databaseStorage: "No `t3 doctor` in the workspace helper; it compacts and reclaims space itself.",
ignoredFileListing:
Expand Down
115 changes: 115 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
import { SourceControlProviderKind } from "@t3tools/contracts";
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFS from "node:fs";
Expand Down Expand Up @@ -69,6 +71,10 @@ type FakeChangeRequest = Omit<ChangeRequest, "provider" | "state" | "updatedAt">
};

interface FakeProviderScenario {
prListGate?: {
readonly started: Deferred.Deferred<void>;
readonly release: Deferred.Deferred<void>;
};
/** The kind the fake host reports itself as. */
kind?: string;
/** Answers to successive change request listings, whatever their head selector. */
Expand Down Expand Up @@ -454,6 +460,13 @@ function createFakeSourceControlProvider(scenario: FakeProviderScenario = {}): {
[]
).map(toChangeRequest),
),
Effect.tap(() =>
scenario.prListGate
? Deferred.succeed(scenario.prListGate.started, undefined).pipe(
Effect.andThen(Deferred.await(scenario.prListGate.release)),
)
: Effect.void,
),
),
createChangeRequest: (input) =>
record(input.cwd, {
Expand Down Expand Up @@ -725,6 +738,108 @@ const layerGitManagerTest = GitVcsDriver.layer.pipe(
);

it.layer(layerGitManagerTest)("GitManager", (it) => {
it.effect(
"publishes local Git changes during a held PR lookup without caching its old branch",
() =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-refresh-status-gh-gate-");
yield* initRepo(repoDir);
yield* runGit(repoDir, ["checkout", "-b", "release"]);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "release"]);
yield* runGit(repoDir, ["tag", "release"]);
const oldPr = {
number: 613,
title: "Release PR",
url: "https://github.com/pingdotgg/codething-mvp/pull/613",
baseRefName: "main",
headRefName: "release",
};
const newPr = { ...oldPr, number: 614, headRefName: "heads/release" };
const providerScenario: FakeProviderScenario = {
prListSequence: [oldPr, oldPr, newPr].map((pr) => [pr]),
};
const { manager } = yield* makeManager({ providerScenario });
const context = yield* Layer.build(
VcsStatusBroadcaster.layer.pipe(
Layer.provide(
GitWorkflowService.layer.pipe(
Layer.provide(Layer.succeed(GitManager.GitManager, manager)),
Layer.provide(
VcsDriverRegistry.layer.pipe(
Layer.provide(VcsProjectConfig.layer),
Layer.provide(VcsProcess.layer),
),
),
),
),
Layer.provide(
Layer.mock(BackgroundPolicy.BackgroundPolicy)({
shouldRunScopeWork: () => Effect.succeed(true),
}),
),
),
);
const broadcaster = Context.get(context, VcsStatusBroadcaster.VcsStatusBroadcaster);
const initial = yield* broadcaster.getStatus({ cwd: repoDir });
expect(initial.refName).toBe("release");
expect(initial.pr?.number).toBe(613);

const subscribed = yield* Deferred.make<void>();
const published = yield* Deferred.make<void>();
yield* broadcaster.streamStatus({ cwd: repoDir, includeRemote: false }).pipe(
Stream.runForEach((event) => {
if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined);
if (event._tag === "localUpdated" && event.local.hasWorkingTreeChanges) {
return Deferred.succeed(published, undefined);
}
return Effect.void;
}),
Effect.forkScoped,
);
yield* Deferred.await(subscribed);
const gate = {
started: yield* Deferred.make<void>(),
release: yield* Deferred.make<void>(),
};
providerScenario.prListGate = gate;
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString(NodePath.join(repoDir, "README.md"), "changed before refresh\n");
const completed = yield* Deferred.make<void>();
const refresh = yield* broadcaster
.refreshStatus(repoDir)
.pipe(Effect.ensuring(Deferred.succeed(completed, undefined)), Effect.forkScoped);
yield* Deferred.await(gate.started);
const pending = Option.getOrThrow(
yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })),
);
expect(pending._tag).toBe("snapshot");
if (pending._tag === "snapshot") expect(pending.local.hasWorkingTreeChanges).toBe(true);
yield* Deferred.await(published);
expect(yield* Deferred.isDone(completed)).toBe(false);

yield* runGit(repoDir, ["checkout", "-b", "heads/release"]);
yield* runGit(repoDir, ["push", "-u", "origin", "refs/heads/heads/release"]);
yield* Deferred.succeed(gate.release, undefined);
const refreshed = yield* Fiber.join(refresh);
expect(refreshed.refName).toBe("heads/release");
expect(refreshed.hasWorkingTreeChanges).toBe(true);
expect(refreshed.pr).toBeNull();
const snapshot = Option.getOrThrow(
yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })),
);
expect(snapshot._tag).toBe("snapshot");
if (snapshot._tag === "snapshot") expect(snapshot.remote).toBeNull();

delete providerScenario.prListGate;
const recovered = yield* broadcaster.getStatus({ cwd: repoDir });
expect(recovered.refName).toBe("heads/release");
expect(recovered.pr?.number).toBe(614);
expect((yield* broadcaster.getStatus({ cwd: repoDir })).pr?.number).toBe(614);
}),
);

it.effect("status reports the listed PR whose head is the branch", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class GitWorkflowService extends Context.Service<
readonly localStatus: (
input: VcsStatusInput,
) => Effect.Effect<VcsStatusLocalResult, GitManagerServiceError>;
readonly currentBranch: (cwd: string) => Effect.Effect<string | null, GitManagerServiceError>;
readonly remoteStatus: (
input: VcsStatusInput,
options?: GitManager.GitRemoteStatusOptions,
Expand Down Expand Up @@ -340,6 +341,32 @@ export const make = Effect.gen(function* () {
),
);
}),
currentBranch: Effect.fn("GitWorkflowService.currentBranch")(function* (cwd) {
if (!(yield* detectGitRepositoryForStatus("GitWorkflowService.currentBranch", cwd))) {
return null;
}
const result = yield* git.execute({
operation: "GitWorkflowService.currentBranch",
cwd,
args: ["symbolic-ref", "--quiet", "HEAD"],
allowNonZeroExit: true,
timeoutMs: 5_000,
});
if (result.exitCode === 0) {
return result.stdout.trim().replace(/^refs\/heads\//, "") || null;
}
if (result.exitCode === 1) return null;
return yield* new GitCommandError({
operation: "GitWorkflowService.currentBranch",
command: "git symbolic-ref",
cwd,
argumentCount: 3,
...(result.exitCode === null ? {} : { exitCode: result.exitCode }),
stdoutLength: result.stdout.length,
stderrLength: result.stderr.length,
detail: "Failed to read the checked-out branch.",
});
}),
remoteStatus: Effect.fn("GitWorkflowService.remoteStatus")(function (input, options) {
return detectGitRepositoryForStatus("GitWorkflowService.remoteStatus", input.cwd).pipe(
Effect.flatMap((isGitRepository) =>
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/mcp/McpInvocationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import { formatThreadLink } from "@t3tools/shared/threadLinks";

const ALL_MCP_CAPABILITIES = [
"preview",
Expand Down Expand Up @@ -47,6 +48,8 @@ export interface McpClientCaller {
* `clientModeCeiling`).
*/
readonly narrowedTo?: ClientModes | undefined;
/** The origin an outside client reached; its thread links open the web app there. */
readonly webOrigin?: string | undefined;
}

export interface ClientModes {
Expand Down Expand Up @@ -100,6 +103,20 @@ export interface McpInvocationScope {
readonly linkedEnvironments?: boolean;
}

/**
* T3 Code renders `t3-thread://` links as thread chips. Other MCP clients cannot open them, so
* they get a web URL on the origin they reached instead.
*/
export const threadLinkFor = (
scope: Pick<McpInvocationScope, "environmentId" | "client">,
threadId: ThreadId,
): string => {
const origin = scope.client?.webOrigin;
return origin === undefined
? formatThreadLink({ environmentId: scope.environmentId, threadId })
: `${origin}/${encodeURIComponent(scope.environmentId)}/${encodeURIComponent(threadId)}`;
};

export class McpInvocationContext extends Context.Service<
McpInvocationContext,
McpInvocationScope
Expand Down
75 changes: 75 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,16 @@ describe("OrchestratorMcpService provider resolution", () => {
models: ["oc/m12", "oc/m01", "oc/m03", "oc/m04", "oc/m05", "oc/m06", "oc/m07", "oc/m08"],
omitted: 3,
});
// A word naming the provider matches all its models, so legacy ones stay out.
assert.deepEqual(listing(yield* capabilitiesWith("Codex")), [
{ id: codexInstanceId, models: ["gpt-6", "gpt-4", "gpt-3", "my-alpha"], omitted: 1 },
{ id: opencodeInstanceId, models: [], omitted: 0 },
]);
assert.deepEqual(listing(yield* capabilitiesWith("codex gpt 5"))[0], {
id: codexInstanceId,
models: ["gpt-5"],
omitted: 0,
});
}),
);

Expand Down Expand Up @@ -2695,6 +2705,71 @@ describe("OrchestratorMcpService thread lookup", () => {
);
});

describe("OrchestratorMcpService thread list", () => {
const client: McpInvocationScope = {
environmentId: EnvironmentId.make("environment:thread-list"),
requestNamespace: "client:thread-list",
thread: undefined,
client: { sessionId: "thread-list", label: "Claude Code", access: "full-access" },
capabilities: new Set(["orchestration"]),
issuedAt: 1,
};
const shell = (id: string, projectId: string, snoozed: boolean) => ({
...liveThreadShell(ThreadId.make(id)),
projectId: ProjectId.make(projectId),
status: "completed" as const,
snoozedUntil: snoozed ? DateTime.makeUnsafe("2099-01-01T00:00:00Z") : null,
});
const shells = [
shell("thread:a-snoozed", "project:a", true),
shell("thread:a-awake", "project:a", false),
shell("thread:b-snoozed", "project:b", true),
];
const service = layerOrchestratorService.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
listProjectThreads: (input) =>
Effect.succeed(
shells.filter(
(thread) => input.projectId === undefined || thread.projectId === input.projectId,
),
),
}),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
list: () => Effect.succeed([]),
}),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
),
),
);

it.effect("lists snoozed threads across every project with allProjects", () =>
Effect.gen(function* () {
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService;
const listed = yield* mcp.listThreads(client, {
allProjects: true,
projectId: ProjectId.make("project:a"),
snoozed: true,
});
assert.equal(listed.projectId, null);
assert.deepStrictEqual(
listed.threads.map((thread) => [thread.threadId, thread.projectId]),
[
["thread:a-snoozed", "project:a"],
["thread:b-snoozed", "project:b"],
],
);
}).pipe(Effect.provide(service)),
);
});

describe("OrchestratorMcpService thread stop", () => {
const driver = ProviderDriverKind.make("codex");
const instanceId = ProviderInstanceId.make("codex");
Expand Down
Loading