Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
6c8d636
chore: 🔀 merge upstream nightly v0.0.46-nightly.20261008.2801
chamaquita-d3[bot] Oct 8, 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
243 changes: 243 additions & 0 deletions apps/desktop/src/app/DesktopCliCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
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$/, "");
yield* command.install;
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