Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
10395bc
fix(server): status refresh no longer pegs CPU in repos with thousand…
t3dotgg Oct 7, 2026
c93080b
perf(server): background branch lookups share one GitHub query per sw…
t3dotgg Oct 7, 2026
cd41c4a
fix(server): threads settle as soon as a client sees their PR merge (…
t3dotgg Oct 7, 2026
611132c
feat(server,web,mobile): agents see snooze state and link to threads …
t3dotgg Oct 7, 2026
521f5b5
fix(web): replace Lineage timers with Stop on hover (#16791)
Bil0000 Oct 7, 2026
6ecb82d
fix(clients): running subagent cards stay visible after their parent …
juliusmarminge Oct 7, 2026
4d976d1
feat: MCP apps render and run inline in threads (#16236)
juliusmarminge Oct 7, 2026
a8c4802
fix(server): a background Claude subagent's work shows while its pare…
Vantrongs Oct 7, 2026
8d564a3
fix(mobile): hide threads from switched-off environments (#16886)
entity Oct 7, 2026
87c366b
fix(server): a refused Claude turn no longer throws away its session …
SunkenInTime Oct 7, 2026
db46c42
fix(web): onboarding Continue no longer locks on computers that won't…
juliusmarminge Oct 7, 2026
d9068d3
fix(server): worktree cleanup no longer deletes files hidden by showU…
SunkenInTime Oct 7, 2026
93e8640
fix(server): merged-worktree cleanup removes worktrees after squash m…
tris203 Oct 7, 2026
e0201d3
fix(server): free worktrees for terminal thread statuses (#15150)
ANSHSINGH050404 Oct 7, 2026
4ce6a51
fix(server): Windows worktrees with long paths no longer fail or stra…
That1Drifter Oct 7, 2026
41985b4
fix(server): main typechecks again after a test used renamed helpers …
juliusmarminge Oct 7, 2026
f570bd2
fix(server): Claude prompts no longer hang on a uuid the session alre…
juliusmarminge Oct 7, 2026
588d32e
feat(models): add Claude Haiku 5.5 and retire Sonnet 5 and Opus 5 to …
juliusmarminge Oct 7, 2026
ba0ea3d
fix(web): iPhone Duo folds animate, center on the hinge, and keep the…
gabrielelpidio Oct 7, 2026
9f48092
fix(server): Claude 5-series models always run 1M context (#16908)
juliusmarminge Oct 7, 2026
73e87f6
fix(desktop): setup prompts name a t3 that runs on desktop installs (…
juliusmarminge Oct 7, 2026
0678e4e
feat(desktop): install the t3 command from Settings (#16683)
juliusmarminge Oct 7, 2026
a9fb6a8
fix(web): show live names in thread-read activity (#13140)
Bil0000 Oct 7, 2026
468ade3
feat(mobile): adopt v5 navigation and native iPad columns (#16733)
juliusmarminge Oct 7, 2026
8a92ebe
fix(mobile): Android composer picker scrolls past the first four rows…
shivamhwp Oct 7, 2026
5383296
fix(desktop): sign-in and captchas work again in desktop browser tabs…
juliusmarminge Oct 7, 2026
8548961
fix(server): missing project folders no longer log favicon warnings (…
yordis Oct 7, 2026
eba0521
fix(server): unload Codex threads left idle on the shared app-server …
RhysSullivan Oct 7, 2026
26285ab
fix(web): fast typing no longer scrambles text when type-to-focus kic…
otavio Oct 7, 2026
7f3628e
fix(web): simplify workspace card rows (#16823)
Bil0000 Oct 7, 2026
7ac5ec6
fix(web): Copy MCP URL shows up for environments reached over plain h…
SunkenInTime Oct 7, 2026
abbf5d5
fix(web): C#, Java, PHP and 11 other languages get file icons (#16974)
juliusmarminge Oct 7, 2026
95ccbf4
feat(clients): live row shows the agent's latest thought (#16284)
t3dotgg Oct 7, 2026
300f7f9
feat(web): block-level Markdown in the rich text composer (#14677)
chrisdeeming Oct 7, 2026
46dd094
fix(web): center project monograms in settled rows (#16841)
Aforno Oct 7, 2026
3143335
fix(web): cancelling a new citation no longer leaves a stray space (#…
Aforno Oct 7, 2026
ff3a04a
fix(settings): provider updates show live progress instead of a bare …
shivamhwp Oct 8, 2026
14fe015
feat(web): find in diffs with Cmd+F (#14623)
juliusmarminge Oct 8, 2026
08b1222
refactor(server): GitHub services are named for the API they call, no…
juliusmarminge Oct 8, 2026
a21ac5f
refactor(server): GitHub GraphQL batches use variables and share one …
juliusmarminge Oct 8, 2026
42d92ec
refactor(server): GitHub source control reads GitHubApi directly (#16…
juliusmarminge Oct 8, 2026
d720210
refactor(server): GitHub rate limits read the response headers (#16986)
juliusmarminge Oct 8, 2026
83a82a4
chore(review): remove the custom Approvability check (#17018)
esthor Oct 8, 2026
f9e16b3
perf(mobile): show the cached thread list sooner and stop the freeze …
juliusmarminge Oct 8, 2026
9fba209
fix(server): replaying a command no longer freezes the server (#17041)
SunkenInTime Oct 8, 2026
04ad174
fix(server): opening a long thread no longer blocks the server for a …
SunkenInTime Oct 8, 2026
0f09627
fix(server,web,mobile): thread links reference the thread id, not a b…
juliusmarminge Oct 8, 2026
6f83358
fix(web): messages with quotes or links no longer collapse when short…
flamboh Oct 8, 2026
843c593
refactor(web): PR loading skeleton shares the detail panel's layout (…
flamboh Oct 8, 2026
2b7fa21
fix(web): PR file headers keep the file name in narrow panels (#15562)
flamboh Oct 8, 2026
b24f0fb
fix(web): PR timeline no longer shifts when its scrollbar appears (#1…
flamboh Oct 8, 2026
0bfd9dd
fix(server): held queued wakes no longer keep delegated tasks running…
juliusmarminge Oct 8, 2026
c28cd90
fix(server): a message sent during a rollback no longer undoes it (#1…
t3dotgg Oct 8, 2026
6053a78
fix(web): PR file stats ignore the hide-whitespace toggle (#16162)
flamboh Oct 8, 2026
28f5516
perf(mobile): omit duplicated turn items from bounded thread snapshot…
juliusmarminge Oct 8, 2026
eb17e0d
perf(mobile): pause elapsed-time timers on hidden thread screens (#15…
juliusmarminge Oct 8, 2026
65404eb
fix(mobile): pause hidden home thread list updates (#15705)
juliusmarminge Oct 8, 2026
e803242
fix(mobile): restore file viewer insets and glass header (#17073)
juliusmarminge Oct 8, 2026
ed27eb7
fix(web): PR code toolbar no longer overlaps in narrow panels (#15561)
flamboh Oct 8, 2026
98b5e1b
fix(web): PR commit menu no longer stretches across the window (#15560)
flamboh Oct 8, 2026
647d8af
fix(web): command palette scrollbar no longer clipped at the top (#17…
flamboh Oct 8, 2026
85a4139
fix(web): Usage breadcrumb stays centered on small viewports (#15552)
flamboh Oct 8, 2026
70eeb39
fix(mobile): stop refreshing Git status on streamed thread updates (#…
juliusmarminge Oct 8, 2026
12a4c05
fix(mobile): skip move indexes for empty and single-thread sections (…
juliusmarminge Oct 8, 2026
8a1fc41
perf(mobile): reuse encoded rows in shell cache saves (#16129)
juliusmarminge Oct 8, 2026
6651aaa
perf(mobile): skip showcase subscriptions in normal builds (#16131)
juliusmarminge Oct 8, 2026
6eed953
perf(mobile): remove unused Home project sorting (#16177)
juliusmarminge Oct 8, 2026
7873504
perf(mobile): reduce move-menu index allocations (#16256)
juliusmarminge Oct 8, 2026
6c1ce06
perf(mobile): skip impossible thread-key lookups (#16263)
juliusmarminge Oct 8, 2026
c001e19
fix(mobile): collect UI runtime garbage on iOS memory warnings (#16296)
juliusmarminge Oct 8, 2026
5c2798d
fix(mobile): stop Git sheet refresh loop (#16305)
juliusmarminge Oct 8, 2026
4fec120
fix(mobile): refresh Git status after reconnect (#16329)
juliusmarminge Oct 8, 2026
833a03a
fix(mobile): update the iOS Git header menu when status changes (#16330)
juliusmarminge Oct 8, 2026
b402f5f
perf(mobile): reuse the settled sort when settled rows are unchanged …
juliusmarminge Oct 8, 2026
33afd5a
perf(mobile): highlight source files in small batches that keep gramm…
juliusmarminge Oct 8, 2026
12865b4
feat(web): quote chips show what you said about the quote (#15703)
flamboh Oct 8, 2026
ff9eb8b
feat(web): projectless threads show their machine in the sidebar (#17…
flamboh Oct 8, 2026
57f96e6
fix(lineage): keep agent effort and speed after completion (#16925)
Bil0000 Oct 8, 2026
97a65a3
fix(mobile): align diff scrolling with glass headers (#17085)
juliusmarminge Oct 8, 2026
73e097b
fix(web): workspace page headers can no longer grow past the top-bar …
maria-rcks Oct 8, 2026
30cc788
feat(mobile): redesign the Add environment sheet (#17092)
juliusmarminge Oct 8, 2026
a6407c4
chore(rtvision): sync upstream main and prepare 0.0.74
kalvenschraut Oct 8, 2026
266ec1b
fix(server): restore upstream branch update authorization
kalvenschraut Oct 8, 2026
3294a03
fix(clients): enforce MCP grants and restore rich composer carets
kalvenschraut Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
23 changes: 1 addition & 22 deletions .coderabbit.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import { defineConfig, type CodeRabbitContext } from "@coderabbitai/config";

const approvability = `Fail when a maintainer should read this pull request before CodeRabbit approves it, and name the rule and file. Fail if it:

- Changes a product default: a setting's default value, or what users get without opting in. Making a feature do what it already promises is a bug fix, not a default change.
- Adds or broadens a directive that disables or suppresses a lint, type-checker, LSP, or other static-analysis diagnostic, including file-level, line-level, and configuration-level overrides.
- Adds a subsystem or user workflow, or is a large refactor across apps or packages.
- Changes packages/contracts or persisted data in a way that existing clients or stored data might not accept.
- Changes authentication, pairing, credentials, secrets, or remote connection trust.
- Adds or changes an external side effect, such as acting on GitHub, publishing a release, or calling a webhook.
- Adds, upgrades, or patches a dependency.
- Changes CI or release configuration, agent or contributor instructions, or any review tool's configuration, including .github/, AGENTS.md, CONTRIBUTING.md, .agents/, and .coderabbit.config.ts.

Otherwise pass. A focused bug fix, copy or layout fix, revert, or docs-only or test-only change passes unless a rule above applies. If you cannot decide, fail rather than report inconclusive. When failing, say that the pull request needs a maintainer's review.
`;

// Org members and collaborators merge their own pull requests. On anyone else's, CodeRabbit
// requests changes until its comments are resolved and its checks pass, then approves.
// requests changes until its comments are resolved, then approves.
const UNGATED_AUTHORS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const isGated = ({ pr }: CodeRabbitContext) => !UNGATED_AUTHORS.has(pr?.authorAssociation ?? "");

Expand All @@ -31,13 +17,6 @@ export default defineConfig((ctx) => ({
pre_merge_checks: {
docstrings: { mode: "off" },
override_requested_reviewers_only: isGated(ctx),
custom_checks: [
{
name: "Approvability",
mode: isGated(ctx) ? "error" : "off",
instructions: approvability,
},
],
},
path_filters: [
// Vendored read-only reference checkouts of upstream Effect and Alchemy
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.73",
"version": "0.0.74",
"private": true,
"type": "module",
"main": "dist-electron/boot.cjs",
Expand Down
246 changes: 246 additions & 0 deletions apps/desktop/src/app/DesktopCliCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import * as DesktopCliCommand from "./DesktopCliCommand.ts";
import * as DesktopCliShim from "./DesktopCliShim.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

const environmentFor = (
path: Path.Path,
input: { home: string; baseDir: string; platform?: NodeJS.Platform; isPackaged?: boolean },
) =>
DesktopEnvironment.DesktopEnvironment.of({
path,
platform: input.platform ?? "linux",
isPackaged: input.isPackaged ?? true,
homeDirectory: input.home,
baseDir: input.baseDir,
stateDir: path.join(input.baseDir, "userdata"),
serverRoot: "/opt/T3 Code/resources/app.asar",
appImagePath: Option.none(),
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);

/** The service for a packaged app; the launcher is written on Install. */
const commandIn = (
input: { home: string; baseDir?: string; platform?: NodeJS.Platform; isPackaged?: boolean },
spawner?: ChildProcessSpawner.ChildProcessSpawner["Service"],
) =>
Effect.gen(function* () {
const path = yield* Path.Path;
const fs = yield* FileSystem.FileSystem;
const baseDir = input.baseDir ?? path.join(input.home, ".t3");
yield* fs.makeDirectory(path.join(baseDir, "userdata"), { recursive: true });
const make = DesktopCliCommand.make.pipe(
Effect.provideService(
DesktopEnvironment.DesktopEnvironment,
environmentFor(path, { ...input, baseDir }),
),
);
return yield* spawner
? make.pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner))
: make;
});

/** A PowerShell that keeps the user's PATH in memory and can be told to fail reads. */
const fakePowerShell = (initial: string) => {
const registry = { path: initial, failReads: false, writes: 0 };
const spawner = ChildProcessSpawner.make((command) =>
Effect.sync(() => {
const { options } = command as unknown as {
readonly options: { readonly env?: Record<string, string> };
};
const env = options.env ?? {};
let exitCode = 0;
let stdout = "";
if (env.T3_SET === "1") {
registry.path = env.T3_PATH ?? "";
registry.writes += 1;
} else if (registry.failReads) {
exitCode = 1;
} else {
stdout = Buffer.from(registry.path, "utf8").toString("base64");
}
return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
stdin: Sink.drain,
stdout: Stream.make(new TextEncoder().encode(stdout)),
stderr: Stream.empty,
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
});
}),
);
return { registry, spawner };
};

it.layer(NodeServices.layer)("DesktopCliCommand", (it) => {
it.effect("links the launcher onto PATH and removes only that link", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const command = yield* commandIn({ home });
const link = path.join(home, ".local", "bin", "t3");

expect(yield* command.state).toEqual({ supported: true, installedPath: null, onPath: false });
// Install writes the launcher itself, even when no local backend ever did.
const installed = yield* command.install;
expect(installed.installedPath).toBe(link);
expect(yield* fs.readLink(link)).toBe(path.join(home, ".t3", "bin", "t3"));
expect((yield* command.install).installedPath).toBe(link);

expect((yield* command.uninstall).installedPath).toBeNull();
expect(yield* fs.exists(link)).toBe(false);
// The launcher itself stays for setup commands.
expect(yield* fs.exists(path.join(home, ".t3", "bin", "t3"))).toBe(true);
}).pipe(Effect.scoped),
);

it.effect("never replaces or removes a t3 it did not create", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const command = yield* commandIn({ home });
const theirs = path.join(home, ".local", "bin", "t3");
yield* fs.makeDirectory(path.dirname(theirs), { recursive: true });
yield* fs.writeFileString(theirs, "npm's t3\n");
// Even a broken link in the next folder is someone else's.
yield* fs.makeDirectory(path.join(home, "bin"), { recursive: true });
yield* fs.symlink(path.join(home, "gone"), path.join(home, "bin", "t3"));

const error = yield* Effect.flip(command.install);
expect(error.message).toContain("Another t3 command is already installed");
yield* command.uninstall;
expect(yield* fs.readFileString(theirs)).toBe("npm's t3\n");
expect(yield* fs.readLink(path.join(home, "bin", "t3"))).toBe(path.join(home, "gone"));
}).pipe(Effect.scoped),
);

it.effect("finds and removes a link left by a previous T3 home", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const before = yield* commandIn({ home, baseDir: path.join(home, "old-t3") });
const link = (yield* before.install).installedPath;

const after = yield* commandIn({ home, baseDir: path.join(home, "new-t3") });
expect((yield* after.state).installedPath).toBe(link);
// Installing again points the link at this home's launcher.
expect((yield* after.install).installedPath).toBe(link);
expect(yield* fs.readLink(link!)).toBe(path.join(home, "new-t3", "bin", "t3"));
yield* after.uninstall;
expect(yield* fs.exists(path.join(home, ".local", "bin", "t3"))).toBe(false);
expect(yield* fs.exists(path.join(home, "bin", "t3"))).toBe(false);
}).pipe(Effect.scoped),
);

it.effect("does not read a large binary another t3 links to", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
// A big executable that happens to contain the marker text is still not ours.
const binary = path.join(home, "native-t3");
yield* fs.writeFileString(binary, `${"\0".repeat(64 * 1024)}${DesktopCliShim.MARKER}`);
yield* fs.makeDirectory(path.join(home, ".local", "bin"), { recursive: true });
yield* fs.symlink(binary, path.join(home, ".local", "bin", "t3"));
const command = yield* commandIn({ home });
expect((yield* command.state).installedPath).toBeNull();
yield* command.uninstall;
expect(yield* fs.readLink(path.join(home, ".local", "bin", "t3"))).toBe(binary);
}).pipe(Effect.scoped),
);

it.effect("reports when another t3 earlier on PATH would run instead", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const shadow = path.join(home, "shadow");
yield* fs.makeDirectory(shadow);
yield* fs.writeFileString(path.join(shadow, "t3"), "#!/bin/sh\n", { mode: 0o755 });
const previous = process.env.PATH;
process.env.PATH = [shadow, path.join(home, ".local", "bin")].join(":");
yield* Effect.addFinalizer(() => Effect.sync(() => (process.env.PATH = previous)));

const command = yield* commandIn({ home });
const installed = yield* command.install;
expect(installed.installedPath).toBe(path.join(home, ".local", "bin", "t3"));
expect(installed.onPath).toBe(false);
yield* fs.remove(path.join(shadow, "t3"));
expect((yield* command.state).onPath).toBe(true);
}).pipe(Effect.scoped),
);

it.effect("leaves the Windows PATH alone when it cannot be read", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const home = yield* fs.makeTempDirectoryScoped();
const userPath = "C:\\Users\\Me\\npm;%USERPROFILE%\\tools";
const { registry, spawner } = fakePowerShell(userPath);
const command = yield* commandIn({ home, platform: "win32" }, spawner);

registry.failReads = true;
const error = yield* Effect.flip(command.install);
expect(error.message).toContain("left unchanged");
expect(registry).toMatchObject({ path: userPath, writes: 0 });

registry.failReads = false;
const launcherDir = DesktopCliShim.launcherPath(
environmentFor(yield* Path.Path, { home, baseDir: `${home}/.t3`, platform: "win32" }),
).replace(/[\\/]t3\.cmd$/, "");
const installed = yield* command.install;
expect(installed.installedPath).not.toBeNull();
// Adding the folder does not prove that npm or a machine command won't shadow it.
expect(installed.onPath).toBe(false);
expect(registry.path).toBe(`${userPath};${launcherDir}`);
yield* command.uninstall;
expect(registry.path).toBe(userPath);
}).pipe(Effect.scoped),
);

it.effect("does not claim or remove a Windows PATH entry the user added", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const launcherDir = path.dirname(
DesktopCliShim.launcherPath(
environmentFor(path, { home, baseDir: path.join(home, ".t3"), platform: "win32" }),
),
);
const userPath = `C:\\Tools;${launcherDir}`;
const { registry, spawner } = fakePowerShell(userPath);
const command = yield* commandIn({ home, platform: "win32" }, spawner);

expect((yield* command.state).installedPath).toBeNull();
yield* command.uninstall;
expect(registry.path).toBe(userPath);
}).pipe(Effect.scoped),
);

it.effect("offers nothing for a development build", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const command = yield* commandIn({
home: yield* fs.makeTempDirectoryScoped(),
isPackaged: false,
});
expect((yield* command.state).supported).toBe(false);
}).pipe(Effect.scoped),
);
});
Loading
Loading