authorclover caruso <git@paperclover.net> 2026-10-12 17:29:07-07:00
committerclover caruso <git@paperclover.net> 2026-10-16 16:52:04-07:00
log1dced964e7161946a640107004968697e5b258b8
treecd98d50fd695ba6c072d917a9b55b9d257890c8a
parent339179f50a84dc599cce373dbb48f172f182f44e
signature Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ (~clover)

feat: setup t3 fork environment

Establish The Knife as Clover’s fork identity across desktop, web, and mobile surfaces. Keep her README and agent guidance here; render the sculpted blade through one canonical Icon Composer project, use its transparent layer in the UI, and derive platform assets with the classic macOS inset. Preserve shipped bundle identifiers, protocols, data paths, and Electron’s credential-storage runtime name. Use The Knife in mobile widget copy, theme choices, and native display names. Keep generated iOS build paths aligned with the renamed target and custom keychain groups aligned with the installed bundle identity. Strip unsupported capabilities after other Expo plugins run for reduced-capability local builds. Enable local iPhone builds with Xcode 27.0 by gating iOS 27.1 hinge and reserved-region APIs on the installed SDK, while retaining safe-area layout on earlier SDKs. Keep licensed fonts in ignored local storage; include them in private builds without adding their bytes to source history. Record behavior-based patch boundaries, dependency-aware ordering, concrete unscoped titles, at-work labels, and private workplace configuration. Fold iterations into their owning patches with jj squash. Fork-only work is exempt from upstream contribution conventions; setup and agent guidance share this patch. Publish every self-tested change through the shared release feed, with automatic downloads, safe idle UI refreshes, and confirmed backend or shell restarts. Assisted-by: gpt-6.1-sol Assisted-by: gpt-6-astra

175 files changed, 666 insertions(+), 1014 deletions(-)

.agents/skills/test-t3-mobile/SKILL.md+2-2
......@@ -26,7 +26,7 @@ Test with meaningful project and thread data. Read the shared
2626[SQLite fixture reference](../test-t3-app/references/sqlite-fixtures.md) only
2727when inspecting or seeding SQLite. Stop the test server before fixture writes.
2828
29## Launch T3 Code Dev
29## Launch The Knife Dev
3030
3131From the checkout being tested on the selected device host, run:
3232
......@@ -57,7 +57,7 @@ arguments stored in `agent_device_command` and the Bash array
5757 "$agent_device_command" "${agent_device_target_args[@]}"
5858```
5959
60It issues a fresh credential and opens T3 Code Dev's existing pairing route
60It issues a fresh credential and opens The Knife Dev's existing pairing route
6161through AgentDevice. For a backend on the device host, use
6262`http://127.0.0.1:<server-port>` on iOS or `http://10.0.2.2:<server-port>`
6363on Android. For a remote backend, use its reachable origin.
.gitignore+1
......@@ -45,3 +45,4 @@ node_modules/
4545
4646# pnpm content-addressable store; never belongs in the repo.
4747.pnpm-store/
48/apps/web/public/fonts/private/
AGENTS.md+42-11
......@@ -1,4 +1,6 @@
1# T3 Code
1# The Knife
2
3The Knife is Clover’s fork of T3 Code, focused on precise engineering across the backend and UI. Fork behavior and identity belong to this patch stack; the upstream architecture and service names below describe the foundation.
24
35T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity) and serves web, desktop, and mobile clients.
46
......@@ -104,16 +106,6 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
104106
105107For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run `node scripts/mobile-native-client.ts ensure <ios|android> <device-id>` on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See `test-t3-mobile` for the full workflow.
106108
107## Pull requests
108
109- Never make a PR unless the developer explicitly asks you to do so.
110- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
111- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
112- UI changes need before/after images. Motion or timing needs a short video.
113- Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as `.github/pr-assets/`.
114- One request is one PR. Split it only when the maintainer asks. Outside contributions follow the stricter [one problem per PR](CONTRIBUTING.md#one-problem) rule.
115- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.
116
117109## Documentation
118110
119111Most code changes do not need an internal documentation change. Agents can read the code.
......@@ -126,6 +118,45 @@ Most code changes do not need an internal documentation change. Agents can read
126118- Keep user docs in the shipped product's voice, without implementation details or contributor tooling. Update the relevant feature section when how to use it changes. A UI tweak does not need a documentation entry, and a new control does not need its own page.
127119- `docs/operations/` holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides.
128120
121## Fork patch workflow
122
123This checkout is a fork of T3 code for Clover. All changes should be
124categorized into a set of well-maintained changes on top of the upstream. When
125a change to something you already patched is requested, the changes are
126squashed in. This allows rebasing without having hundreds of changes. The
127patches can also be cherry-picked for upstreaming if Theo or someone elses fork
128wants the change. Exact operating rules:
129
130- Fork-only work follows these rules instead of upstream contribution and PR
131 conventions. Apply the upstream process when Clover requests an upstream submission.
132- Keep one Jujutsu change per complete behavior above `main@upstream`, not per
133 iteration or touched file. Fold revisions into their owning patch with
134 `jj squash --from @ --into <patch-change-id> --use-destination-message`.
135- Order setup first, general UI patches next, and work integrations last. Within
136 each group, put dependencies before their consumers.
137- Use unscoped titles such as `feat: ...` and `fix: ...`. The only scope is
138 `at-work`, for work-specific patches.
139- Titles name the concrete change. For replacements, name the original T3
140 component and replacement package; explain behavior and retained features in
141 the description. Rewrite descriptions when a patch's scope changes.
142- Keep fork setup and agent guidance together in the setup patch.
143- After testing a change yourself, publish it with `vp run fork:release` before
144 handing it back. Follow the
145 [private release procedure](docs/operations/development.md#private-fork-releases-for-apple-silicon).
146 Publish every supported desktop platform affected by the change, using the
147 native build and ingestion steps in that procedure.
148 Reuse unchanged UI, static assets, application code, and shell artifacts; all machines consume
149 the same public feed. Keep the upload API key and signing credentials in
150 `~/.config/t3-fork-release.json`, outside the repository.
151- Downloads are automatic. Backend and shell restarts require confirmation.
152 Automatically refresh a pending UI after one minute unfocused or ten minutes
153 idle only when browser, terminal, draft, composer, and scroll state survive.
154 Defer the refresh when a live view or unsaved work cannot be restored.
155- Keep employer names, logos, repository identifiers, and service origins in
156 private host configuration, outside tracked files and change descriptions.
157- Describe the intended behavior and why Clover requested it in each change's description.
158- Use managed Jujutsu workspaces when they help parallel subagents; integrate their changes into the same stack.
159
129160## Plans and work artifacts
130161
131162- Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. `.plans/` is gitignored only as a safety net for legacy tooling.
README.md+5-139
......@@ -1,141 +1,7 @@
1# T3 Code
1# The Knife
22
3T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes).
3A fork of [T3 Code](https://t3.codes) with more precise engineering from the
4backend to the UI/UX.
45
5Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, and Google Antigravity. If they're set up on your computer, T3 Code can control them.
6
7## "Wait, what are you selling me?"
8
9Nothing. We built T3 Code because we wanted the best possible development experience with agents. We were inspired by existing solutions like the Codex desktop app, Conductor, Claude Desktop and Cursor Glass, but none met our bar.
10
11We wanted something performant, remote-ready, and truly open. If we ever go the wrong direction, we want you to have everything you need to fork and build the editor that you want.
12
13## Installation
14
15> [!WARNING]
16> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, and Antigravity. Install and authenticate at least one provider before use:
17>
18> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
19> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
20> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login`
21> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
22> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
23> - Antigravity: enable it in Settings, then use **Install Antigravity** and **Sign in with Google**. No CLI is required.
24
25### Command line
26
27```bash
28curl -fsSL https://t3.codes/install.sh | sh
29```
30
31On Windows, in PowerShell:
32
33```powershell
34irm https://t3.codes/install.ps1 | iex
35```
36
37Then run `t3` to start the server and open the local web app. `t3 service install` keeps it running in the background, `t3 update` moves to a newer release, and `t3 --help` has the full reference.
38
39To try it once without installing, run `npx t3@latest` instead.
40
41### Desktop app
42
43Install the latest version of the desktop app from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), or from your favorite package registry:
44
45#### Windows (`winget`)
46
47```bash
48winget install T3Tools.T3Code
49```
50
51#### Windows (`scoop`)
52
53```bash
54scoop install extras/t3code
55```
56
57#### macOS (Homebrew)
58
59```bash
60brew install --cask t3-code
61```
62
63#### Debian, Ubuntu (`.deb`)
64
65Download the `.deb` from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), then:
66
67```bash
68sudo apt install ./T3-Code-*.deb
69```
70
71#### Arch Linux (AUR)
72
73Stable:
74
75```bash
76yay -S t3code-bin
77```
78
79Nightly:
80
81```bash
82yay -S t3code-nightly-bin
83```
84
85The AUR packaging is maintained in this repository under [`packaging/aur`](./packaging/aur).
86
87## Some notes
88
89We are very very early in this project. Expect bugs.
90
91We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be.
92
93## Documentation
94
95Full docs live in [docs/](./docs). There's no docs site yet.
96
97- [Install and first run](./docs/user/install.md)
98- [Permission modes](./docs/user/permission-modes.md)
99- [Keyboard shortcuts](./docs/user/keybindings.md)
100- [Project settings](./docs/user/project-settings.md)
101- [Appearance preferences](./docs/user/appearance.md)
102- [Remote access from a phone or another machine](./docs/user/remote-access.md)
103- [Connect Claude Code, Codex, ChatGPT and other agents over MCP](./docs/user/outside-agents.md)
104- [Keeping app and server in sync](./docs/user/updating.md)
105- [Source control integrations](./docs/user/source-control.md)
106- Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md)
107- [Run T3 Code as a background service](./docs/user/background-service.md)
108
109Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md).
110
111## If you REALLY want to contribute still.... read this first
112
113### Install `vp`
114
115T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool.
116
117#### macOS / Linux
118
119```bash
120curl -fsSL https://vite.plus | bash
121```
122
123#### Windows
124
125```bash
126irm https://vite.plus/ps1 | iex
127```
128
129Checkout their getting started guide for more information: https://viteplus.dev/guide/
130
131### Install dependencies
132
133```bash
134vp i
135```
136
137Read [CONTRIBUTING.md](./CONTRIBUTING.md) before reporting a bug or opening a PR.
138
139Have a feature request? Start an [Ideas discussion](https://github.com/pingdotgg/t3code/discussions/categories/ideas).
140
141Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv).
6- [Read patches](https://shale.paperclover.net/the-knife/log).
7- Download for [Apple Silicon Mac](https://file.paperclover.net/shr/the-knife/The-Knife-darwin-arm64.zip), [Windows](https://file.paperclover.net/shr/the-knife/The-Knife-win32-x64.exe), or [Linux x64](https://file.paperclover.net/shr/the-knife/The-Knife-linux-x64.AppImage).
apps/desktop/gnome-extension/captureFeedback.js+1-1
......@@ -110,7 +110,7 @@ export class CaptureFeedback {
110110 check();
111111 }));
112112 if (this._owned(sender) !== session || !target)
113 throw new Error("T3 Code's window is not available for activation.");
113 throw new Error("The Knife's window is not available for activation.");
114114 session.target = target;
115115 Main.activateWindow(target, global.get_current_time());
116116 }
apps/desktop/gnome-extension/captureService.js+1-1
......@@ -43,7 +43,7 @@ export class CaptureService {
4343 break;
4444 }
4545 }
46 if (!allowed) throw new Error("Only T3 Code may request a snapshot.");
46 if (!allowed) throw new Error("Only The Knife may request a snapshot.");
4747 const pid = options ? await this._getProcessId(sender) : undefined;
4848 this._checkSession();
4949 const snapshot = await this._takeSnapshot(options?.animate ?? false);
apps/desktop/gnome-extension/captureService.test.js+3-3
......@@ -23,7 +23,7 @@ function fixture(overrides = {}) {
2323
2424it("rejects other clients before looking at the active window", async () => {
2525 const { service, snapshots } = fixture();
26 await expect(service.capture(":1.99")).rejects.toThrow("Only T3 Code");
26 await expect(service.capture(":1.99")).rejects.toThrow("Only The Knife");
2727 expect(snapshots()).toBe(0);
2828 expect(await service.capture(":1.23")).toBe("pixels");
2929});
......@@ -38,7 +38,7 @@ it("rechecks the name owner on each capture", async () => {
3838 const { service } = fixture({ getNameOwner: async () => owner });
3939 expect(await service.capture(owner)).toBe("pixels");
4040 owner = ":1.24";
41 await expect(service.capture(":1.23")).rejects.toThrow("Only T3 Code");
41 await expect(service.capture(":1.23")).rejects.toThrow("Only The Knife");
4242});
4343
4444it("rejects locked and non-Wayland sessions without taking a screenshot", async () => {
......@@ -94,7 +94,7 @@ it("rejects concurrent requests and clears busy after a failed capture", async (
9494 await expect(service.capture(":1.23")).rejects.toThrow("already in progress");
9595 finish(new Error("gone"));
9696 await expect(first).rejects.toThrow("gone");
97 await expect(service.capture(":1.99")).rejects.toThrow("Only T3 Code");
97 await expect(service.capture(":1.99")).rejects.toThrow("Only The Knife");
9898});
9999
100100it("prepares focus/effects only after pixels and identity have been captured", async () => {
apps/desktop/gnome-extension/metadata.json+2-2
......@@ -1,7 +1,7 @@
11{
22 "uuid": "snap-shot@t3.codes",
3 "name": "T3 Code SnapShots",
4 "description": "Allows T3 Code to capture the focused window, identify it for optional accessibility text, and animate it into T3 Code. Disable this extension to revoke access.",
3 "name": "The Knife SnapShots",
4 "description": "Allows The Knife to capture the focused window, identify it for optional accessibility text, and animate it into The Knife. Disable this extension to revoke access.",
55 "shell-version": ["45", "46", "47", "48", "49", "50"],
66 "version": 2,
77 "url": "https://github.com/pingdotgg/t3code"
apps/desktop/package.json+1-1
......@@ -42,5 +42,5 @@
4242 "tailwindcss": "^4.0.0",
4343 "vite-plus": "catalog:"
4444 },
45 "productName": "T3 Code (Alpha)"
45 "productName": "The Knife"
4646}
apps/desktop/resources/dmg/dmg-background-latest.svg+1-1
......@@ -20,7 +20,7 @@
2020 <svg x="32" y="23" width="30" height="18" viewBox="15.5309 37 94.3941 56.96" fill="#303034">
2121 <path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"/>
2222 </svg>
23 <text x="320" y="104" fill="#303034" font-family="Helvetica" font-size="23" font-weight="700" text-anchor="middle">Drag T3 Code into Applications.</text>
23 <text x="320" y="104" fill="#303034" font-family="Helvetica" font-size="23" font-weight="700" text-anchor="middle">Drag The Knife into Applications.</text>
2424 <path d="M270 204Q320 179 370 204M358 190L370 204L351 209" stroke="#505057" stroke-width="2.2" fill="none" opacity="1" stroke-linecap="round" stroke-linejoin="round"/>
2525 <text x="320" y="367" fill="#626269" font-family="Helvetica" font-size="12" font-weight="400" text-anchor="middle">Once copied, open it from Applications.</text>
2626</svg>
apps/desktop/resources/dmg/dmg-background-nightly.svg+1-1
......@@ -27,7 +27,7 @@
2727 <path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"/>
2828 </svg>
2929 <text x="608" y="38" fill="#C3C8E5" font-family="Helvetica" font-size="9" font-weight="400" text-anchor="end">NIGHTLY</text>
30 <text x="320" y="104" fill="#edf0ff" font-family="Helvetica" font-size="23" font-weight="700" text-anchor="middle">Drag T3 Code into Applications.</text>
30 <text x="320" y="104" fill="#edf0ff" font-family="Helvetica" font-size="23" font-weight="700" text-anchor="middle">Drag The Knife into Applications.</text>
3131 <path d="M270 204Q320 179 370 204M358 190L370 204L351 209" stroke="#ECEDFF" stroke-width="2.2" fill="none" opacity="1" stroke-linecap="round" stroke-linejoin="round"/>
3232 <text x="320" y="367" fill="#D0CFE6" font-family="Helvetica" font-size="12" font-weight="400" text-anchor="middle">Once copied, open it from Applications.</text>
3333</svg>
apps/desktop/resources/linux/com.t3tools.t3code.metainfo.xml+2-2
......@@ -3,14 +3,14 @@
33 <id>com.t3tools.t3code</id>
44 <metadata_license>CC0-1.0</metadata_license>
55 <project_license>MIT</project_license>
6 <name>T3 Code</name>
6 <name>The Knife</name>
77 <summary>Desktop GUI for coding agents</summary>
88 <developer id="com.t3tools">
99 <name>T3 Tools Inc.</name>
1010 </developer>
1111 <description>
1212 <p>
13 T3 Code is an open-source desktop app for coding agents. Work with your
13 The Knife is an open-source desktop app for coding agents. Work with your
1414 existing agent subscriptions, review code changes, and run commands in
1515 your projects.
1616 </p>
apps/desktop/scripts/electron-launcher.mjs+9-18
......@@ -1,4 +1,4 @@
1// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron"
1// This file mostly exists because we want dev mode to say "The Knife (Dev)" instead of "electron"
22
33import * as NodeChildProcess from "node:child_process";
44import * as NodeFS from "node:fs";
......@@ -15,19 +15,13 @@ const repoRoot = NodePath.resolve(desktopDir, "..", "..");
1515const devBundleIdSuffix = NodePath.basename(repoRoot)
1616 .toLowerCase()
1717 .replaceAll(/[^a-z0-9]+/g, "");
18const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
18const APP_DISPLAY_NAME = isDevelopment ? "The Knife (Dev)" : "The Knife";
1919const APP_BUNDLE_ID = isDevelopment
2020 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
2121 : "com.t3tools.t3code";
2222const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
2323const LAUNCHER_VERSION = 20;
24const developmentMacIconPngPath = NodePath.join(
25 repoRoot,
26 "assets",
27 "dev",
28 "blueprint-macos-1024.png",
29);
30const productionMacIconPngPath = NodePath.join(repoRoot, "assets", "prod", "black-macos-1024.png");
24const macIconPngPath = NodePath.join(repoRoot, "assets", "knife", "macos-1024.png");
3125// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
3226const hostPlatform = NodeOS.platform();
3327
......@@ -203,13 +197,10 @@ function registerMacLauncherBundle(appBundlePath) {
203197}
204198
205199// Bundle-internal paths are macOS paths whatever host builds them.
206export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) {
200export function resolveMacLauncherIconPaths(runtimeDir) {
207201 return {
208 sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath,
209 generatedIconPath: NodePath.posix.join(
210 runtimeDir,
211 development ? "icon-dev.icns" : "icon-prod.icns",
212 ),
202 sourceIconPath: macIconPngPath,
203 generatedIconPath: NodePath.posix.join(runtimeDir, "icon.icns"),
213204 };
214205}
215206
......@@ -270,8 +261,8 @@ export function resolveMacBundleInfoPlistStrings(executableName) {
270261 CFBundleExecutable: executableName,
271262 CFBundleIconFile: "icon.icns",
272263 NSScreenCaptureUsageDescription:
273 "T3 Code captures the active window when you use the snapshot shortcut.",
274 NSDocumentsFolderUsageDescription: "T3 Code reads project files you open in the desktop app.",
264 "The Knife captures the active window when you use the snapshot shortcut.",
265 NSDocumentsFolderUsageDescription: "The Knife reads project files you open in the desktop app.",
275266 };
276267}
277268
......@@ -404,7 +395,7 @@ function buildMacLauncher(electronBinaryPath) {
404395 if (isDevelopment) {
405396 // Keep Electron's native executable inside the branded bundle. Launching the
406397 // node_modules copy makes macOS associate the process (and Dock label) with
407 // Electron.app even though this bundle's Info.plist has the T3 Code name.
398 // Electron.app even though this bundle's Info.plist names The Knife.
408399 // Its conventional executable name also keeps Electron's default-app runtime
409400 // in development mode instead of making app.isPackaged report true.
410401 writeDevelopmentEnvironmentScript();
apps/desktop/scripts/electron-launcher.test.mjs+15-20
......@@ -76,18 +76,18 @@ describe("electron development launcher", () => {
7676
7777 it("keeps the native Electron executable name inside the branded macOS bundle", () => {
7878 const paths = resolveMacLauncherPaths(
79 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
80 "T3 Code (Dev)",
79 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app",
80 "The Knife (Dev)",
8181 );
8282
83 assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
83 assert.equal(paths.launcherExecutableName, "The Knife (Dev) Launcher");
8484 assert.equal(
8585 paths.launcherBinaryPath,
86 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
86 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/The Knife (Dev) Launcher",
8787 );
8888 assert.equal(
8989 paths.runtimeElectronBinaryPath,
90 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
90 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/Electron",
9191 );
9292
9393 const script = makeDevelopmentLauncherScript({
......@@ -98,32 +98,32 @@ describe("electron development launcher", () => {
9898 });
9999 assert.include(
100100 script,
101 "exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
101 "exec '/repo/apps/desktop/.electron-runtime/The Knife (Dev).app/Contents/MacOS/Electron'",
102102 );
103103 assert.notInclude(script, "node_modules/electron");
104104 });
105105
106106 it("declares why the macOS app needs protected access", () => {
107 const values = resolveMacBundleInfoPlistStrings("T3 Code (Dev) Launcher");
107 const values = resolveMacBundleInfoPlistStrings("The Knife (Dev) Launcher");
108108
109109 assert.equal(
110110 values.NSScreenCaptureUsageDescription,
111 "T3 Code captures the active window when you use the snapshot shortcut.",
111 "The Knife captures the active window when you use the snapshot shortcut.",
112112 );
113113 assert.equal(
114114 values.NSDocumentsFolderUsageDescription,
115 "T3 Code reads project files you open in the desktop app.",
115 "The Knife reads project files you open in the desktop app.",
116116 );
117117 });
118118
119119 it("ad-hoc signs the complete development app bundle", () => {
120 assert.deepEqual(resolveMacCodeSignArguments("/runtime/T3 Code (Dev).app"), [
120 assert.deepEqual(resolveMacCodeSignArguments("/runtime/The Knife (Dev).app"), [
121121 "--force",
122122 "--deep",
123123 "--sign",
124124 "-",
125125 "--timestamp=none",
126 "/runtime/T3 Code (Dev).app",
126 "/runtime/The Knife (Dev).app",
127127 ]);
128128 });
129129
......@@ -141,14 +141,9 @@ describe("electron development launcher", () => {
141141 }
142142 });
143143
144 it("derives launcher icons from canonical development and production assets", () => {
145 const development = resolveMacLauncherIconPaths("/runtime", true);
146 const production = resolveMacLauncherIconPaths("/runtime", false);
147
148 // The source icons are real repo paths, joined for the host.
149 assert.match(development.sourceIconPath, /assets[\\/]dev[\\/]blueprint-macos-1024\.png$/);
150 assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns");
151 assert.match(production.sourceIconPath, /assets[\\/]prod[\\/]black-macos-1024\.png$/);
152 assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns");
144 it("derives launcher icons from the canonical Knife artwork", () => {
145 const icon = resolveMacLauncherIconPaths("/runtime");
146 assert.match(icon.sourceIconPath, /assets[\\/]knife[\\/]macos-1024\.png$/);
147 assert.equal(icon.generatedIconPath, "/runtime/icon.icns");
153148 });
154149});
apps/desktop/scripts/verify-preload-bundle.mjs+1
......@@ -108,6 +108,7 @@ const executeBundle = (source, sandboxModules) => {
108108 {
109109 process: sandboxProcess,
110110 require: requireSandboxModule,
111 window: { addEventListener: () => undefined },
111112 },
112113 {
113114 filename: "desktop-preload.cjs",
apps/desktop/src/app/DesktopApp.ts+1-1
......@@ -136,7 +136,7 @@ const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupErr
136136 const wasQuitting = yield* Ref.getAndSet(state.quitting, true);
137137 if (!wasQuitting) {
138138 yield* electronDialog.showErrorBox(
139 "T3 Code failed to start",
139 "The Knife failed to start",
140140 `Stage: ${stage}\n${message}${detail}`,
141141 );
142142 }
apps/desktop/src/app/DesktopAppActivation.ts+1-1
......@@ -171,7 +171,7 @@ export async function startDesktopAppControlServer(input: {
171171 activeRequestId = parsed.requestId;
172172 void input.handle(parsed).then(finish, () => {
173173 finish(
174 invalidResponse(parsed.requestId, "T3 Code could not process the desktop app request."),
174 invalidResponse(parsed.requestId, "The Knife could not process the desktop app request."),
175175 );
176176 });
177177 });
apps/desktop/src/app/DesktopAppActivationBroker.ts+4-4
......@@ -45,7 +45,7 @@ export class DesktopAppActivationBroker {
4545 request(request: DesktopAppActivationRequest): Promise<DesktopAppActivationResponse> {
4646 if (this.#closed) {
4747 return Promise.resolve(
48 failure(request.requestId, "renderer-unavailable", "T3 Code is shutting down."),
48 failure(request.requestId, "renderer-unavailable", "The Knife is shutting down."),
4949 );
5050 }
5151 if (this.#pending.has(request.requestId)) {
......@@ -90,7 +90,7 @@ export class DesktopAppActivationBroker {
9090 failure(
9191 pending.request.requestId,
9292 "renderer-unavailable",
93 "The T3 Code window closed before it opened the project.",
93 "The Knife closed before opening the project.",
9494 ),
9595 );
9696 }
......@@ -103,7 +103,7 @@ export class DesktopAppActivationBroker {
103103
104104 cancel(requestId: string): void {
105105 this.#settle(
106 failure(requestId, "renderer-unavailable", "The command closed before T3 Code was ready."),
106 failure(requestId, "renderer-unavailable", "The command closed before The Knife was ready."),
107107 );
108108 }
109109
......@@ -112,7 +112,7 @@ export class DesktopAppActivationBroker {
112112 this.#renderer = null;
113113 for (const pending of this.#pending.values()) {
114114 this.#settle(
115 failure(pending.request.requestId, "renderer-unavailable", "T3 Code is shutting down."),
115 failure(pending.request.requestId, "renderer-unavailable", "The Knife is shutting down."),
116116 );
117117 }
118118 }
apps/desktop/src/app/DesktopAppIdentity.test.ts+27-21
......@@ -218,7 +218,7 @@ describe("DesktopAppIdentity", () => {
218218 yield* identity.configure;
219219
220220 assert.deepEqual(calls.setName, ["T3 Code Alpha"]);
221 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");
221 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "The Knife");
222222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
223223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
224224 // Packaged: the bundle's own icon stands, so a custom one the user
......@@ -247,28 +247,34 @@ describe("DesktopAppIdentity", () => {
247247 stage: "Dev",
248248 environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
249249 },
250 ])("uses a valid native User-Agent product name for $stage", ({ stage, environment }) => {
251 const calls: ElectronAppCalls = {
252 setAboutPanelOptions: [],
253 setDockIcon: [],
254 setName: [],
255 };
250 ])(
251 "preserves the credential identity and valid native User-Agent for $stage",
252 ({ stage, environment }) => {
253 const calls: ElectronAppCalls = {
254 setAboutPanelOptions: [],
255 setDockIcon: [],
256 setName: [],
257 };
256258
257 return withIdentity(
258 Effect.gen(function* () {
259 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
260 yield* identity.configure;
259 return withIdentity(
260 Effect.gen(function* () {
261 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
262 yield* identity.configure;
261263
262 const runtimeName = calls.setName[0];
263 assert.isDefined(runtimeName);
264 assert.equal(runtimeName, `T3 Code ${stage}`);
265 // RFC 9110's token grammar, after Electron removes ASCII spaces.
266 assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
267 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`);
268 }),
269 { calls, environment },
270 );
271 });
264 const runtimeName = calls.setName[0];
265 assert.isDefined(runtimeName);
266 assert.equal(runtimeName, `T3 Code ${stage}`);
267 // RFC 9110's token grammar, after Electron removes ASCII spaces.
268 assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
269 assert.equal(
270 calls.setAboutPanelOptions[0]?.applicationName,
271 stage === "Alpha" ? "The Knife" : `The Knife (${stage})`,
272 );
273 }),
274 { calls, environment },
275 );
276 },
277 );
272278
273279 it.effect("sets the dock icon only when running unpackaged", () => {
274280 const calls: ElectronAppCalls = {
apps/desktop/src/app/DesktopAppIdentity.ts+2-6
......@@ -91,12 +91,8 @@ export const make = Effect.gen(function* () {
9191
9292 const configure = Effect.gen(function* () {
9393 const commitHash = yield* resolveAboutCommitHash;
94 // Electron removes spaces from this name to build the native User-Agent
95 // product token, but leaves parentheses intact. Keep the runtime name valid
96 // without rewriting preview sessions (which breaks Turnstile, #7110).
97 yield* electronApp.setName(
98 `${environment.branding.baseName} ${environment.branding.stageLabel}`,
99 );
94 // Electron's runtime name also identifies the shipped Safe Storage/keyring entry.
95 yield* electronApp.setName(`T3 Code ${environment.branding.stageLabel}`);
10096 yield* electronApp.setAboutPanelOptions({
10197 applicationName: environment.displayName,
10298 applicationVersion: environment.appVersion,
apps/desktop/src/app/DesktopAssets.test.ts+3-3
......@@ -50,7 +50,7 @@ describe("DesktopAssets", () => {
5050 ),
5151 );
5252 const layerFileSystem = FileSystem.layerNoop({
53 exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")),
53 exists: (path) => Effect.succeed(String(path).includes("/assets/knife/")),
5454 });
5555 const assets = yield* DesktopAssets.DesktopAssets.pipe(
5656 Effect.provide(
......@@ -62,8 +62,8 @@ describe("DesktopAssets", () => {
6262
6363 const icons = yield* assets.iconPaths;
6464
65 assert.match(Option.getOrThrow(icons.ico), /assets\/dev\/blueprint-windows\.ico$/);
66 assert.match(Option.getOrThrow(icons.png), /assets\/dev\/blueprint-universal-1024\.png$/);
65 assert.match(Option.getOrThrow(icons.ico), /assets\/knife\/icon\.ico$/);
66 assert.match(Option.getOrThrow(icons.png), /assets\/knife\/icon-1024\.png$/);
6767 assert.isTrue(Option.isNone(icons.icns));
6868 }),
6969 );
apps/desktop/src/app/DesktopAssets.ts+4-19
......@@ -61,33 +61,18 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func
6161 return Option.none<string>();
6262});
6363
64const sourceTreeIconFileNames = {
65 dev: {
66 ico: "blueprint-windows.ico",
67 macPng: "blueprint-macos-1024.png",
68 universalPng: "blueprint-universal-1024.png",
69 },
70 prod: {
71 ico: "t3-black-windows.ico",
72 macPng: "black-macos-1024.png",
73 universalPng: "black-universal-1024.png",
74 },
75} as const;
76
7764function resolveSourceTreeIconPath(
7865 environment: DesktopEnvironment.DesktopEnvironment["Service"],
7966 ext: keyof DesktopIconPaths,
8067): string | undefined {
8168 if (environment.isPackaged || ext === "icns") return undefined;
82 const brand = environment.isDevelopment ? "dev" : "prod";
83 const fileNames = sourceTreeIconFileNames[brand];
8469 const fileName =
8570 ext === "ico"
86 ? fileNames.ico
71 ? "icon.ico"
8772 : environment.platform === "darwin"
88 ? fileNames.macPng
89 : fileNames.universalPng;
90 return environment.path.join(environment.rootDir, "assets", brand, fileName);
73 ? "macos-1024.png"
74 : "icon-1024.png";
75 return environment.path.join(environment.rootDir, "assets", "knife", fileName);
9176}
9277
9378const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
apps/desktop/src/app/DesktopCliShim.ts+1-1
......@@ -24,7 +24,7 @@ const shellWord = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
2424const cmdText = (value: string) => value.replaceAll("%", "%%");
2525const cmdWord = (value: string) => `"${cmdText(value)}"`;
2626
27const MOVED = "T3 Code has moved or been removed. Open the app once to update this command.";
27const MOVED = "The Knife has moved or been removed. Open the app once to update this command.";
2828
2929export type CliShimTarget =
3030 | { readonly kind: "appimage"; readonly appImage: string; readonly executableName: string }
apps/desktop/src/app/DesktopEnvironment.ts+2-2
......@@ -93,7 +93,7 @@ export class DesktopEnvironment extends Context.Service<
9393 }
9494>()("@t3tools/desktop/app/DesktopEnvironment") {}
9595
96const APP_BASE_NAME = "T3 Code";
96const APP_BASE_NAME = "The Knife";
9797
9898function resolveDesktopAppStageLabel(input: {
9999 readonly isDevelopment: boolean;
......@@ -114,7 +114,7 @@ export function resolveDesktopAppBranding(input: {
114114 return {
115115 baseName: APP_BASE_NAME,
116116 stageLabel,
117 displayName: `${APP_BASE_NAME} (${stageLabel})`,
117 displayName: stageLabel === "Alpha" ? APP_BASE_NAME : `${APP_BASE_NAME} (${stageLabel})`,
118118 };
119119}
120120
apps/desktop/src/app/DesktopWebLinks.ts+2-2
......@@ -6,14 +6,14 @@ import * as Option from "effect/Option";
66import * as ElectronWindow from "../electron/ElectronWindow.ts";
77import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";
88
9/** An http(s) link the operating system handed to T3 Code as the default browser. */
9/** An http(s) link the operating system handed to The Knife as the default browser. */
1010export const isWebLink = (value: string) => {
1111 if (!URL.canParse(value)) return false;
1212 const { protocol } = new URL(value);
1313 return protocol === "http:" || protocol === "https:";
1414};
1515
16/** An HTML file the operating system asked T3 Code to open, as browsers are asked to. */
16/** An HTML file the operating system asked The Knife to open, as browsers are asked to. */
1717export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path);
1818
1919/**
apps/desktop/src/backend/DesktopBackendPool.ts+3-3
......@@ -247,7 +247,7 @@ export const layer = Layer.effect(
247247 );
248248 yield* electronDialog.showErrorBox(
249249 "WSL backend is still unavailable",
250 `${reason}\n\nT3 Code will use the Windows backend for this launch and retry WSL the next time the app starts.`,
250 `${reason}\n\nThe Knife will use the Windows backend for this launch and retry WSL the next time the app starts.`,
251251 );
252252 yield* appSettings.applyWslWindowsFallbackInMemory;
253253 return true;
......@@ -258,7 +258,7 @@ export const layer = Layer.effect(
258258 });
259259 yield* electronDialog.showErrorBox(
260260 "WSL backend couldn't start",
261 `${reason}\n\nFalling back to the Windows backend so T3 Code can open. Re-enable the WSL backend from Settings > Connections once the WSL distro is fixed.`,
261 `${reason}\n\nFalling back to the Windows backend so The Knife can open. Re-enable the WSL backend from Settings > Connections once the WSL distro is fixed.`,
262262 );
263263 // Fully disable the WSL backend — both flags, matching the "Switch to
264264 // Windows" recovery path — so the manager's next restart re-resolves the
......@@ -309,7 +309,7 @@ export const layer = Layer.effect(
309309 electronDialog
310310 .showErrorBox(
311311 "This T3 home is unavailable",
312 "Another T3 Code server, such as the background service, is using this data directory. Stop that server and reopen T3 Code, or set a separate T3CODE_HOME and pair with the running server.",
312 "Another The Knife server, such as the background service, is using this data directory. Stop that server and reopen The Knife, or set a separate T3CODE_HOME and pair with the running server.",
313313 )
314314 .pipe(Effect.andThen(electronApp.quit)),
315315 });
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts+1-1
......@@ -104,7 +104,7 @@ export const make = Effect.gen(function* () {
104104 httpBaseUrl: config.httpBaseUrl.href,
105105 credential,
106106 clientMetadata: {
107 label: "T3 Code Desktop",
107 label: "The Knife Desktop",
108108 deviceType: "desktop",
109109 },
110110 }).pipe(
apps/desktop/src/electron/WindowsForeground.ts+1-1
......@@ -216,7 +216,7 @@ export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> {
216216export async function activateWindowsForeground(handleBuffer: Buffer): Promise<void> {
217217 const api = await loadWindowsForegroundApi();
218218 if (activateWindowsForegroundWithApi(handleBuffer, api)) return;
219 throw new Error("Windows refused to activate the T3 Code window.");
219 throw new Error("Windows refused to activate The Knife.");
220220}
221221
222222export async function isWindowsShellHostedForeground(): Promise<boolean> {
apps/desktop/src/permissions/MacPermissionHelper.test.ts+8-8
......@@ -61,7 +61,7 @@ vi.mock("electron", async () => {
6161 }
6262 return {
6363 app: {
64 getPath: () => "/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code",
64 getPath: () => "/Applications/The Knife (Nightly).app/Contents/MacOS/The Knife",
6565 },
6666 nativeImage: { createFromPath: mocks.createFromPath },
6767 BrowserWindow: class extends MockWindow {},
......@@ -124,11 +124,11 @@ function send(action: string, trusted = true) {
124124
125125describe("macAppBundlePath", () => {
126126 it("resolves bundles with spaces and refuses non-bundle executables", () => {
127 expect(macAppBundlePath("/Applications/T3 Code.app/Contents/MacOS/T3 Code")).toBe(
128 "/Applications/T3 Code.app",
127 expect(macAppBundlePath("/Applications/The Knife.app/Contents/MacOS/The Knife")).toBe(
128 "/Applications/The Knife.app",
129129 );
130130 expect(macAppBundlePath("/usr/local/bin/electron")).toBeUndefined();
131 expect(macAppBundlePath("/Applications/T3 Code.app/other/MacOS/T3 Code")).toBeUndefined();
131 expect(macAppBundlePath("/Applications/The Knife.app/other/MacOS/The Knife")).toBeUndefined();
132132 });
133133});
134134it("drags the running app bundle only for the helper's own renderer", async () => {
......@@ -138,11 +138,11 @@ it("drags the running app bundle only for the helper's own renderer", async () =
138138 send("drag");
139139 expect(mocks.createFromPath).toHaveBeenCalledWith("/bundle/prod-resources/icon.png");
140140 expect(mocks.startDrag).toHaveBeenCalledWith({
141 file: "/Applications/T3 Code (Nightly).app",
141 file: "/Applications/The Knife (Nightly).app",
142142 icon: mocks.createFromPath.mock.results[0]!.value.resize(),
143143 });
144144 send("finder");
145 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");
145 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/The Knife (Nightly).app");
146146});
147147it("rechecks permissions and releases resources when granted", async () => {
148148 await open();
......@@ -168,7 +168,7 @@ it("does not open for a permission already granted", async () => {
168168});
169169it("does not show a helper with a missing packaged icon", async () => {
170170 mocks.createFromPath.mockReturnValueOnce({ isEmpty: () => true });
171 await expect(open()).rejects.toThrow("packaged T3 Code icon is missing");
171 await expect(open()).rejects.toThrow("packaged The Knife icon is missing");
172172 expect(windows).toHaveLength(0);
173173});
174174it("cleans up when the helper page fails to load", async () => {
......@@ -184,7 +184,7 @@ it("offers the Finder fallback when native dragging fails", async () => {
184184 throw new Error("drag failed");
185185 });
186186 send("drag");
187 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");
187 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/The Knife (Nightly).app");
188188 expect(windows[0]!.destroyed).toBe(false);
189189});
190190
apps/desktop/src/permissions/MacPermissionHelper.ts+3-3
......@@ -60,8 +60,8 @@ button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; }
6060img { width: 32px; height: 32px; pointer-events: none; }
6161</style></head><body><main id="panel">
6262<button id="close" aria-label="Close permission helper">×</button>
63<header>↑ Drag T3 Code into the list above</header>
64<button id="app" draggable="true" aria-label="Drag T3 Code to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">T3 Code</button>
63<header>↑ Drag The Knife into the list above</header>
64<button id="app" draggable="true" aria-label="Drag The Knife to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">The Knife</button>
6565</main></body></html>`;
6666}
6767
......@@ -95,7 +95,7 @@ export class MacPermissionHelper {
9595 const appIcon = iconPaths
9696 .map((iconPath) => Electron.nativeImage.createFromPath(iconPath))
9797 .find((image) => !image.isEmpty());
98 if (!appIcon) throw new Error("The packaged T3 Code icon is missing.");
98 if (!appIcon) throw new Error("The packaged The Knife icon is missing.");
9999 const icon = appIcon.resize({ width: 64, height: 64 });
100100 const window = new Electron.BrowserWindow({
101101 width: 560,
apps/desktop/src/snapShot/DesktopSnapShot.test.ts+3-3
......@@ -1630,7 +1630,7 @@ it.effect(
16301630 const warning = logs.find(
16311631 (message) =>
16321632 Array.isArray(message) &&
1633 message[0] === "The compositor could not activate T3 Code after the snapshot",
1633 message[0] === "The compositor could not activate The Knife after the snapshot",
16341634 );
16351635 assert.strictEqual(Array.isArray(warning) ? warning[1] : undefined, activationFailure);
16361636 const pending = yield* decodePendingMetadata(saved);
......@@ -3161,7 +3161,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re
31613161 const revoked = yield* service.state;
31623162 assert.equal(
31633163 revoked.message,
3164 "Allow Screen Recording in System Settings, then restart T3 Code.",
3164 "Allow Screen Recording in System Settings, then restart The Knife.",
31653165 );
31663166 assert.deepEqual(revoked.macPermissions, { screenRecording: false, accessibility: true });
31673167
......@@ -3174,7 +3174,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re
31743174 const blocked = yield* service.state;
31753175 assert.equal(
31763176 blocked.message,
3177 "Allow Screen Recording in System Settings, then restart T3 Code.",
3177 "Allow Screen Recording in System Settings, then restart The Knife.",
31783178 );
31793179 assert.isFalse(blocked.shortcutRegistered);
31803180
apps/desktop/src/snapShot/DesktopSnapShot.ts+8-8
......@@ -101,11 +101,11 @@ const FLASH_FRAME_INTERVAL_MS = 16;
101101const FLASH_PEAK_OPACITY = 0.08;
102102const MAC_SCREEN_CAPTURE_SETTINGS_URL = MAC_PERMISSION_SETTINGS_URLS["screen-recording"];
103103const MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE =
104 "Allow Screen Recording in System Settings, then restart T3 Code.";
104 "Allow Screen Recording in System Settings, then restart The Knife.";
105105const MAC_ACCESSIBILITY_PERMISSION_MESSAGE =
106 "Allow Accessibility in System Settings, then restart T3 Code.";
106 "Allow Accessibility in System Settings, then restart The Knife.";
107107const MAC_BOTH_PERMISSIONS_MESSAGE =
108 "Allow Accessibility and Screen Recording in System Settings, then restart T3 Code.";
108 "Allow Accessibility and Screen Recording in System Settings, then restart The Knife.";
109109const MAC_PERMISSION_MESSAGES = new Set([
110110 MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE,
111111 MAC_ACCESSIBILITY_PERMISSION_MESSAGE,
......@@ -187,7 +187,7 @@ export class DesktopSnapShot extends Context.Service<
187187 shortcut: SnapShotShortcut,
188188 ) => Effect.Effect<DesktopSnapShotShortcutAvailability>;
189189 readonly setShortcutSuppressed: (suppressed: boolean) => Effect.Effect<void>;
190 /** Capture the foreground window in place, including T3 Code itself. */
190 /** Capture the foreground window in place, including The Knife itself. */
191191 readonly capture: Effect.Effect<void, DesktopSnapShotError>;
192192 readonly listPending: Effect.Effect<
193193 ReadonlyArray<DesktopPendingSnapShot>,
......@@ -898,7 +898,7 @@ export const make = Effect.gen(function* () {
898898 const capturedAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
899899 if (snapshot.linuxActivationFailure) {
900900 yield* Effect.logWarning(
901 "The compositor could not activate T3 Code after the snapshot",
901 "The compositor could not activate The Knife after the snapshot",
902902 snapshot.linuxActivationFailure.cause,
903903 );
904904 }
......@@ -1018,7 +1018,7 @@ export const make = Effect.gen(function* () {
10181018 if (mode === "portal" && niriSocketPath()) {
10191019 return {
10201020 available: false,
1021 message: "Configure the capture shortcut in your Niri config, not in T3 Code.",
1021 message: "Configure the capture shortcut in your Niri config, not in The Knife.",
10221022 };
10231023 }
10241024 if (mode === "portal" && isHyprlandCaptureSession()) {
......@@ -1168,7 +1168,7 @@ export const make = Effect.gen(function* () {
11681168 const { startNiriCaptureShortcut } = await import("./NiriCaptureShortcut.ts");
11691169 return startNiriCaptureShortcut(linuxAppId, onCurrentShortcut, () => {
11701170 void runPromise(
1171 setShortcutFailure("The Niri capture endpoint disconnected. Restart T3 Code."),
1171 setShortcutFailure("The Niri capture endpoint disconnected. Restart The Knife."),
11721172 ).catch(() => undefined);
11731173 });
11741174 }).pipe(
......@@ -1190,7 +1190,7 @@ export const make = Effect.gen(function* () {
11901190 shortcutActionRegistered: registered,
11911191 shortcutMessage: registered
11921192 ? "Set up the shortcut to add it to your Niri config."
1193 : "Could not start the Niri capture endpoint. Another T3 Code instance may be using it.",
1193 : "Could not start the Niri capture endpoint. Another The Knife instance may be using it.",
11941194 message: null,
11951195 });
11961196 return;
apps/desktop/src/snapShot/GnomeCaptureSetup.ts+5-5
......@@ -56,7 +56,7 @@ export async function installGnomeCaptureBundle({ bundle, dataHome }: SetupPaths
5656 await NodeFSP.readFile(NodePath.join(target, "metadata.json"), "utf8"),
5757 );
5858 if (installed.version > metadata.version)
59 throw new Error("A newer extension is installed. Update T3 Code instead of replacing it.");
59 throw new Error("A newer extension is installed. Update The Knife instead of replacing it.");
6060 }
6161 const staged = await NodeFSP.mkdtemp(NodePath.join(parent, ".t3-capture-install-"));
6262 let backup: string | undefined;
......@@ -184,7 +184,7 @@ export class GnomeCaptureSetup {
184184 return {
185185 status: "restart-required",
186186 message:
187 "Installed. Save your work, sign out of GNOME and sign back in, then return here to enable the extension. Restarting T3 Code alone is not enough.",
187 "Installed. Save your work, sign out of GNOME and sign back in, then return here to enable the extension. Restarting The Knife alone is not enough.",
188188 };
189189 if ((installed?.version ?? info.version?.value ?? 0) < bundled.version)
190190 return {
......@@ -196,12 +196,12 @@ export class GnomeCaptureSetup {
196196 return {
197197 status: "extensions-disabled",
198198 message:
199 "GNOME has disabled user extensions. Turn on Extensions in the GNOME Extensions app, then check again. T3 Code will not enable your other extensions for you.",
199 "GNOME has disabled user extensions. Turn on Extensions in the GNOME Extensions app, then check again. The Knife will not enable your other extensions for you.",
200200 };
201201 if (info.state?.value === 1)
202202 return {
203203 status: "enabled",
204 message: "The T3 Code extension is running. Active-window snapshots are available.",
204 message: "The Knife extension is running. Active-window snapshots are available.",
205205 };
206206 if (info.state?.value === 3 || info.state?.value === 4)
207207 return {
......@@ -213,7 +213,7 @@ export class GnomeCaptureSetup {
213213 return {
214214 status: "disabled",
215215 message:
216 "Enable the T3 Code extension to allow active-window snapshots. You can disable it here at any time.",
216 "Enable The Knife’s extension to allow active-window snapshots. You can disable it here at any time.",
217217 };
218218 } catch (error) {
219219 return {
apps/desktop/src/snapShot/HyprlandSnapShot.ts+1-1
......@@ -96,7 +96,7 @@ export class HyprlandCaptureSetup {
9696 const bundle = await regularFile(this.paths.bundle);
9797 if (!bundle)
9898 throw new Error(
99 "The Hyprland capture helper is missing from this build. Update or reinstall T3 Code.",
99 "The Hyprland capture helper is missing from this build. Update or reinstall The Knife.",
100100 );
101101 if (!installed.equals(bundle))
102102 return {
apps/desktop/src/snapShot/KdeSnapShot.ts+3-3
......@@ -33,7 +33,7 @@ export function kdeCaptureDesktopEntry(executable: string): string {
3333 return [
3434 "[Desktop Entry]",
3535 "Type=Application",
36 "Name=T3 Code SnapShots",
36 "Name=The Knife SnapShots",
3737 "NoDisplay=true",
3838 `Exec=${escapeDesktopEntryExecArgument(executable)} check`,
3939 // KService reads this custom property as a KConfig list, not an XDG ';' list.
......@@ -116,7 +116,7 @@ export class KdeCaptureSetup {
116116 if (!bundle)
117117 return {
118118 status: "error",
119 message: "The capture helper is missing from this build. Update or reinstall T3 Code.",
119 message: "The capture helper is missing from this build. Update or reinstall The Knife.",
120120 };
121121 if (!installed.equals(bundle) || entry.toString() !== kdeCaptureDesktopEntry(executable))
122122 return {
......@@ -162,7 +162,7 @@ export class KdeCaptureSetup {
162162 const bundle = await regularFile(this.paths.bundle);
163163 if (!bundle)
164164 throw new Error(
165 "The capture helper is missing from this build. Update or reinstall T3 Code.",
165 "The capture helper is missing from this build. Update or reinstall The Knife.",
166166 );
167167 await NodeFSP.mkdir(directory, { recursive: true });
168168 await NodeFSP.mkdir(NodePath.dirname(desktop), { recursive: true });
apps/desktop/src/snapShot/LinuxSnapShot.ts+1-1
......@@ -306,7 +306,7 @@ export class LinuxCaptureConnection {
306306 this.bus.requestName(`${appId}.SnapShot`, NameFlag.DO_NOT_QUEUE),
307307 );
308308 if (result !== RequestNameReply.PRIMARY_OWNER) {
309 throw new Error("Another T3 Code instance is capturing a window. Try again.");
309 throw new Error("Another The Knife instance is capturing a window. Try again.");
310310 }
311311 const withFeedback = this.feedbackAvailable && options !== undefined;
312312 const reply = await this.call({
apps/desktop/src/snapShot/NiriCaptureShortcut.ts+1-1
......@@ -55,7 +55,7 @@ export async function startNiriCaptureShortcut(
5555 }),
5656 ]);
5757 if (result !== RequestNameReply.PRIMARY_OWNER)
58 throw new Error("Another T3 Code instance already owns the capture shortcut.");
58 throw new Error("Another The Knife instance already owns the capture shortcut.");
5959 return close;
6060 } catch (error) {
6161 close();
apps/desktop/src/snapShot/NiriSnapShot.ts+1-1
......@@ -175,7 +175,7 @@ async function activateNiriWindow(path: string, title: string, signal: AbortSign
175175 (window) => window.pid === process.pid && window.title === title,
176176 );
177177 if (matches.length > 1)
178 throw new Error("More than one T3 Code window matches the capture destination.");
178 throw new Error("More than one The Knife window matches the capture destination.");
179179 return matches[0];
180180 });
181181 connection.send("EventStream");
apps/desktop/src/snapShot/PortalCaptureShortcut.test.ts+1-1
......@@ -318,7 +318,7 @@ it("guides users to manual desktop settings when the portal cannot open them", a
318318 expect(client.hasSession).toBe(true);
319319 expect(client.state.shortcutCanRetry).toBe(false);
320320 expect(client.state.shortcutMessage).toBe(
321 "Shortcut permission wasn't granted. Allow T3 Code in your desktop's shortcut settings.",
321 "Shortcut permission wasn't granted. Allow The Knife in your desktop's shortcut settings.",
322322 );
323323 await expect(client.configure()).rejects.toThrow("Open your desktop's shortcut settings");
324324 expect(bus.calls.some((message) => message.member === "ConfigureShortcuts")).toBe(false);
apps/desktop/src/snapShot/PortalCaptureShortcut.ts+4-4
......@@ -135,7 +135,7 @@ export class PortalCaptureShortcut {
135135 throw new Error("Change the capture binding in your Hyprland config, then save it.");
136136 if (!this.hasSession || this.version < 2)
137137 throw new Error(
138 "Open your desktop's shortcut settings and allow T3 Code's capture shortcut.",
138 "Open your desktop's shortcut settings and allow The Knife's capture shortcut.",
139139 );
140140 await this.call({
141141 destination: this.owner,
......@@ -164,7 +164,7 @@ export class PortalCaptureShortcut {
164164 // This failed session is closing, so retry can register a fresh one.
165165 shortcutCanRetry: !this.managedByHyprland,
166166 shortcutMessage: this.managedByHyprland
167 ? "Couldn't connect to Hyprland shortcuts. Make sure xdg-desktop-portal-hyprland is running, then restart T3 Code."
167 ? "Couldn't connect to Hyprland shortcuts. Make sure xdg-desktop-portal-hyprland is running, then restart The Knife."
168168 : error instanceof Error
169169 ? error.message
170170 : "Could not register the capture shortcut.",
......@@ -304,7 +304,7 @@ export class PortalCaptureShortcut {
304304 shortcutMessage:
305305 this.version >= 2
306306 ? "Shortcut permission wasn't granted. Open shortcut permissions to allow it."
307 : "Shortcut permission wasn't granted. Allow T3 Code in your desktop's shortcut settings.",
307 : "Shortcut permission wasn't granted. Allow The Knife in your desktop's shortcut settings.",
308308 });
309309 return undefined;
310310 }
......@@ -327,7 +327,7 @@ export class PortalCaptureShortcut {
327327 shortcutPending: false,
328328 shortcutMessage: shortcut
329329 ? "Managed by Hyprland. Add the binding to your config and save it."
330 : "Hyprland did not register the capture action. Check that xdg-desktop-portal-hyprland is running, then restart T3 Code.",
330 : "Hyprland did not register the capture action. Check that xdg-desktop-portal-hyprland is running, then restart The Knife.",
331331 });
332332 return;
333333 }
apps/desktop/src/snapShot/SnapShotTransition.ts+1-1
......@@ -114,7 +114,7 @@ function createWindow(
114114 resizable: false,
115115 show: false,
116116 skipTaskbar: true,
117 title: "T3 Code Snapshot Animation",
117 title: "The Knife Snapshot Animation",
118118 transparent: true,
119119 webPreferences: {
120120 backgroundThrottling: false,
apps/desktop/src/snapShot/snapShot.ts+1-1
......@@ -447,7 +447,7 @@ export function hideAndWaitForBlur(window: {
447447 return new Promise((resolve, reject) => {
448448 const timeout = setTimeout(() => {
449449 window.removeListener("blur", onBlur);
450 reject(new Error("Timed out waiting for T3 Code to lose focus."));
450 reject(new Error("Timed out waiting for The Knife to lose focus."));
451451 }, WINDOW_BLUR_TIMEOUT_MS);
452452 const onBlur = () => {
453453 clearTimeout(timeout);
apps/desktop/src/ssh/DesktopSshEnvironment.test.ts+1-1
......@@ -30,7 +30,7 @@ describe("sshEnvironment", () => {
3030 assert.equal(cause.message, "Failed to present SSH password prompt for devbox.");
3131 assert.equal(
3232 DesktopSshEnvironment.toSshPasswordPromptError(cause).message,
33 "T3 Code window is not available for SSH authentication.",
33 "The Knife window is not available for SSH authentication.",
3434 );
3535 });
3636
apps/desktop/src/ssh/DesktopSshEnvironment.ts+1-1
......@@ -97,7 +97,7 @@ export function toSshPasswordPromptError(
9797 break;
9898 case "DesktopSshPromptWindowUnavailableError":
9999 case "DesktopSshPromptPresentationError":
100 message = "T3 Code window is not available for SSH authentication.";
100 message = "The Knife window is not available for SSH authentication.";
101101 break;
102102 case "DesktopSshPromptTimedOutError":
103103 message = `SSH authentication timed out for ${cause.destination}.`;
apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts+1-1
......@@ -63,7 +63,7 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedError<D
6363) {
6464 override get message(): string {
6565 const request = this.requestId === null ? "before a request id was assigned" : this.requestId;
66 return `T3 Code window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`;
66 return `The Knife window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`;
6767 }
6868}
6969
apps/desktop/src/window/DesktopApplicationMenu.test.ts+5-5
......@@ -29,7 +29,7 @@ const environmentInput = {
2929
3030const layerElectronApp = Layer.succeed(ElectronApp.ElectronApp, {
3131 metadata: Effect.die("unexpected metadata read"),
32 name: Effect.succeed("T3 Code"),
32 name: Effect.succeed("The Knife"),
3333 systemLocale: Effect.succeed("en-US"),
3434 whenReady: Effect.void,
3535 quit: Effect.void,
......@@ -142,21 +142,21 @@ describe("DesktopApplicationMenu", () => {
142142 const template = yield* Deferred.await(applicationMenuTemplate);
143143 const applicationMenu = template[0];
144144 assert.isDefined(applicationMenu);
145 assert.equal(applicationMenu.label, "T3 Code (Nightly)");
145 assert.equal(applicationMenu.label, "The Knife (Nightly)");
146146 if (!Array.isArray(applicationMenu.submenu)) {
147147 throw new Error("Expected application menu submenu to be an array.");
148148 }
149149 assert.equal(
150150 applicationMenu.submenu.find((item) => item.role === "about")?.label,
151 "About T3 Code (Nightly)",
151 "About The Knife (Nightly)",
152152 );
153153 assert.equal(
154154 applicationMenu.submenu.find((item) => item.role === "hide")?.label,
155 "Hide T3 Code (Nightly)",
155 "Hide The Knife (Nightly)",
156156 );
157157 assert.equal(
158158 applicationMenu.submenu.find((item) => item.role === "quit")?.label,
159 "Quit T3 Code (Nightly)",
159 "Quit The Knife (Nightly)",
160160 );
161161 }),
162162 );
apps/desktop/src/window/DesktopApplicationMenu.ts+1-1
......@@ -74,7 +74,7 @@ const checkForUpdatesFromMenu = Effect.gen(function* () {
7474 yield* electronDialog.showMessageBox({
7575 type: "info",
7676 title: "You're up to date!",
77 message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`,
77 message: `The Knife ${updateState.currentVersion} is currently the newest version available.`,
7878 buttons: ["OK"],
7979 });
8080 } else if (updateState.status === "error") {
apps/desktop/src/wsl/DesktopWslBackend.test.ts+1-1
......@@ -172,7 +172,7 @@ describe("DesktopWslBackend", () => {
172172
173173 yield* recordOwnershipFailure();
174174 const refusal = yield* backend.lastPreflightError;
175 assert.include(Option.getOrThrow(refusal), "Another T3 Code server");
175 assert.include(Option.getOrThrow(refusal), "Another The Knife server");
176176 yield* backend.reconcile;
177177 assert.equal(startCount, 1);
178178 assert.deepEqual(yield* backend.lastPreflightError, refusal);
apps/desktop/src/wsl/DesktopWslBackend.ts+1-1
......@@ -177,7 +177,7 @@ export const layer = Layer.effect(
177177 Ref.set(
178178 preflightErrorRef,
179179 Option.some(
180 "Another T3 Code server is using this distro's T3 home. Stop it, or pair with it instead.",
180 "Another The Knife server is using this distro's T3 home. Stop it, or pair with it instead.",
181181 ),
182182 ),
183183 ),
apps/desktop/src/wsl/DesktopWslEnvironment.ts+1-1
......@@ -505,7 +505,7 @@ const NODE_PTY_BINARY_MISSING_EXIT_CODE = 4;
505505
506506const formatNodePtyProbeFailureReason = (exitCode: number): string | null =>
507507 exitCode === NODE_PTY_BINARY_MISSING_EXIT_CODE
508 ? "WSL support is missing from this T3 Code build: the packaged Linux node-pty binary was not included. Install a build that includes WSL support."
508 ? "WSL support is missing from this build: the packaged Linux node-pty binary was not included. Install a build that includes WSL support."
509509 : null;
510510
511511// Captures the login-shell PATH as `resolvedPath:` so the launch can forward the
apps/mobile/README.md+5-5
......@@ -1,7 +1,7 @@
1# T3 Code Mobile
1# The Knife Mobile
22
33> [!WARNING]
4> T3 Code Mobile is currently in development and is not distributed yet. If you want to try it out, you can build it from source.
4> The Knife Mobile is currently in development and is not distributed yet. If you want to try it out, you can build it from source.
55
66## Quickstart
77
......@@ -10,9 +10,9 @@
1010
1111This app has three variants:
1212
13- `development`: Expo dev client, installable side-by-side as `T3 Code Dev`
14- `preview`: persistent internal preview build, installable side-by-side as `T3 Code Preview`
15- `production`: store/release build as `T3 Code`
13- `development`: Expo dev client, installable side-by-side as `The Knife Dev`
14- `preview`: persistent internal preview build, installable side-by-side as `The Knife Preview`
15- `production`: store/release build as `The Knife`
1616
1717Run commands from `apps/mobile`.
1818
apps/mobile/app.config.ts+27-29
......@@ -19,8 +19,8 @@ const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
1919
2020const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;
2121// Android layers are rendered by scripts/export-android-icons.ts from the Icon Composer sources.
22// The wordmark sits inside the adaptive safe zone; the variant artwork is a full-bleed background.
23const androidAdaptiveForeground = "./assets/android-icon-foreground.png";
22// The knife sits inside the adaptive icon safe zone.
23const androidAdaptiveForeground = "../../assets/knife/android-foreground.png";
2424
2525if (
2626 isIosPersonalTeamBuild &&
......@@ -37,11 +37,11 @@ const DEVELOPMENT_ASSETS = {
3737 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject),
3838 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),
3939 androidAdaptiveForeground,
40 androidAdaptiveBackgroundColor: "#347FF8",
41 androidAdaptiveBackgroundImage: "./assets/android-icon-background-dev.png",
42 androidSplashIcon: "./assets/android-splash-icon-dev.png",
43 androidMonochromeIcon: "./assets/android-icon-mark.png",
44 androidNotificationIcon: "./assets/android-notification-icon.png",
40 androidAdaptiveBackgroundColor: "#171422",
41 androidAdaptiveBackgroundImage: undefined,
42 androidSplashIcon: "../../assets/knife/android-splash.png",
43 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
44 androidNotificationIcon: "../../assets/knife/android-foreground.png",
4545 androidNotificationColor: "#00639B",
4646} as const;
4747
......@@ -50,11 +50,11 @@ const PREVIEW_ASSETS = {
5050 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject),
5151 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),
5252 androidAdaptiveForeground,
53 androidAdaptiveBackgroundColor: "#111533",
54 androidAdaptiveBackgroundImage: "./assets/android-icon-background-nightly.png",
55 androidSplashIcon: "./assets/android-splash-icon-nightly.png",
56 androidMonochromeIcon: "./assets/android-icon-mark.png",
57 androidNotificationIcon: "./assets/android-notification-icon.png",
53 androidAdaptiveBackgroundColor: "#171422",
54 androidAdaptiveBackgroundImage: undefined,
55 androidSplashIcon: "../../assets/knife/android-splash.png",
56 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
57 androidNotificationIcon: "../../assets/knife/android-foreground.png",
5858 androidNotificationColor: "#7565C7",
5959} as const;
6060
......@@ -63,17 +63,17 @@ const RELEASE_ASSETS = {
6363 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject),
6464 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),
6565 androidAdaptiveForeground,
66 androidAdaptiveBackgroundColor: "#000000",
66 androidAdaptiveBackgroundColor: "#171422",
6767 androidAdaptiveBackgroundImage: undefined,
68 androidSplashIcon: "./assets/android-splash-icon-prod.png",
69 androidMonochromeIcon: "./assets/android-icon-mark.png",
70 androidNotificationIcon: "./assets/android-notification-icon.png",
68 androidSplashIcon: "../../assets/knife/android-splash.png",
69 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
70 androidNotificationIcon: "../../assets/knife/android-foreground.png",
7171 androidNotificationColor: "#FFFFFF",
7272} as const;
7373
7474const VARIANT_CONFIG = {
7575 development: {
76 appName: "T3 Code Dev",
76 appName: "The Knife Dev",
7777 scheme: "t3code-dev",
7878 iosBundleIdentifier: "com.t3tools.t3code.dev",
7979 androidPackage: "com.t3tools.t3code.dev",
......@@ -81,7 +81,7 @@ const VARIANT_CONFIG = {
8181 assets: DEVELOPMENT_ASSETS,
8282 },
8383 preview: {
84 appName: "T3 Code Preview",
84 appName: "The Knife Preview",
8585 scheme: "t3code-preview",
8686 iosBundleIdentifier: "com.t3tools.t3code.preview",
8787 androidPackage: "com.t3tools.t3code.preview",
......@@ -89,7 +89,7 @@ const VARIANT_CONFIG = {
8989 assets: PREVIEW_ASSETS,
9090 },
9191 production: {
92 appName: "T3 Code",
92 appName: "The Knife",
9393 scheme: "t3code",
9494 iosBundleIdentifier: "com.t3tools.t3code",
9595 androidPackage: "com.t3tools.t3code",
......@@ -134,7 +134,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
134134 {
135135 name: "SubscriptionUsage",
136136 displayName: "Subscription usage",
137 description: "Subscription quotas from your connected T3 Code environments.",
137 description: "Subscription quotas from your connected environments.",
138138 ios: {
139139 configuration: {
140140 title: "Subscription usage",
......@@ -185,7 +185,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
185185 {
186186 name: "AgentActivity",
187187 displayName: "Agent Activity",
188 description: "Shows the current state of active T3 Code agents.",
188 description: "Shows the current state of your active agents.",
189189 // Live Activity companion; there is no Android presentation for it.
190190 android: null,
191191 ios: { supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"] },
......@@ -261,7 +261,7 @@ const config: ExpoConfig = {
261261 `webcredentials:${variant.relyingParty}`,
262262 ],
263263 entitlements: {
264 "keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],
264 "keychain-access-groups": [`$(AppIdentifierPrefix)${iosBundleIdentifier}`],
265265 },
266266 infoPlist: {
267267 UISupportedInterfaceOrientations: [
......@@ -273,8 +273,8 @@ const config: ExpoConfig = {
273273 NSAllowsArbitraryLoads: true,
274274 },
275275 NSLocalNetworkUsageDescription:
276 "Allow T3 Code to connect to T3 Code servers on your local network or tailnet.",
277 NSPhotoLibraryAddUsageDescription: "Allow T3 Code to save images to your photo library.",
276 "Allow The Knife to connect to The Knife servers on your local network or tailnet.",
277 NSPhotoLibraryAddUsageDescription: "Allow The Knife to save images to your photo library.",
278278 // "Audio, AirPlay, and Picture in Picture": the browser screen's system
279279 // picture in picture needs it to start and to stay up outside the app.
280280 UIBackgroundModes: ["audio"],
......@@ -321,6 +321,7 @@ const config: ExpoConfig = {
321321 favicon: variant.assets.appIcon,
322322 },
323323 plugins: [
324 ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
324325 "expo-asset",
325326 [
326327 "expo-font",
......@@ -359,8 +360,6 @@ const config: ExpoConfig = {
359360 mode: APP_VARIANT === "development" ? "development" : "production",
360361 },
361362 ],
362 // appleSignIn must be gated here: withoutIosPersonalTeamCapabilities.cjs runs before
363 // plugins earlier in this array, so it cannot strip the entitlement Clerk would add.
364363 ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],
365364 "expo-web-browser",
366365 [
......@@ -382,7 +381,7 @@ const config: ExpoConfig = {
382381 [
383382 "expo-audio",
384383 {
385 microphonePermission: "Allow T3 Code to use your microphone for voice input.",
384 microphonePermission: "Allow The Knife to use your microphone for voice input.",
386385 recordAudioAndroid: false,
387386 enableBackgroundPlayback: false,
388387 enableBackgroundRecording: false,
......@@ -391,7 +390,7 @@ const config: ExpoConfig = {
391390 [
392391 "expo-camera",
393392 {
394 cameraPermission: "Allow T3 Code to access your camera so you can scan pairing QR codes.",
393 cameraPermission: "Allow The Knife to access your camera so you can scan pairing QR codes.",
395394 microphonePermission: false,
396395 barcodeScannerEnabled: true,
397396 recordAudioAndroid: false,
......@@ -466,7 +465,6 @@ const config: ExpoConfig = {
466465 "./plugins/withAndroidModernAlertDialog.cjs",
467466 "./plugins/withAndroidPredictiveBackCompat.cjs",
468467 "./plugins/withAndroidTabletOrientation.cjs",
469 ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
470468 ],
471469 extra: {
472470 appVariant: APP_VARIANT,
apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt+3-3
......@@ -18,7 +18,7 @@ internal object T3ComposerClipboard {
1818 }
1919 val records = payload?.optJSONArray("records")
2020 if (payload == null || records == null) {
21 clipboard.setPrimaryClip(ClipData.newPlainText("T3 Code", text))
21 clipboard.setPrimaryClip(ClipData.newPlainText("The Knife", text))
2222 return
2323 }
2424 val all = (0 until records.length()).map { records.getJSONObject(it) }
......@@ -36,12 +36,12 @@ internal object T3ComposerClipboard {
3636 clipboard.setPrimaryClip(
3737 if (selected.isEmpty()) {
3838 ClipData.newPlainText(
39 "T3 Code",
39 "The Knife",
4040 text
4141 )
4242 } else {
4343 ClipData.newHtmlText(
44 "T3 Code",
44 "The Knife",
4545 text,
4646 "<pre data-t3-context-fragment=\"$encoded\">$escaped</pre>"
4747 )
apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift+2-2
......@@ -14,7 +14,7 @@ final class T3LayoutMetricsView: ExpoView {
1414 registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { (view: T3LayoutMetricsView, _: UITraitCollection) in
1515 view.publishMetrics()
1616 }
17 #if compiler(>=6.4)
17 #if T3_IOS_27_1_SDK
1818 if #available(iOS 27.1, *) {
1919 hasHinge = nil
2020 addInteraction(UIHingeInteraction { [weak self] _, update in
......@@ -67,7 +67,7 @@ final class T3LayoutMetricsView: ExpoView {
6767 }
6868 var verticalBarEdge = "none"
6969 var regions: [[String: Any]] = []
70 #if compiler(>=6.4)
70 #if T3_IOS_27_1_SDK
7171 if #available(iOS 27.1, *) {
7272 let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft
7373 switch traitCollection.verticalBarEdge {
apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec+6
......@@ -15,5 +15,11 @@ Pod::Spec.new do |s|
1515 s.pod_target_xcconfig = {
1616 'DEFINES_MODULE' => 'YES',
1717 }
18
19 ios_sdk_version = Gem::Version.new(`xcrun --sdk iphoneos --show-sdk-version`.strip)
20 if ios_sdk_version >= Gem::Version.new('27.1')
21 s.pod_target_xcconfig['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = '$(inherited) T3_IOS_27_1_SDK'
22 end
23
1824 s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'
1925end
apps/mobile/plugins/withWidgetLogoAsset.cjs+10-8
......@@ -1,9 +1,9 @@
11"use strict";
22
3// Ships the branded T3 mark to the Live Activity / widget extension.
3// Ships the knife silhouette to the Live Activity / widget extension.
44//
55// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and
6// has no asset support, so this plugin (a) writes an SVG template image set into
6// has no asset support, so this plugin (a) writes a template image set into
77// the generated widget asset catalog and (b) wires that catalog into the widget
88// target with an actool build phase.
99//
......@@ -22,17 +22,16 @@ const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs");
2222
2323const TARGET_NAME = "ExpoWidgetsTarget";
2424const CATALOG_NAME = "Assets.xcassets";
25const IMAGE_SET = "T3Mark.imageset";
26const SVG_NAME = "T3Mark.svg";
25const IMAGE_SET = "KnifeMark.imageset";
26const IMAGE_NAME = "knife.png";
2727
2828const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n";
2929const IMAGE_SET_CONTENTS =
3030 JSON.stringify(
3131 {
32 images: [{ idiom: "universal", filename: SVG_NAME }],
32 images: [{ idiom: "universal", filename: IMAGE_NAME }],
3333 info: { author: "expo", version: 1 },
3434 properties: {
35 "preserves-vector-representation": true,
3635 "template-rendering-intent": "template",
3736 },
3837 },
......@@ -44,13 +43,16 @@ function withAssetFiles(config) {
4443 return withDangerousMod(config, [
4544 "ios",
4645 (cfg) => {
47 const source = path.join(cfg.modRequest.projectRoot, "assets", "widget", SVG_NAME);
46 const source = path.resolve(
47 cfg.modRequest.projectRoot,
48 "../../assets/knife/android-foreground.png",
49 );
4850 const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME);
4951 const imageSetDir = path.join(catalogDir, IMAGE_SET);
5052 fs.mkdirSync(imageSetDir, { recursive: true });
5153 fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS);
5254 fs.writeFileSync(path.join(imageSetDir, "Contents.json"), IMAGE_SET_CONTENTS);
53 fs.copyFileSync(source, path.join(imageSetDir, SVG_NAME));
55 fs.copyFileSync(source, path.join(imageSetDir, IMAGE_NAME));
5456 return cfg;
5557 },
5658 ]);
apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs+1
......@@ -4,6 +4,7 @@ module.exports = function withoutIosPersonalTeamCapabilities(config) {
44 return withEntitlementsPlist(config, (modConfig) => {
55 delete modConfig.modResults["aps-environment"];
66 delete modConfig.modResults["com.apple.developer.applesignin"];
7 delete modConfig.modResults["com.apple.developer.associated-domains"];
78 delete modConfig.modResults["com.apple.security.application-groups"];
89 return modConfig;
910 });
apps/mobile/src/Stack.tsx+1-1
......@@ -280,7 +280,7 @@ const SettingsContentStack = createV5SheetStackNavigator({
280280 SettingsAbout: createNativeStackScreen({
281281 screen: SettingsAboutRouteScreen,
282282 linking: "about",
283 options: { title: "About T3 Code" },
283 options: { title: "About The Knife" },
284284 }),
285285 SettingsEnvironmentNew: createNativeStackScreen({
286286 screen: ConnectionsNewRouteScreen,
apps/mobile/src/components/BrandMark.tsx+3-4
......@@ -3,7 +3,7 @@ import { Image } from "expo-image";
33import { View } from "react-native";
44
55import { AppText as Text } from "./AppText";
6import { T3_CODE_BRAND_MARK_SOURCE } from "./brandAssets";
6import { KNIFE_BRAND_MARK_SOURCE } from "./brandAssets";
77
88const appVariant = Constants.expoConfig?.extra?.appVariant;
99const DEFAULT_STAGE_LABEL =
......@@ -17,17 +17,16 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab
1717 return (
1818 <View className="flex-row items-center gap-3">
1919 <Image
20 source={T3_CODE_BRAND_MARK_SOURCE}
20 source={KNIFE_BRAND_MARK_SOURCE}
2121 accessibilityIgnoresInvertColors
2222 style={{
2323 width: iconSize,
2424 height: iconSize,
25 borderRadius: compact ? 10 : 14,
2625 }}
2726 />
2827 <View className="gap-1">
2928 <View className="flex-row items-center gap-2">
30 <Text className="text-lg font-t3-bold tracking-[-0.4px] text-foreground">T3 Code</Text>
29 <Text className="text-lg font-t3-bold tracking-[-0.4px] text-foreground">The Knife</Text>
3130 <View className="rounded-full bg-subtle px-2 py-1">
3231 <Text className="text-3xs font-t3-bold tracking-[1.1px] uppercase text-foreground-muted">
3332 {stageLabel}
apps/mobile/src/components/CompactBrandTitle.tsx+4-4
......@@ -4,7 +4,7 @@ import type { NativeStackNavigationOptions } from "@react-navigation/native-stac
44import { Platform, View } from "react-native";
55
66import { AppText as Text } from "./AppText";
7import { T3Wordmark } from "./T3Wordmark";
7import { KnifeMark } from "./KnifeMark";
88import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
99import { resolveMobileStageLabel } from "../lib/mobileBranding";
1010import { useAndroidControlSizing } from "./useAndroidControlSizing";
......@@ -34,19 +34,19 @@ export function CompactBrandTitle(
3434 return (
3535 <View
3636 aria-level={1}
37 accessibilityLabel={props.accessibilityLabel ?? "T3 Code, Threads"}
37 accessibilityLabel={props.accessibilityLabel ?? "The Knife, Threads"}
3838 accessible
3939 role="heading"
4040 className="flex-row items-center gap-1.5"
4141 style={[{ marginLeft: titleOffset }, Platform.OS === "android" && { gap: 5.25 * scale }]}
4242 >
43 <T3Wordmark colorClassName="accent-icon" height={Math.round(15 * scale)} />
43 <KnifeMark height={Math.round(26 * scale)} />
4444 <Text
4545 allowFontScaling={props.allowFontScaling}
4646 className="font-t3-medium text-foreground-muted"
4747 style={{ fontSize: 21 * scale, letterSpacing: -0.5 * scale }}
4848 >
49 Code
49 The Knife
5050 </Text>
5151 <View
5252 className="rounded-full bg-subtle px-1.5 py-0.5"
apps/mobile/src/components/KnifeMark.tsx created+14
......@@ -0,0 +1,14 @@
1import { Image } from "expo-image";
2import { KNIFE_BRAND_MARK_SOURCE } from "./brandAssets";
3
4export function KnifeMark(props: { readonly height: number }) {
5 return (
6 <Image
7 source={KNIFE_BRAND_MARK_SOURCE}
8 accessibilityIgnoresInvertColors
9 accessible={false}
10 contentFit="contain"
11 style={{ height: props.height, width: props.height }}
12 />
13 );
14}
apps/mobile/src/components/T3Wordmark.tsx deleted-32
......@@ -1,32 +0,0 @@
1import type { ColorValue } from "react-native";
2import Svg, { Path } from "react-native-svg";
3import { withUniwind } from "uniwind";
4
5const ThemedPath = withUniwind(Path);
6
7/**
8 * The "T3" brand mark, matching the desktop sidebar's T3Wordmark SVG
9 * (apps/web Sidebar.tsx). Width derives from the viewBox aspect ratio.
10 */
11export function T3Wordmark(props: {
12 readonly height: number;
13 readonly color?: ColorValue;
14 readonly colorClassName?: string;
15}) {
16 const aspectRatio = 94.3941 / 56.96;
17 return (
18 <Svg
19 accessibilityLabel="T3"
20 height={props.height}
21 width={props.height * aspectRatio}
22 viewBox="15.5309 37 94.3941 56.96"
23 >
24 <ThemedPath
25 d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
26 color={props.color}
27 colorClassName={props.colorClassName}
28 fill="currentColor"
29 />
30 </Svg>
31 );
32}
apps/mobile/src/components/brandAssets.ts+1-10
......@@ -1,10 +1 @@
1import Constants from "expo-constants";
2
3const appVariant = Constants.expoConfig?.extra?.appVariant;
4
5export const T3_CODE_BRAND_MARK_SOURCE =
6 appVariant === "development"
7 ? require("../../../../assets/dev/blueprint-ios-1024.png")
8 : appVariant === "preview"
9 ? require("../../../../assets/nightly/nightly-ios-1024.png")
10 : require("../../../../assets/prod/black-ios-1024.png");
1export const KNIFE_BRAND_MARK_SOURCE = require("../../../../assets/knife/apple-touch-180.png");
apps/mobile/src/features/agent-awareness/remoteRegistration.ts+1-1
......@@ -534,7 +534,7 @@ function armAgentAwarenessLiveActivityForLocalWorkNow(input: {
534534 const nowIso = new Date(Date.now()).toISOString();
535535 const activity = startAgentLiveActivity(
536536 {
537 title: "T3 Code",
537 title: "The Knife",
538538 subtitle: "Agent work in progress",
539539 activeCount: 1,
540540 updatedAt: nowIso,
apps/mobile/src/features/diagnostics/crash-log-model.test.ts+2-2
......@@ -61,14 +61,14 @@ describe("formatStartupCrashReport", () => {
6161 version: "1.1.1",
6262 build: "56",
6363 });
64 expect(report.startsWith("T3 Code 1.1.1 (56)\n")).toBe(true);
64 expect(report.startsWith("The Knife 1.1.1 (56)\n")).toBe(true);
6565 expect(report).toContain("2026-09-13T05:35:52.000Z");
6666 expect(report).toContain("at NewTaskFlowProvider");
6767 });
6868
6969 it("says so when nothing was recorded", () => {
7070 expect(formatStartupCrashReport([], { version: "1.1.1", build: "56" })).toBe(
71 "T3 Code 1.1.1 (56)\nNo startup crashes recorded.",
71 "The Knife 1.1.1 (56)\nNo startup crashes recorded.",
7272 );
7373 });
7474});
apps/mobile/src/features/diagnostics/crash-log-model.ts+1-1
......@@ -72,7 +72,7 @@ export function formatStartupCrashReport(
7272 records: ReadonlyArray<StartupCrashRecord>,
7373 app: { readonly version: string; readonly build: string },
7474): string {
75 const header = `T3 Code ${app.version} (${app.build})`;
75 const header = `The Knife ${app.version} (${app.build})`;
7676 if (records.length === 0) return `${header}\nNo startup crashes recorded.`;
7777 return [
7878 header,
apps/mobile/src/features/pull-requests/MaterialPullRequestToolbar.android.tsx+1-1
......@@ -69,7 +69,7 @@ export function MaterialPullRequestToolbar(props: { readonly onRequestVisibility
6969 brand={
7070 <CompactBrandTitle
7171 allowFontScaling={false}
72 accessibilityLabel="T3 Code, Pull requests"
72 accessibilityLabel="The Knife, Pull requests"
7373 />
7474 }
7575 onPress={() =>
apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts+1-1
......@@ -23,7 +23,7 @@ export function pullRequestAgentPrompt(task: PullRequestAgentTask): string {
2323 case "conflicts":
2424 return "Resolve this pull request's conflicts with its base branch. First confirm this thread is on the PR's branch; if it isn't, prepare the PR in its own worktree. Bring it up to date using this repository's convention, preserve the intent of both sides, and run the relevant checks before pushing.";
2525 case "watch":
26 return "Watch this pull request for check results, new feedback, and conflicts using T3's pull request watch. End the turn once the watch is armed. When it wakes you, assess what changed and report what needs attention.";
26 return "Watch this pull request for check results, new feedback, and conflicts using The Knife's pull request watch. End the turn once the watch is armed. When it wakes you, assess what changed and report what needs attention.";
2727 }
2828}
2929
apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx+1-1
......@@ -20,7 +20,7 @@ export function SettingsAboutRouteScreen() {
2020 const insets = useSafeAreaInsets();
2121
2222 return (
23 <SettingsScreen title="About T3 Code">
23 <SettingsScreen title="About The Knife">
2424 <ScrollView
2525 contentInsetAdjustmentBehavior="automatic"
2626 showsVerticalScrollIndicator={false}
apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx+3-3
......@@ -113,7 +113,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
113113 return;
114114 Alert.alert(
115115 `Update ${environment?.environmentLabel ?? "environment"}?`,
116 `Install T3 Code ${targetVersion}. ${capabilities.serverSelfUpdate === "desktop-managed" ? "The desktop app will close and relaunch." : "The server will restart and reconnect."} Running threads may be interrupted.`,
116 `Install The Knife ${targetVersion}. ${capabilities.serverSelfUpdate === "desktop-managed" ? "The desktop app will close and relaunch." : "The server will restart and reconnect."} Running threads may be interrupted.`,
117117 [
118118 { text: "Cancel", style: "cancel" },
119119 {
......@@ -227,7 +227,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
227227 {notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null}
228228 {config ? (
229229 <>
230 <SettingsSection title="T3 Code">
230 <SettingsSection title="The Knife">
231231 <View className="gap-1 p-4">
232232 <Text className="text-base text-foreground">Version {version}</Text>
233233 {running ? (
......@@ -252,7 +252,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
252252 <Text className="text-sm text-foreground-muted">
253253 {capabilities?.serverSelfUpdate === "desktop-managed"
254254 ? "Update the desktop app on this machine."
255 : "Update and restart T3 Code on this machine."}
255 : "Update and restart The Knife on this machine."}
256256 </Text>
257257 ) : null}
258258 </View>
apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx+1-1
......@@ -166,7 +166,7 @@ export function SettingsMicrophoneRouteScreen() {
166166 <Text className="px-2 text-sm text-foreground-muted">
167167 {permission === "ask"
168168 ? "Voice input needs microphone access before it can record or list your microphones."
169 : "Microphone access is off for T3 Code. Turn it on in Settings to use voice input."}
169 : "Microphone access is off for The Knife. Turn it on in Settings to use voice input."}
170170 </Text>
171171 </>
172172 ) : null}
apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx+2-2
......@@ -338,7 +338,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {
338338
339339 Alert.alert(
340340 "Disable notifications",
341 "Open system Settings to disable notifications for T3 Code.",
341 "Open system Settings to disable notifications for The Knife.",
342342 [
343343 { text: "Cancel", style: "cancel" },
344344 { text: "Open Settings", onPress: () => void Linking.openSettings() },
......@@ -492,7 +492,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {
492492 void openAndroidLiveUpdateSettings().catch(() => {
493493 Alert.alert(
494494 "Couldn't open Settings",
495 "Open Android Settings, select T3 Code, then enable Live Updates in Notifications.",
495 "Open Android Settings, select The Knife, then enable Live Updates in Notifications.",
496496 );
497497 });
498498 }}
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+1-1
......@@ -215,7 +215,7 @@ function SettingsIndexSections() {
215215
216216 <SettingsSection title="App">
217217 <SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />
218 <SettingsRow icon="info.circle" label="About T3 Code" target="SettingsAbout" />
218 <SettingsRow icon="info.circle" label="About The Knife" target="SettingsAbout" />
219219 </SettingsSection>
220220 </>
221221 );
apps/mobile/src/features/showcase/showcaseAgentActivity.ts+1-1
......@@ -74,7 +74,7 @@ export function buildShowcaseAgentActivity(
7474 });
7575 }
7676 return {
77 title: "T3 Code",
77 title: "The Knife",
7878 subtitle: "Agent work in progress",
7979 activeCount: rows.filter((row) => ACTIVE_PHASES.has(row.phase)).length,
8080 updatedAt: new Date(now).toISOString(),
apps/mobile/src/features/threads/ThreadComposer.tsx+1-1
......@@ -636,7 +636,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
636636 armAgentAwarenessLiveActivityForLocalWork({
637637 environmentId: props.environmentId,
638638 threadTitle: props.selectedThread.title,
639 projectTitle: props.environmentLabel ?? "T3 Code",
639 projectTitle: props.environmentLabel ?? "The Knife",
640640 });
641641 } finally {
642642 inFlightThreadIdsRef.current.delete(threadKey);
apps/mobile/src/features/threads/ThreadFeed.tsx+1-1
......@@ -1200,7 +1200,7 @@ function renderFeedEntry(
12001200 </Text>
12011201 ) : presentation.attribution === "t3code" ? (
12021202 <Text className="mb-1 pr-1 font-t3-medium text-2xs text-foreground-muted opacity-60">
1203 Sent by T3 Code
1203 Sent by The Knife
12041204 </Text>
12051205 ) : presentation.attribution === "agent" ? (
12061206 <AgentMessageAttribution
apps/mobile/src/features/threads/thread-work-log.tsx+2-2
......@@ -57,7 +57,7 @@ import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity";
5757import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";
5858
5959import { AppText as Text } from "../../components/AppText";
60import { T3Wordmark } from "../../components/T3Wordmark";
60import { KnifeMark } from "../../components/KnifeMark";
6161import { BrowserAppIcon } from "../../components/BrowserAppIcon";
6262import { cn } from "../../lib/cn";
6363import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";
......@@ -131,7 +131,7 @@ function WorkLogIcon(props: {
131131 const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName;
132132 if (props.icon === "t3-code") {
133133 return (
134 <T3Wordmark height={10} {...(colorClassName ? { colorClassName } : { color: props.color })} />
134 <KnifeMark height={14} />
135135 );
136136 }
137137 if (props.icon === "computer") {
apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx+2-2
......@@ -42,7 +42,7 @@ function WorkspaceRootTitle(props: {
4242 <View style={{ width: props.maxWidth, minHeight: 32, justifyContent: "center" }}>
4343 <WorkspaceConnectionTitle
4444 brand={
45 <CompactBrandTitle accessibilityLabel={`T3 Code, ${TAB_TITLES[state.selectedTab]}`} />
45 <CompactBrandTitle accessibilityLabel={`The Knife, ${TAB_TITLES[state.selectedTab]}`} />
4646 }
4747 maxWidth={props.maxWidth}
4848 onPress={props.onOpenEnvironments}
......@@ -114,7 +114,7 @@ function WorkspaceRootContent() {
114114 />
115115 ) : (
116116 <AndroidScreenHeader
117 title="T3 Code"
117 title="The Knife"
118118 titleContent={renderHeaderTitle()}
119119 actions={[
120120 {
apps/mobile/src/lib/authClientMetadata.ts+1-1
......@@ -7,7 +7,7 @@ export function authClientMetadata(appVersion?: string): AuthClientPresentationM
77 const deviceModel = Device.modelName?.trim();
88
99 return {
10 label: "T3 Code Mobile",
10 label: "The Knife Mobile",
1111 deviceType:
1212 Device.deviceType === Device.DeviceType.TABLET
1313 ? "tablet"
apps/mobile/src/lib/connection.test.ts+1-1
......@@ -40,7 +40,7 @@ describe("mobile remote connection records", () => {
4040
4141 it("identifies mobile token exchanges for authorized-client presentation", () => {
4242 expect(authClientMetadata()).toEqual({
43 label: "T3 Code Mobile",
43 label: "The Knife Mobile",
4444 deviceType: "mobile",
4545 os: "iOS",
4646 osMajorVersion: 18,
apps/mobile/src/lib/mobileTheme.ts+1-1
......@@ -26,7 +26,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
2626 readonly id: MobileThemeId;
2727 readonly label: string;
2828}> = [
29 { id: DEFAULT_MOBILE_THEME_ID, label: "T3 Code" },
29 { id: DEFAULT_MOBILE_THEME_ID, label: "The Knife" },
3030 { id: "material-you", label: "Material You" },
3131 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
3232];
apps/mobile/src/widgets/AgentActivity.tsx+2-8
......@@ -244,15 +244,9 @@ export function AgentActivity(
244244 </HStack>
245245 );
246246
247 // The branded T3 mark. `assetName` resolves the template image set bundled in
248 // the widget extension's asset catalog. Image views only honor `resizable`
249 // directly (frame/foregroundStyle are dropped), so we size it via a container
250 // frame the resizable image fills and tint it through the container's
251 // foreground style, which the template image inherits. The 3:2 frame matches
252 // the glyph's aspect ratio so it never distorts.
253247 const renderLogo = (height: number, color: Foreground) => (
254 <HStack modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(color)]}>
255 <Image assetName="T3Mark" modifiers={[resizable()]} />
248 <HStack modifiers={[frame({ width: height, height }), foregroundStyle(color)]}>
249 <Image assetName="KnifeMark" modifiers={[resizable()]} />
256250 </HStack>
257251 );
258252
apps/mobile/src/widgets/SubscriptionUsage.android.tsx+17-5
......@@ -29,8 +29,20 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
2929 });
3030 const muted = colors.onSurfaceVariant;
3131 const providers = props.providers ?? [
32 { name: "Codex", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 },
33 { name: "Claude", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 },
32 {
33 name: "Codex",
34 detail: "Open The Knife to connect",
35 windows: [],
36 expiresAt: 0,
37 totalWindows: 0,
38 },
39 {
40 name: "Claude",
41 detail: "Open The Knife to connect",
42 windows: [],
43 expiresAt: 0,
44 totalWindows: 0,
45 },
3446 ];
3547 return (
3648 // The card is one Button so a tap reaches the app's interaction listener,
......@@ -60,7 +72,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
6072 </Text>
6173 {shown.length === 0 ? (
6274 <Text color={muted} maxLines={1} style={{ fontSize: 11 }}>
63 {stale ? "Open T3 to refresh" : provider.detail}
75 {stale ? "Open The Knife to refresh" : provider.detail}
6476 </Text>
6577 ) : null}
6678 {shown.map((window) => {
......@@ -90,7 +102,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
90102 })}
91103 {hidden > 0 ? (
92104 <Text color={muted} maxLines={1} style={{ fontSize: 10 }}>
93 {`${hidden} more in T3`}
105 {`${hidden} more in The Knife`}
94106 </Text>
95107 ) : null}
96108 </Column>
......@@ -104,7 +116,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
104116 >
105117 {props.checkedAt
106118 ? `As of ${new Date(props.checkedAt).toLocaleString(undefined, { hour: "numeric", minute: "2-digit", month: "short", day: "numeric" })}`
107 : "Tap to connect in T3"}
119 : "Tap to connect in The Knife"}
108120 </Text>
109121 </Column>
110122 </Button>
apps/mobile/src/widgets/SubscriptionUsage.tsx+6-6
......@@ -39,8 +39,8 @@ function SubscriptionUsage(
3939 const monochrome =
4040 environment.widgetRenderingMode !== "fullColor" || environment.isLuminanceReduced;
4141 const providers = props.providers ?? [
42 { name: "Codex", detail: "Open T3 to connect", windows: [], expiresAt: 0 },
43 { name: "Claude", detail: "Open T3 to connect", windows: [], expiresAt: 0 },
42 { name: "Codex", detail: "Open The Knife to connect", windows: [], expiresAt: 0 },
43 { name: "Claude", detail: "Open The Knife to connect", windows: [], expiresAt: 0 },
4444 ];
4545 const columns = providers.map((provider) => {
4646 const stale = provider.windows.length > 0 && now >= provider.expiresAt;
......@@ -73,7 +73,7 @@ function SubscriptionUsage(
7373 ].slice(0, limit)
7474 : windows.slice(0, limit);
7575 const detail = stale
76 ? "Open T3 to refresh"
76 ? "Open The Knife to refresh"
7777 : period !== "auto" && windows.length === 0 && provider.windows.length > 0
7878 ? `No ${period} limit reported`
7979 : provider.detail;
......@@ -121,7 +121,7 @@ function SubscriptionUsage(
121121 ? `${tightest.remaining}% left`
122122 : period !== "auto" && !stale && provider.windows.length > 0
123123 ? "N/A"
124 : "Open T3"}
124 : "Open The Knife"}
125125 </Text>
126126 </HStack>
127127 {tightest ? (
......@@ -223,7 +223,7 @@ function SubscriptionUsage(
223223 >
224224 {(period === "auto" ? (provider.totalWindows ?? windows.length) : windows.length) -
225225 limit}{" "}
226 more in T3
226 more in The Knife
227227 </Text>
228228 ) : null}
229229 </VStack>
......@@ -255,7 +255,7 @@ function SubscriptionUsage(
255255 >
256256 {props.checkedAt
257257 ? `As of ${new Date(props.checkedAt).toLocaleString(undefined, { hour: "numeric", minute: "2-digit", month: "short", day: "numeric" })}`
258 : "Tap to connect in T3"}
258 : "Tap to connect in The Knife"}
259259 </Text>
260260 ) : null}
261261 </VStack>
apps/mobile/src/widgets/publishSubscriptionUsage.android.ts+1-2
......@@ -22,8 +22,7 @@ export async function publishSubscriptionUsage(snapshot: SubscriptionUsageSnapsh
2222 });
2323 }
2424 widget.updateSnapshot(snapshot);
25 // Android has no timeline; an alarm re-renders the stored snapshot at each
26 // deadline so stale readings flip to "Open T3 to refresh" unattended.
25 // Android has no timeline; an alarm re-renders the stored snapshot at each deadline.
2726 requireOptionalNativeModule<{ schedule: (name: string, deadlines: number[]) => void }>(
2827 "T3WidgetExpiry",
2928 )?.schedule(
apps/mobile/src/widgets/subscriptionUsageSnapshot.ts+2-2
......@@ -94,7 +94,7 @@ function subscriptionUsageProps(
9494 return {
9595 name,
9696 detail: !fresh
97 ? "Open T3 to refresh"
97 ? "Open The Knife to refresh"
9898 : pool.accounts.length > 1
9999 ? `${pool.accounts.length} accounts · pooled`
100100 : "Subscription remaining",
......@@ -161,7 +161,7 @@ export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, n
161161 ...snapshot,
162162 providers: snapshot.providers.map((provider) =>
163163 provider.windows.length > 0 && provider.expiresAt <= date
164 ? { ...provider, detail: "Open T3 to refresh", windows: [], totalWindows: 0 }
164 ? { ...provider, detail: "Open The Knife to refresh", windows: [], totalWindows: 0 }
165165 : provider,
166166 ),
167167 },
apps/web/index.html+3-3
......@@ -477,13 +477,13 @@
477477 cursor: pointer;
478478 }
479479 </style>
480 <title>T3 Code (Alpha)</title>
480 <title>The Knife</title>
481481 </head>
482482 <body>
483483 <div id="root">
484484 <div id="boot-shell">
485 <div id="boot-shell-card" aria-label="T3 Code splash screen">
486 <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" />
485 <div id="boot-shell-card" aria-label="The Knife splash screen">
486 <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="The Knife" />
487487 </div>
488488 </div>
489489 </div>
apps/web/public/apple-touch-icon.png
Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ
apps/web/public/favicon-16x16.png
Binary files a/apps/web/public/favicon-16x16.png and b/apps/web/public/favicon-16x16.png differ
apps/web/public/favicon-32x32.png
Binary files a/apps/web/public/favicon-32x32.png and b/apps/web/public/favicon-32x32.png differ
apps/web/public/favicon.ico
Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ
apps/web/public/manifest.webmanifest+2
......@@ -1,4 +1,6 @@
11{
2 "name": "The Knife",
3 "short_name": "The Knife",
24 "id": "/",
35 "start_url": "/",
46 "scope": "/",
apps/web/src/bootstrap.test.ts+2-2
......@@ -64,7 +64,7 @@ describe("app startup failures", () => {
6464 await import("./bootstrap");
6565 await vi.dynamicImportSettled();
6666
67 expect(bootShell?.text).toContain("T3 Code could not load.");
67 expect(bootShell?.text).toContain("The Knife could not load.");
6868 const reloadButton = bootShell?.children[0]?.children.find(
6969 (element) => element.tagName === "button",
7070 );
......@@ -78,7 +78,7 @@ describe("app startup failures", () => {
7878
7979 showBootError(new Error("internal module path"));
8080
81 expect(bootShell?.text).toContain("T3 Code could not load.");
81 expect(bootShell?.text).toContain("The Knife could not load.");
8282 expect(bootShell?.text.includes("internal module path")).toBe(dev);
8383 });
8484
apps/web/src/branding.test.ts+15-15
......@@ -24,9 +24,9 @@ describe("branding", () => {
2424 value: {
2525 desktopBridge: {
2626 getAppBranding: () => ({
27 baseName: "T3 Code",
27 baseName: "The Knife",
2828 stageLabel: "Nightly",
29 displayName: "T3 Code (Nightly)",
29 displayName: "The Knife (Nightly)",
3030 }),
3131 },
3232 },
......@@ -34,9 +34,9 @@ describe("branding", () => {
3434
3535 const branding = await import("./branding");
3636
37 expect(branding.APP_BASE_NAME).toBe("T3 Code");
37 expect(branding.APP_BASE_NAME).toBe("The Knife");
3838 expect(branding.APP_STAGE_LABEL).toBe("Nightly");
39 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
39 expect(branding.APP_DISPLAY_NAME).toBe("The Knife (Nightly)");
4040 });
4141
4242 it("normalizes hosted app channel metadata", async () => {
......@@ -47,7 +47,7 @@ describe("branding", () => {
4747 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");
4848 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");
4949 expect(branding.APP_STAGE_LABEL).toBe("Nightly");
50 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
50 expect(branding.APP_DISPLAY_NAME).toBe("The Knife (Nightly)");
5151 });
5252
5353 it("does not label the latest hosted app channel", async () => {
......@@ -58,7 +58,7 @@ describe("branding", () => {
5858 expect(branding.HOSTED_APP_CHANNEL).toBe("latest");
5959 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest");
6060 expect(branding.APP_STAGE_LABEL).toBe("Latest");
61 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code");
61 expect(branding.APP_DISPLAY_NAME).toBe("The Knife");
6262 });
6363
6464 it("ignores unknown hosted app channels", async () => {
......@@ -84,33 +84,33 @@ describe("branding logic", () => {
8484 it("updates the display name for nightly primary server versions", () => {
8585 expect(
8686 resolveServerBackedAppDisplayName({
87 baseName: "T3 Code",
88 fallbackDisplayName: "T3 Code (Alpha)",
87 baseName: "The Knife",
88 fallbackDisplayName: "The Knife (Alpha)",
8989 fallbackStageLabel: "Alpha",
9090 primaryServerVersion: "0.0.28-nightly.20260616.12",
9191 }),
92 ).toBe("T3 Code (Nightly)");
92 ).toBe("The Knife (Nightly)");
9393 });
9494
9595 it("keeps the fallback display name for stable primary server versions", () => {
9696 expect(
9797 resolveServerBackedAppDisplayName({
98 baseName: "T3 Code",
99 fallbackDisplayName: "T3 Code (Alpha)",
98 baseName: "The Knife",
99 fallbackDisplayName: "The Knife (Alpha)",
100100 fallbackStageLabel: "Alpha",
101101 primaryServerVersion: "0.0.27",
102102 }),
103 ).toBe("T3 Code (Alpha)");
103 ).toBe("The Knife (Alpha)");
104104 });
105105
106106 it("keeps the fallback display name for malformed nightly primary server versions", () => {
107107 expect(
108108 resolveServerBackedAppDisplayName({
109 baseName: "T3 Code",
110 fallbackDisplayName: "T3 Code (Alpha)",
109 baseName: "The Knife",
110 fallbackDisplayName: "The Knife (Alpha)",
111111 fallbackStageLabel: "Alpha",
112112 primaryServerVersion: "0.0.28-nightly.20260616",
113113 }),
114 ).toBe("T3 Code (Alpha)");
114 ).toBe("The Knife (Alpha)");
115115 });
116116});
apps/web/src/branding.ts+2-2
......@@ -16,11 +16,11 @@ export const HOSTED_APP_CHANNEL =
1616 hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null;
1717export const HOSTED_APP_CHANNEL_LABEL =
1818 HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null;
19export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code";
19export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "The Knife";
2020export const APP_STAGE_LABEL =
2121 injectedDesktopAppBranding?.stageLabel ??
2222 HOSTED_APP_CHANNEL_LABEL ??
23 (import.meta.env.DEV ? "Dev" : "Alpha");
23 (import.meta.env.DEV ? "Dev" : "Latest");
2424export const APP_DISPLAY_NAME =
2525 injectedDesktopAppBranding?.displayName ??
2626 formatAppDisplayName({ baseName: APP_BASE_NAME, stageLabel: APP_STAGE_LABEL });
apps/web/src/cloud/linkEnvironment.ts+2-2
......@@ -59,7 +59,7 @@ function ensureRelayClientAvailable(
5959 if (status.status === "available") return;
6060 if (status.status === "unsupported") {
6161 return yield* new CloudEnvironmentLinkError({
62 message: `T3 Code cannot install the relay client automatically on ${status.platform}-${status.arch}.`,
62 message: `The Knife cannot install the relay client automatically on ${status.platform}-${status.arch}.`,
6363 });
6464 }
6565
......@@ -95,7 +95,7 @@ function ensureRelayClientAvailable(
9595 return yield* new CloudEnvironmentLinkError({
9696 message:
9797 installedStatus.status === "unsupported"
98 ? `T3 Code cannot install the relay client automatically on ${installedStatus.platform}-${installedStatus.arch}.`
98 ? `The Knife cannot install the relay client automatically on ${installedStatus.platform}-${installedStatus.arch}.`
9999 : "The relay client is still unavailable after installation.",
100100 });
101101 }
apps/web/src/components/ChatView.tsx+1-1
......@@ -11178,7 +11178,7 @@ export default function ChatView(props: ChatViewProps) {
1117811178 ) : renderedRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? (
1117911179 <PullRequestsUnavailableState
1118011180 title="Pull requests unavailable"
11181 error="Update this environment's T3 Code server to browse pull requests."
11181 error="Update The Knife server on this environment to browse pull requests."
1118211182 />
1118311183 ) : renderedRightPanelSurface?.kind === "pull-request" ? (
1118411184 // No onClose: the surface tab's own X owns closing here, and a second X in the header
apps/web/src/components/KnifeMark.tsx created+6
......@@ -0,0 +1,6 @@
1import type { ImgHTMLAttributes } from "react";
2import knife from "../../../../assets/knife/app-icon.icon/Assets/knife.png";
3
4export function KnifeMark(props: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">) {
5 return <img {...props} src={knife} alt="" />;
6}
apps/web/src/components/RightPanelTabs.tsx+1-1
......@@ -171,7 +171,7 @@ export function shouldOpenDefaultBrowserProfileFromMenuClick(
171171}
172172
173173const SURFACE_DISABLED_REASONS = {
174 browser: "Browser previews are only available in the T3 Code desktop app.",
174 browser: "Browser previews are only available in The Knife desktop app.",
175175 terminal: "Terminal surfaces are only available from a project thread.",
176176 files: "Files are only available when a project is open.",
177177 diff: "Diff is only available for server threads in Git repositories.",
apps/web/src/components/ServerUpdateAction.tsx+2-2
......@@ -157,7 +157,7 @@ export function ServerUpdatesAction({
157157 if (desktopTargets.length > 0) {
158158 const confirmed =
159159 (await requestConfirmDialog(
160 `Update the T3 Code desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}? They will close and relaunch on those machines.`,
160 `Update The Knife desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}? They will close and relaunch on those machines.`,
161161 )) ?? true;
162162 if (!confirmed) return;
163163 }
......@@ -279,7 +279,7 @@ export function ServerUpdateAction({
279279 // remote machine installs without asking anyone there.
280280 const confirmed =
281281 (await requestConfirmDialog(
282 `Update the T3 Code desktop app that runs the ${serverLabel}? It will close and relaunch on that machine.`,
282 `Update The Knife desktop app that runs the ${serverLabel}? It will close and relaunch on that machine.`,
283283 )) ?? true;
284284 if (!confirmed) {
285285 return;
apps/web/src/components/T3Wordmark.tsx deleted-12
......@@ -1,12 +0,0 @@
1import type { SVGProps } from "react";
2
3export function T3Wordmark(props: SVGProps<SVGSVGElement>) {
4 return (
5 <svg {...props} viewBox="15.5309 37 94.3941 56.96" xmlns="http://www.w3.org/2000/svg">
6 <path
7 d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
8 fill="currentColor"
9 />
10 </svg>
11 );
12}
apps/web/src/components/chat/MessagesTimeline.tsx+4-4
......@@ -119,7 +119,7 @@ import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";
119119import ReactMarkdown from "react-markdown";
120120import remarkGfm from "remark-gfm";
121121import type { Root, RootContent } from "mdast";
122import { T3Wordmark } from "../T3Wordmark";
122import { KnifeMark } from "../KnifeMark";
123123import { ThreadContextChip } from "../ThreadContextChip";
124124import {
125125 BotIcon,
......@@ -2462,7 +2462,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
24622462 className="me-1 text-2xs text-muted-foreground/70"
24632463 data-user-message-attribution="t3code"
24642464 >
2465 Sent by T3 Code
2465 Sent by The Knife
24662466 </p>
24672467 ) : userMessage.attribution === "agent" ? (
24682468 <p className="me-1 text-2xs text-muted-foreground/70" data-user-message-attribution="agent">
......@@ -2837,7 +2837,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
28372837 return (
28382838 <>
28392839 <div className="relative min-w-0 px-1 py-0.5">
2840 <MessageAuthorHeading>T3 Code</MessageAuthorHeading>
2840 <MessageAuthorHeading>The Knife</MessageAuthorHeading>
28412841 <div data-thread-find-text="true">
28422842 <AssistantCitationSource
28432843 messageId={row.message.id}
......@@ -5236,7 +5236,7 @@ function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className
52365236 case "device":
52375237 return <SmartphoneIcon className={className} aria-hidden />;
52385238 case "t3-code":
5239 return <T3Wordmark className={className} aria-hidden />;
5239 return <KnifeMark className={className} aria-hidden />;
52405240 case "check":
52415241 return <CheckIcon className={className} aria-hidden />;
52425242 case "circle-alert":
apps/web/src/components/chat/V2LifecycleRow.tsx+2-2
......@@ -48,7 +48,7 @@ import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"
4848import { cn } from "~/lib/utils";
4949import { TimelineSystemDivider } from "./TimelineSystemDivider";
5050import { Button, InlineButton } from "../ui/button";
51import { T3Wordmark } from "../T3Wordmark";
51import { KnifeMark } from "../KnifeMark";
5252
5353const LIFECYCLE_TYPES = new Set<OrchestrationV2TurnItem["type"]>([
5454 "run_interrupt_request",
......@@ -170,7 +170,7 @@ export function V2LifecycleRow(props: {
170170 return (
171171 <WorkLogRow
172172 data-v2-item-type={item.type}
173 icon={<T3Wordmark className="size-4 text-icon-muted" aria-hidden />}
173 icon={<KnifeMark className="size-4 text-icon-muted" aria-hidden />}
174174 label={<>Created thread{item.title ? ` · ${item.title}` : ""}</>}
175175 trailing={
176176 <InlineButton
apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.test.ts+3-3
......@@ -31,7 +31,7 @@ describe("mobile client presentation", () => {
3131 it("describes the client platform and enabled notification events", () => {
3232 const client = device();
3333
34 expect(mobileClientPlatformLabel(client)).toBe("iOS 18 · T3 Code 1.2.3");
34 expect(mobileClientPlatformLabel(client)).toBe("iOS 18 · The Knife 1.2.3");
3535 expect(mobileClientNotificationDetail(client)).toBe(
3636 "Alerts enabled for approvals, completions.",
3737 );
......@@ -42,7 +42,7 @@ describe("mobile client presentation", () => {
4242 mobileClientPlatformLabel(
4343 device({ platform: "android", iosMajorVersion: null, androidApiLevel: 36 }),
4444 ),
45 ).toBe("Android · T3 Code 1.2.3");
45 ).toBe("Android · The Knife 1.2.3");
4646 });
4747
4848 it("distinguishes disabled notifications from an empty event selection", () => {
......@@ -68,7 +68,7 @@ describe("mobile client presentation", () => {
6868
6969 it("handles missing app versions and invalid update timestamps", () => {
7070 expect(mobileClientPlatformLabel(device({ iosMajorVersion: null }))).toBe(
71 "iOS · T3 Code 1.2.3",
71 "iOS · The Knife 1.2.3",
7272 );
7373 expect(mobileClientPlatformLabel(device({ appVersion: null }))).toBe("iOS 18");
7474 expect(mobileClientUpdatedAtLabel("not-a-date")).toBe("Update time unavailable");
apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.ts+1-1
......@@ -21,7 +21,7 @@ export function mobileClientPlatformLabel(device: RelayClientDeviceRecord): stri
2121 : device.iosMajorVersion === null
2222 ? "iOS"
2323 : `iOS ${device.iosMajorVersion}`;
24 return `${platform}${device.appVersion ? ` · T3 Code ${device.appVersion}` : ""}`;
24 return `${platform}${device.appVersion ? ` · The Knife ${device.appVersion}` : ""}`;
2525}
2626
2727export function mobileClientNotificationDetail(device: RelayClientDeviceRecord): string {
apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx+1-1
......@@ -94,7 +94,7 @@ function EmptyMobileClients() {
9494 <EmptyHeader>
9595 <EmptyTitle>No mobile clients</EmptyTitle>
9696 <EmptyDescription>
97 Install T3 Code on your phone and sign in to T3 Connect to get push notifications and Live
97 Install The Knife on your phone and sign in to T3 Connect to get push notifications and Live
9898 Activities.
9999 </EmptyDescription>
100100 </EmptyHeader>
apps/web/src/components/cloud/RelayClientInstallDialog.tsx+4-4
......@@ -69,8 +69,8 @@ export function RelayClientInstallDialog() {
6969 </DialogTitle>
7070 <DialogDescription>
7171 {isInstalling
72 ? "T3 Code is preparing this environment for secure access through T3 Connect."
73 : "T3 Code needs the relay client to make this environment available through T3 Connect."}
72 ? "The Knife is preparing this environment for secure access through T3 Connect."
73 : "The Knife needs the relay client to make this environment available through T3 Connect."}
7474 </DialogDescription>
7575 </DialogHeader>
7676 <DialogPanel scrollFade={false}>
......@@ -91,14 +91,14 @@ export function RelayClientInstallDialog() {
9191 value={activeStepIndex + 1}
9292 />
9393 <p className="text-xs leading-relaxed text-muted-foreground">
94 Keep T3 Code open while the relay client is installed.
94 Keep The Knife open while the relay client is installed.
9595 </p>
9696 </div>
9797 ) : (
9898 <div className="rounded-xl border border-border/70 bg-muted/35 p-3">
9999 <p className="text-sm font-medium text-foreground">Managed relay client</p>
100100 <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
101 T3 Code will download and install version{" "}
101 The Knife will download and install version{" "}
102102 {view.status === "confirming" ? view.version : ""} locally.
103103 </p>
104104 </div>
apps/web/src/components/desktop/DesktopAppActivationCoordinator.tsx+1-1
......@@ -63,7 +63,7 @@ export function DesktopAppActivationCoordinator() {
6363 });
6464 if (result._tag === "Failure") {
6565 const error = squashAtomCommandFailure(result);
66 throw error instanceof Error ? error : new Error("T3 Code could not add the project.");
66 throw error instanceof Error ? error : new Error("The Knife could not add the project.");
6767 }
6868 return projectId;
6969 },
apps/web/src/components/desktop/SshPasswordPromptDialog.tsx+1-1
......@@ -161,7 +161,7 @@ function ActiveSshPasswordPrompt({
161161 <DialogTitle>SSH Password Required</DialogTitle>
162162 <DialogDescription>
163163 T3 needs your SSH password to connect to <code>{target}</code>. The password is passed
164 to the local SSH process for this connection attempt and is not saved by T3 Code.
164 to the local SSH process for this connection attempt and is not saved by The Knife.
165165 </DialogDescription>
166166 </DialogHeader>
167167 <DialogPanel scrollFade={false}>
apps/web/src/components/desktopUpdate.logic.test.ts+3-3
......@@ -279,7 +279,7 @@ describe("desktop update UI helpers", () => {
279279 availableVersion: "1.1.0",
280280 downloadedVersion: "1.1.1",
281281 }),
282 ).toContain("Install update 1.1.1 and restart T3 Code?");
282 ).toContain("Install update 1.1.1 and restart The Knife?");
283283 });
284284
285285 it("falls back to generic install confirmation copy when no version is available", () => {
......@@ -288,7 +288,7 @@ describe("desktop update UI helpers", () => {
288288 availableVersion: null,
289289 downloadedVersion: null,
290290 }),
291 ).toContain("Install update and restart T3 Code?");
291 ).toContain("Install update and restart The Knife?");
292292 });
293293
294294 it("keeps the same install confirmation copy across desktop platforms", () => {
......@@ -298,7 +298,7 @@ describe("desktop update UI helpers", () => {
298298 downloadedVersion: "1.1.0",
299299 }),
300300 ).toBe(
301 "Install update 1.1.0 and restart T3 Code?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.",
301 "Install update 1.1.0 and restart The Knife?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.",
302302 );
303303 });
304304});
apps/web/src/components/desktopUpdate.logic.ts+4-4
......@@ -62,12 +62,12 @@ export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState):
6262
6363 const action = resolveDesktopUpdateButtonAction(state);
6464 if (action === "download") {
65 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build.";
65 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build.";
6666 }
6767 if (action === "install") {
68 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build.";
68 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build.";
6969 }
70 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build.";
70 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build.";
7171}
7272
7373export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {
......@@ -108,7 +108,7 @@ export function getDesktopUpdateInstallConfirmationMessage(
108108 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
109109): string {
110110 const version = state.downloadedVersion ?? state.availableVersion;
111 return `Install update${version ? ` ${version}` : ""} and restart T3 Code?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.`;
111 return `Install update${version ? ` ${version}` : ""} and restart The Knife?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.`;
112112}
113113
114114export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {
apps/web/src/components/onboarding/FirstRunGate.tsx+1-1
......@@ -214,7 +214,7 @@ function FirstRunRecovery({
214214 <p className="mt-2 text-sm text-muted-foreground">
215215 {settingsReadFailed
216216 ? "Your saved settings could not be loaded."
217 : "T3 Code could not confirm this workspace."}
217 : "The Knife could not confirm this workspace."}
218218 </p>
219219 <Button
220220 className="mt-5"
apps/web/src/components/onboarding/WelcomeWizard.tsx+9-9
......@@ -84,7 +84,7 @@ import { TerminalViewport } from "../ThreadTerminalDrawer";
8484import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
8585import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation";
8686import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
87import { T3Wordmark } from "../T3Wordmark";
87import { KnifeMark } from "../KnifeMark";
8888import { Alert, AlertDescription } from "../ui/alert";
8989import { Button } from "../ui/button";
9090import { CommandBlock } from "../CommandBlock";
......@@ -233,12 +233,12 @@ export function WelcomeWizard({
233233 initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true}
234234 >
235235 <WizardHeader
236 title="Set up T3 Code"
236 title="Set up The Knife"
237237 identity={
238 <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code">
239 <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden />
238 <div className="flex items-center gap-2" role="img" aria-label="The Knife">
239 <KnifeMark className="size-8 shrink-0" aria-hidden />
240240 <span className="text-2xl font-medium tracking-tight text-muted-foreground">
241 Code
241 The Knife
242242 </span>
243243 </div>
244244 }
......@@ -432,8 +432,8 @@ function ConnectionStep({
432432 ) : null}
433433 <div className="mt-6 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
434434 <p className="min-w-0 text-xs leading-relaxed text-muted-foreground">
435 T3 Code collects anonymous usage data to help us improve it. To read more about how your
436 data is used and how to opt out, see our{" "}
435 Anonymous usage data helps improve T3 Code. To read more about how your data is used and
436 how to opt out, see our{" "}
437437 <a
438438 className="underline underline-offset-2 hover:text-foreground"
439439 href={PRIVACY_POLICY_URL}
......@@ -530,7 +530,7 @@ function ConnectAccountOption({
530530 </p>
531531 <CommandBlock command="npx t3 connect" className="mt-3" />
532532 <p className="mt-3 text-xs text-muted-foreground">
533 Keep T3 Code running. Select the computers you want to set up above.
533 Keep The Knife running. Select the computers you want to set up above.
534534 </p>
535535 </div>
536536 </CollapsiblePanel>
......@@ -644,7 +644,7 @@ function PairingForm({
644644 </p>
645645 <CommandBlock command="npx t3 pair" className="mt-2" />
646646 <p className="mt-2 text-xs leading-relaxed text-muted-foreground">
647 Start T3 Code first, or run <code className="font-mono">npx t3 serve</code>. Add{" "}
647 Start The Knife first, or run <code className="font-mono">npx t3 serve</code>. Add{" "}
648648 <code className="font-mono">--tailscale</code> to use your tailnet.
649649 </p>
650650 </CollapsiblePanel>
apps/web/src/components/preview/PreviewPanel.tsx+1-1
......@@ -41,7 +41,7 @@ export function PreviewPanel({
4141 <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
4242 <p className="max-w-sm text-sm text-muted-foreground">
4343 {canOperatePreview
44 ? "Preview is only available in the T3 Code desktop app."
44 ? "Preview is only available in The Knife desktop app."
4545 : "Pair this client again with preview access to control browser previews."}
4646 </p>
4747 </div>
apps/web/src/components/preview/PreviewView.tsx+1-1
......@@ -1097,7 +1097,7 @@ export function PreviewView({
10971097 ) : (
10981098 <div className="flex h-full items-center justify-center p-8 text-center">
10991099 <p className="max-w-sm text-sm text-muted-foreground">
1100 This tab is open in the T3 Code desktop app.
1100 This tab is open in The Knife desktop app.
11011101 </p>
11021102 </div>
11031103 )
apps/web/src/components/promptLinks/HostedPromptLinkLanding.tsx+1-1
......@@ -146,7 +146,7 @@ function HostedPromptLinkLanding({
146146 <Alert className="mt-5" variant="error">
147147 <AlertTitle>This prompt had hidden characters, it may be malicious</AlertTitle>
148148 <AlertDescription>
149 T3 Code removed {prompt.hiddenCharacters} hidden character
149 The Knife removed {prompt.hiddenCharacters} hidden character
150150 {prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it.
151151 </AlertDescription>
152152 </Alert>
apps/web/src/components/promptLinks/NewThreadLinkView.tsx+2-2
......@@ -99,7 +99,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
9999 replace: true,
100100 }).catch(() => null);
101101 if (opened === null) {
102 setState({ status: "failed", message: "T3 Code could not open a new thread." });
102 setState({ status: "failed", message: "The Knife could not open a new thread." });
103103 return;
104104 }
105105 setState({ status: "opened" });
......@@ -169,7 +169,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
169169 setState({
170170 status: "failed",
171171 message:
172 "This machine cannot clone from a link. Update T3 Code on it, or clone the repository from the command palette.",
172 "This machine cannot clone from a link. Update The Knife on it, or clone the repository from the command palette.",
173173 });
174174 return;
175175 }
apps/web/src/components/promptLinks/linkedPrompt.ts+1-1
......@@ -10,7 +10,7 @@ export function warnAboutHiddenCharacters(prompt: LinkedPrompt): void {
1010 toastManager.add({
1111 type: "warning",
1212 title: "This prompt had hidden characters, it may be malicious",
13 description: `T3 Code removed ${prompt.hiddenCharacters} hidden character${prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it.`,
13 description: `The Knife removed ${prompt.hiddenCharacters} hidden character${prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it.`,
1414 timeout: 0,
1515 });
1616}
apps/web/src/components/settings/BrowserImportWizard.tsx+4-4
......@@ -285,9 +285,9 @@ function FullDiskAccessStep({
285285 return (
286286 <>
287287 <DialogHeader>
288 <DialogTitle>Let T3 Code read {source.name}&rsquo;s cookies</DialogTitle>
288 <DialogTitle>Let The Knife read {source.name}&rsquo;s cookies</DialogTitle>
289289 <DialogDescription>
290 To import cookies from {source.name}, T3 Code needs Full Disk Access. Turn it on in System
290 To import cookies from {source.name}, The Knife needs Full Disk Access. Turn it on in System
291291 Settings, then come back to finish the import — you can revoke it again once the import is
292292 done.
293293 </DialogDescription>
......@@ -319,8 +319,8 @@ function FullDiskAccessStep({
319319 {!permission.isReady(["fullDiskAccess"]) ? (
320320 <p className="mt-3 text-xs text-muted-foreground">
321321 {stillRequired
322 ? "Access is still required. Quit and reopen T3 Code if you just allowed it, then retry the import."
323 : "If access doesn't update after you allow it, quit and reopen T3 Code, then retry the import."}
322 ? "Access is still required. Quit and reopen The Knife if you just allowed it, then retry the import."
323 : "If access doesn't update after you allow it, quit and reopen The Knife, then retry the import."}
324324 </p>
325325 ) : null}
326326 </DialogPanel>
apps/web/src/components/settings/CaptureShortcutConfig.tsx+5-5
......@@ -217,7 +217,7 @@ export function CaptureShortcutConfig({
217217 ) : (
218218 <>
219219 <p className="text-muted-foreground">
220 Allow T3 Code to read your desktop settings. You'll review any changes here before
220 Allow The Knife to read your desktop settings. You'll review any changes here before
221221 saving.
222222 </p>
223223 <Button
......@@ -229,7 +229,7 @@ export function CaptureShortcutConfig({
229229 </Button>
230230 {!supported ? (
231231 <p className="text-xs text-muted-foreground">
232 Update T3 Code to finish setting up your shortcut.
232 Update The Knife to finish setting up your shortcut.
233233 </p>
234234 ) : null}
235235 </>
......@@ -243,7 +243,7 @@ export function CaptureShortcutConfig({
243243 <p role="status" className="text-muted-foreground">
244244 {state.shortcutPending
245245 ? "Connecting to your desktop…"
246 : "Restart T3 Code to finish connecting your shortcut."}
246 : "Restart The Knife to finish connecting your shortcut."}
247247 </p>
248248 ) : null}
249249 <details className="text-xs text-muted-foreground">
......@@ -262,7 +262,7 @@ export function CaptureShortcutConfig({
262262 state.shortcutConfigPath ??
263263 (niri ? "~/.config/niri/config.kdl" : "~/.config/hypr/hyprland.conf")}
264264 </p>
265 {niri ? <p>T3 Code also reads any files included by this file.</p> : null}
265 {niri ? <p>The Knife also reads any files included by this file.</p> : null}
266266 {preview && preview.resolvedPath !== preview.path ? (
267267 <p className="break-all">Linked to {preview.resolvedPath}. The link will be kept.</p>
268268 ) : null}
......@@ -323,7 +323,7 @@ export function CaptureShortcutConfig({
323323 {isCopied ? "Copied" : "Copy shortcut"}
324324 </Button>
325325 <p>
326 Turn capture off in T3 Code to stop it. Remove the shortcut from {desktop} to free up
326 Turn capture off in The Knife to stop it. Remove the shortcut from {desktop} to free up
327327 the keys.
328328 </p>
329329 {state.shortcutActionRegistered === false ? (
apps/web/src/components/settings/ChatGptWelcomeCoordinator.tsx+1-1
......@@ -66,7 +66,7 @@ export function ChatGptWelcomeCoordinator() {
6666 <OpenAI className="mb-2 size-8" aria-hidden="true" />
6767 <DialogTitle>Your ChatGPT plan is connected</DialogTitle>
6868 <DialogDescription>
69 Eligible usage in T3 Code uses your ChatGPT plan. Manage your shared usage and any
69 Eligible usage in The Knife uses your ChatGPT plan. Manage your shared usage and any
7070 credit settings in ChatGPT.
7171 </DialogDescription>
7272 <p className="text-xs text-muted-foreground">
apps/web/src/components/settings/CliCommandSettingsRow.tsx+2-2
......@@ -52,9 +52,9 @@ export function CliCommandSettingsRow() {
5252 if (!bridge || !state?.supported) return null;
5353 const installed = state.installedPath !== null;
5454 const description = state.shadowedBy
55 ? `Another t3 at ${state.shadowedBy} runs first in a new terminal. Remove it to use T3 Code's.`
55 ? `Another t3 at ${state.shadowedBy} runs first in a new terminal. Remove it to use The Knife's.`
5656 : !installed
57 ? "Run T3 Code's CLI as `t3` from any terminal."
57 ? "Run The Knife's CLI as `t3` from any terminal."
5858 : state.onPath
5959 ? `Installed at ${state.installedPath}. Open a new terminal to use it.`
6060 : `Installed at ${state.installedPath}, which is not on your PATH yet. Add its folder to your PATH to run \`t3\`.`;
apps/web/src/components/settings/CodexSetupSection.tsx+4-4
......@@ -577,8 +577,8 @@ function ManagedCodexSetup({
577577 : installation?.phase === "verifying"
578578 ? "Checking Codex."
579579 : installed
580 ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by T3 Code"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.`
581 : (installation?.message ?? "T3 Code downloads and manages Codex for you.");
580 ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by The Knife"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.`
581 : (installation?.message ?? "The Knife downloads and manages Codex for you.");
582582 const accountDescription = finishingSignIn ? (
583583 "Finishing sign-in..."
584584 ) : installActive ? (
......@@ -655,7 +655,7 @@ function ManagedCodexSetup({
655655 const callbackCompletion =
656656 !handoff && auth?.phase === "waiting" && url ? (
657657 <div className="flex w-full flex-col gap-3 text-xs leading-relaxed text-muted-foreground">
658 <p>If sign-in doesn't return to T3 Code, paste the URL from the final localhost page.</p>
658 <p>If sign-in doesn't return to The Knife, paste the URL from the final localhost page.</p>
659659 <form
660660 className="flex flex-col gap-2 sm:flex-row sm:items-center"
661661 onSubmit={(event) => {
......@@ -1032,7 +1032,7 @@ export function CodexManagedRuntimeFields({
10321032 <>
10331033 <SettingsRow
10341034 title="Binary path"
1035 description="Selected by T3 Code."
1035 description="Selected by The Knife."
10361036 control={
10371037 <div className="w-full sm:w-80">
10381038 <Input
apps/web/src/components/settings/ConnectionsSettings.tsx+10-10
......@@ -3480,7 +3480,7 @@ export function ConnectionsSettings() {
34803480 {desktopWslState.enabled ? (
34813481 <SettingsRow
34823482 title="WSL only"
3483 description="Run only the WSL backend. T3 Code restarts when this changes."
3483 description="Run only the WSL backend. The Knife restarts when this changes."
34843484 className="bg-muted/20 pl-7 sm:pl-8"
34853485 control={
34863486 <Switch
......@@ -3805,8 +3805,8 @@ export function ConnectionsSettings() {
38053805 </AlertDialogTitle>
38063806 <AlertDialogDescription>
38073807 {pendingDesktopServerExposureMode === "network-accessible"
3808 ? "Let your other devices connect to T3 Code over the network. Pair devices to give them access. T3 Code will restart."
3809 : "Devices connected over your local network will disconnect. Existing tunnels, such as T3 Connect or Tailscale HTTPS, keep working. T3 Code will restart."}
3808 ? "Let your other devices connect to The Knife over the network. Pair devices to give them access. The Knife will restart."
3809 : "Devices connected over your local network will disconnect. Existing tunnels, such as T3 Connect or Tailscale HTTPS, keep working. The Knife will restart."}
38103810 </AlertDialogDescription>
38113811 </AlertDialogHeader>
38123812 <AlertDialogFooter>
......@@ -3862,15 +3862,15 @@ export function ConnectionsSettings() {
38623862 <AlertDialogDescription>
38633863 {pendingWslChange?.kind === "disable"
38643864 ? pendingWslChange.wasWslOnly
3865 ? "T3 Code will restart on the Windows backend. Threads and projects opened against WSL stay safe inside the distro and become available again when you re-enable WSL."
3866 : "The WSL backend will stop. Threads and projects opened against WSL stay safe inside the distro, but they'll be unavailable in T3 Code until you re-enable WSL."
3865 ? "The Knife will restart on the Windows backend. Threads and projects opened against WSL stay safe inside the distro and become available again when you re-enable WSL."
3866 : "The WSL backend will stop. Threads and projects opened against WSL stay safe inside the distro, but they'll be unavailable in The Knife until you re-enable WSL."
38673867 : pendingWslChange?.kind === "distro"
3868 ? "T3 Code will restart the WSL backend on the new distro. Sessions still running on the current distro will be interrupted."
3868 ? "The Knife will restart the WSL backend on the new distro. Sessions still running on the current distro will be interrupted."
38693869 : pendingWslChange?.kind === "enable"
38703870 ? "Run the WSL backend alongside the Windows one, or stop the Windows backend and use only WSL? You can change this later from Settings."
38713871 : pendingWslChange?.nextValue
3872 ? "T3 Code will restart and start only the WSL backend. Your Windows-side projects won't be accessible until you turn this off again."
3873 : "T3 Code will restart and bring the Windows backend back up alongside WSL."}
3872 ? "The Knife will restart and start only the WSL backend. Your Windows-side projects won't be accessible until you turn this off again."
3873 : "The Knife will restart and bring the Windows backend back up alongside WSL."}
38743874 </AlertDialogDescription>
38753875 </AlertDialogHeader>
38763876 <AlertDialogFooter>
......@@ -3956,7 +3956,7 @@ export function ConnectionsSettings() {
39563956 <AlertDialogHeader>
39573957 <AlertDialogTitle>Disable Tailscale HTTPS?</AlertDialogTitle>
39583958 <AlertDialogDescription>
3959 T3 Code will restart the local backend without Tailscale Serve.
3959 The Knife will restart the local backend without Tailscale Serve.
39603960 </AlertDialogDescription>
39613961 </AlertDialogHeader>
39623962 <AlertDialogFooter>
......@@ -3994,7 +3994,7 @@ export function ConnectionsSettings() {
39943994 <DialogHeader>
39953995 <DialogTitle>Set up Tailscale HTTPS?</DialogTitle>
39963996 <DialogDescription>
3997 T3 Code will restart the local backend with Tailscale Serve enabled and ask
3997 The Knife will restart the local backend with Tailscale Serve enabled and ask
39983998 Tailscale to proxy HTTPS traffic to this backend.
39993999 </DialogDescription>
40004000 </DialogHeader>
apps/web/src/components/settings/IntegrationsSettings.tsx+1-1
......@@ -577,7 +577,7 @@ function BrowserRecordingFrameRateSetting({ disabled }: { readonly disabled: boo
577577
578578const LINK_TARGET_LABELS: Readonly<Record<BrowserLinkTarget, string>> = {
579579 system: "Your default browser",
580 app: "T3 Code",
580 app: "The Knife",
581581};
582582
583583function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean }) {
apps/web/src/components/settings/KeybindingsSettings.tsx+1-1
......@@ -257,7 +257,7 @@ function UnknownWhenVariableWarning({
257257
258258 return (
259259 <WarningTooltipIcon label={label} focusable={focusable} className="size-4.5">
260 T3 Code does not recognize this condition yet. It can still be saved, but it may not match
260 The Knife does not recognize this condition yet. It can still be saved, but it may not match
261261 unless the runtime provides it.
262262 </WarningTooltipIcon>
263263 );
apps/web/src/components/settings/LocalEnvironmentSetting.tsx+3-3
......@@ -43,7 +43,7 @@ export function LocalEnvironmentSetting() {
4343 {...searchableSetting("local-environment")}
4444 description={
4545 enabled
46 ? "Run agents on this computer. Turn off to use T3 Code only with remote environments."
46 ? "Run agents on this computer. Turn off to use The Knife only with remote environments."
4747 : "Turned off. Agents only run in remote environments."
4848 }
4949 control={
......@@ -70,8 +70,8 @@ export function LocalEnvironmentSetting() {
7070 </AlertDialogTitle>
7171 <AlertDialogDescription>
7272 {enabled
73 ? "T3 Code will restart without running a server on this computer. Any agents and terminals running here will stop, and other devices will no longer be able to connect to this computer. Your projects, history, and remote environments are unaffected."
74 : "T3 Code will restart and start running a server on this computer again."}
73 ? "The Knife will restart without running a server on this computer. Any agents and terminals running here will stop, and other devices will no longer be able to connect to this computer. Your projects, history, and remote environments are unaffected."
74 : "The Knife will restart and start running a server on this computer again."}
7575 </AlertDialogDescription>
7676 </AlertDialogHeader>
7777 {error ? <p className="px-6 pb-4 text-sm text-destructive">{error}</p> : null}
apps/web/src/components/settings/NotificationSettings.tsx+1-1
......@@ -22,7 +22,7 @@ export function NotificationSettings() {
2222 {...searchableSetting("thread-notifications")}
2323 description={
2424 permissionMessage ??
25 "System alerts when a thread finishes, fails, or needs input or approval. Applies to this device while T3 Code is open."
25 "System alerts when a thread finishes, fails, or needs input or approval. Applies to this device while The Knife is open."
2626 }
2727 control={
2828 <Select
apps/web/src/components/settings/SettingsPanels.tsx+1-1
......@@ -3422,7 +3422,7 @@ export function GeneralSettingsPanel() {
34223422 />
34233423 <SettingsRow
34243424 {...searchableSetting("open-source-licenses")}
3425 description="Notices for dependencies, assets, and optional tools used by T3 Code."
3425 description="Notices for dependencies, assets, and optional tools used by The Knife."
34263426 control={
34273427 <Button
34283428 render={<Link to="/settings/open-source-licenses" />}
apps/web/src/components/settings/SnapShotSettings.tsx+2-2
......@@ -263,7 +263,7 @@ export function SnapShotSettings() {
263263 const shortcutStatus = recording
264264 ? "Press your shortcut. Esc cancels."
265265 : candidateConflict
266 ? `T3 Code already uses this for "${commandLabel(candidateConflict)}".`
266 ? `The Knife already uses this for "${commandLabel(candidateConflict)}".`
267267 : shortcutCheck.status === "checking"
268268 ? "Checking shortcut…"
269269 : shortcutCheck.availability
......@@ -315,7 +315,7 @@ export function SnapShotSettings() {
315315 try {
316316 if (state?.macPermissions) {
317317 saveSnapShotSetupResume(wizard?.wasEnabled ?? settings.snapShotEnabled);
318 if (!bridge?.setupSnapShot) throw new Error("Restart T3 Code to finish capture setup.");
318 if (!bridge?.setupSnapShot) throw new Error("Restart The Knife to finish capture setup.");
319319 await bridge.setupSnapShot("test-mac-capture");
320320 }
321321 if (state?.mode === "direct")
apps/web/src/components/settings/SnapShotSetupDialog.tsx+2-2
......@@ -174,7 +174,7 @@ export function SnapShotSetupDialog({
174174 ? "Update the capture helper"
175175 : "Allow snapshots",
176176 description:
177 "T3 Code's capture helper lets you capture other apps and return to your draft. It's included with T3 Code.",
177 "The Knife's capture helper lets you capture other apps and return to your draft. It's included with The Knife.",
178178 }
179179 : backend === "niri"
180180 ? {
......@@ -355,7 +355,7 @@ export function SnapShotSetupDialog({
355355 </p>
356356 ))}
357357 {step === "access" && (backend === "gnome" || helperBackend) ? (
358 <p>Included with T3 Code. No download needed.</p>
358 <p>Included with The Knife. No download needed.</p>
359359 ) : null}
360360 {step === "access" && backend === "gnome" && extension?.status === "enabled" ? (
361361 <Button
apps/web/src/components/settings/providerStatus.ts+1-1
......@@ -44,7 +44,7 @@ export function getProviderSummary(provider: ServerProvider | undefined) {
4444 return {
4545 headline: "Disabled",
4646 detail:
47 provider.message ?? "This provider is installed but disabled for new sessions in T3 Code.",
47 provider.message ?? "This provider is installed but disabled for new sessions in The Knife.",
4848 };
4949 }
5050 if (!provider.installed) {
apps/web/src/components/settings/scopedSettings.ts+1-1
......@@ -475,7 +475,7 @@ export async function persistScopedSettingsPatch(
475475 return [
476476 {
477477 ...environment,
478 message: "This environment does not support this setting. Update T3 Code on it.",
478 message: "This environment does not support this setting. Update The Knife on it.",
479479 },
480480 ];
481481 }
apps/web/src/components/settings/settingsLayout.tsx+1
......@@ -573,6 +573,7 @@ export function SettingsPageContainer({
573573 <div
574574 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto"
575575 data-settings-page-scroll
576 data-scroll-restoration-id="settings-page"
576577 >
577578 <WorkspacePageContainer width={width} className={cn("gap-8", className)}>
578579 <SettingsScopeSentence />
apps/web/src/components/sidebar/mainAppLocation.ts+5-4
......@@ -14,8 +14,6 @@ export function isSidebarUtilityPage(pathname: string) {
1414 );
1515}
1616
17let mainAppHref: string | null = null;
18
1917// Mount once in the app shell. Records the latest main app URL so Back can
2018// return there no matter how many utility pages were visited since.
2119export function MainAppLocationTracker() {
......@@ -23,7 +21,7 @@ export function MainAppLocationTracker() {
2321 select: (location) => (isSidebarUtilityPage(location.pathname) ? null : location.href),
2422 });
2523 useEffect(() => {
26 if (href !== null) mainAppHref = href;
24 if (href !== null) sessionStorage.setItem("t3code-main-app-href", href);
2725 }, [href]);
2826 return null;
2927}
......@@ -32,5 +30,8 @@ export function MainAppLocationTracker() {
3230// the app was opened directly on a utility page.
3331export function useNavigateToMainApp() {
3432 const navigate = useNavigate();
35 return useCallback(() => navigate({ href: mainAppHref ?? "/" }), [navigate]);
33 return useCallback(
34 () => navigate({ href: sessionStorage.getItem("t3code-main-app-href") ?? "/" }),
35 [navigate],
36 );
3637}
apps/web/src/components/sidebar/updateEverythingRun.tsx+1-1
......@@ -133,7 +133,7 @@ export function DesktopUpdateLaunchNotice() {
133133 stackedThreadToast({
134134 type: "error",
135135 title: "Update did not apply",
136 description: `T3 Code is still on ${APP_VERSION}. Install ${notAppliedVersion} again from the update button.`,
136 description: `The Knife is still on ${APP_VERSION}. Install ${notAppliedVersion} again from the update button.`,
137137 }),
138138 );
139139 }, [notAppliedVersion]);
apps/web/src/connection/clientMetadata.test.ts+1-1
......@@ -80,7 +80,7 @@ describe("client telemetry metadata", () => {
8080 desktopBridge: { getClientPlatform: () => "darwin" },
8181 }),
8282 ).toEqual({
83 label: "T3 Code Desktop",
83 label: "The Knife Desktop",
8484 deviceType: "desktop",
8585 os: "macOS",
8686 surface: "desktop",
apps/web/src/connection/clientMetadata.ts+2-2
......@@ -77,7 +77,7 @@ export function clientPresentationMetadata(input: {
7777}): AuthClientPresentationMetadata {
7878 if (input.desktopBridge !== undefined) {
7979 return {
80 label: "T3 Code Desktop",
80 label: "The Knife Desktop",
8181 deviceType: "desktop",
8282 os: clientOsFromElectronPlatform(input.desktopBridge.getClientPlatform?.()),
8383 surface: "desktop",
......@@ -86,7 +86,7 @@ export function clientPresentationMetadata(input: {
8686 }
8787
8888 return {
89 label: "T3 Code Web",
89 label: "The Knife Web",
9090 deviceType: browserDeviceType(input.identity),
9191 os: browserClientOs(input.identity),
9292 surface: "web",
apps/web/src/desktopAppActivation.ts+3-3
......@@ -85,7 +85,7 @@ export async function handleDesktopAppActivationRequest(
8585 return failure(
8686 request.requestId,
8787 "project-create-failed",
88 errorMessage(error, "T3 Code could not add the project."),
88 errorMessage(error, "The Knife could not add the project."),
8989 );
9090 }
9191 }
......@@ -99,7 +99,7 @@ export async function handleDesktopAppActivationRequest(
9999 return failure(
100100 request.requestId,
101101 "thread-open-failed",
102 "T3 Code could not open a new thread for the project.",
102 "The Knife could not open a new thread for the project.",
103103 );
104104 }
105105 return {
......@@ -113,7 +113,7 @@ export async function handleDesktopAppActivationRequest(
113113 return failure(
114114 request.requestId,
115115 "thread-open-failed",
116 errorMessage(error, "T3 Code could not open a new thread for the project."),
116 errorMessage(error, "The Knife could not open a new thread for the project."),
117117 );
118118 }
119119}
apps/web/src/lib/bootError.ts+2-2
......@@ -1,6 +1,6 @@
11/** Shows startup failures before React can replace the boot splash. */
22export function showBootError(error: unknown) {
3 console.error("T3 Code failed to start.", error);
3 console.error("The Knife failed to start.", error);
44 const bootShell = document.getElementById("boot-shell");
55 if (!bootShell) return;
66
......@@ -9,7 +9,7 @@ export function showBootError(error: unknown) {
99 content.setAttribute("role", "alert");
1010
1111 const message = document.createElement("p");
12 message.textContent = "T3 Code could not load.";
12 message.textContent = "The Knife could not load.";
1313 content.append(message);
1414
1515 if (import.meta.env.DEV && error instanceof Error) {
apps/web/src/router.ts+2
......@@ -1,12 +1,14 @@
11import { createRouter, RouterHistory } from "@tanstack/react-router";
22
33import { routeTree } from "./routeTree.gen";
4import { isSidebarUtilityPage } from "./components/sidebar/mainAppLocation";
45
56export function getRouter(history: RouterHistory) {
67 return createRouter({
78 routeTree,
89 history,
910 context: {},
11 scrollRestoration: ({ location }) => isSidebarUtilityPage(location.pathname),
1012 // Route components are split chunks (autoCodeSplitting in vite.config);
1113 // fetching them on hover/focus intent hides the load from the first
1214 // settings or pull-request navigation.
apps/web/src/routes/_chat.index.tsx+2-2
......@@ -158,9 +158,9 @@ function HostedStaticOnboardingState() {
158158 <div className="mx-auto mb-5 flex size-11 items-center justify-center rounded-xl border border-border/70 bg-background/70 text-muted-foreground">
159159 <LinkIcon className="size-5" />
160160 </div>
161 <EmptyTitle>Connect to a computer running T3 Code</EmptyTitle>
161 <EmptyTitle>Connect to a computer running The Knife</EmptyTitle>
162162 <EmptyDescription>
163 This app connects to T3 Code running on your computer or a server. Start the T3 Code
163 This app connects to The Knife running on your computer or a server. Start The Knife
164164 desktop app or command-line server on that machine and keep it running.
165165 </EmptyDescription>
166166 <EmptyDescription>{description}</EmptyDescription>
apps/web/src/routes/_chat.pull-requests.tsx+1-1
......@@ -1914,7 +1914,7 @@ function PullRequestsRouteView() {
19141914 ) : !pullRequestsSupported ? (
19151915 <PullRequestsUnavailableState
19161916 title="Pull requests unavailable"
1917 error="Update your T3 Code servers to browse pull requests."
1917 error="Update your servers to browse pull requests."
19181918 />
19191919 ) : firstLoad ? (
19201920 <PullRequestListGhost rows={7} />
apps/web/src/routes/_chat.tsx+1-1
......@@ -153,7 +153,7 @@ function ChatRouteGlobalShortcuts() {
153153 stackedThreadToast({
154154 type: "info",
155155 title: "Preview is desktop-only",
156 description: "Open T3 Code in the desktop app to use the in-app preview.",
156 description: "Open The Knife in the desktop app to use the in-app preview.",
157157 }),
158158 );
159159 return;
apps/web/src/versionSkew.test.ts+1-1
......@@ -24,7 +24,7 @@ import {
2424} from "./versionSkew";
2525
2626const MISMATCH_HINT =
27 "Version mismatch. Try syncing the client and server to the same T3 Code version.";
27 "Version mismatch. Try syncing the client and server to the same version of The Knife.";
2828
2929describe("versionSkew", () => {
3030 it("updates only the proven npm prefix and safely quotes its path", () => {
apps/web/src/versionSkew.ts+1-1
......@@ -87,7 +87,7 @@ export function resolveVersionMismatch(
8787 return {
8888 clientVersion: normalizedClientVersion,
8989 serverVersion: normalizedServerVersion,
90 hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.",
90 hint: "Version mismatch. Try syncing the client and server to the same version of The Knife.",
9191 };
9292}
9393
assets/README.md+20-60
......@@ -1,67 +1,27 @@
1# Brand icons
1# The Knife artwork
22
3The three Icon Composer projects are the source of truth for full application icons:
3`knife/app-icon.icon` is the canonical Icon Composer project. Its rendered knife
4layer lives in `Assets/knife.png`; the project owns the obsidian backplate. All
5release channels use this one identity. The older T3 projects remain as upstream
6artwork references.
47
5- `dev/app-icon.icon`
6- `nightly/app-icon.icon`
7- `prod/app-icon.icon`
8`vp run icons:export` regenerates the macOS, iOS, Linux, Windows, and web renditions,
9including browser public assets. `vp run icons:check` verifies those exports.
10Icon Composer 2 or newer is required; `ICON_COMPOSER_TOOL` can select its `ictool`.
811
9Each project uses `text.svg` for the T3 mark and `background.svg` when the background is a vector layer. Additional layers use semantic names that describe their role and placement.
12## macOS
1013
11Run `vp run icons:export` from the repository root to regenerate the tracked iOS, Linux, Windows, and web assets. The development web exports are also copied to `apps/web/public` for the browser favicon and splash screen. Run `vp run icons:check` to verify that the generated assets and public copies match their sources without changing files.
14Icon Composer renders the macOS artwork with design generation 26. The exporter
15fits its full-bleed output to an 824×824 body on a transparent 1024×1024 canvas,
16inset 100px on every side. A black shadow uses 20% opacity, a 12px blur, and an
178px downward offset. Only the shadow extends outside the body's safe area.
1218
13Exporting requires Icon Composer 2 or newer on macOS. The script selects the newest compatible exporter from Xcode or a standalone Icon Composer installation and pins design generation 26. Set `ICON_COMPOSER_TOOL` to the full path of `Icon Composer.app/Contents/Executables/ictool` to override automatic discovery.
19`vp run icons:export` writes `knife/macos-1024.png`; no manual GUI export is
20needed. Desktop packaging derives its ICNS and smaller resource PNG from it.
1421
15## macOS exports
22## Android
1623
17Icon Composer's command-line exporter does not expose the `macOS pre-Tahoe` preset. A plain command-line `macOS` export is full bleed and is not suitable for the desktop app, so the export script intentionally leaves the tracked macOS PNGs unchanged and prints a reminder after every run.
18
19After changing an Icon Composer project, open it in Icon Composer and export the macOS PNG with exactly these settings:
20
21- Platform: `macOS pre-Tahoe`
22- Appearance: `Default`
23- Size: `1024pt`
24- Scale: `1×`
25
26Save the three exports to:
27
28- `dev/app-icon.icon` -> `dev/blueprint-macos-1024.png`
29- `nightly/app-icon.icon` -> `nightly/nightly-macos-1024.png`
30- `prod/app-icon.icon` -> `prod/black-macos-1024.png`
31
32The result must be a 1024×1024 PNG with the classic macOS safe area: the opaque icon body is 824×824, inset 100 pixels on every side, with only the native Icon Composer shadow extending into the surrounding transparent canvas.
33
34To have Codex perform the native exports, paste this prompt into a task opened at the repository root:
35
36```text
37Use [@Computer](plugin://computer-use@openai-bundled) and the Icon Composer app to export the three macOS app icons in this repository.
38
39For each project below, use Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, and Scale: 1×, then save the PNG to the exact destination:
40
41- assets/dev/app-icon.icon -> assets/dev/blueprint-macos-1024.png
42- assets/nightly/app-icon.icon -> assets/nightly/nightly-macos-1024.png
43- assets/prod/app-icon.icon -> assets/prod/black-macos-1024.png
44
45Do not resize, composite, or otherwise post-process the exported PNGs.
46
47Verify every result is 1024×1024 and has the classic macOS safe area: an 824×824 opaque body inset 100px on every side, with only Icon Composer's native shadow extending beyond it.
48```
49
50Do not edit the generated PNG or ICO files directly.
51
52## Android launcher and splash artwork
53
54Android masks the central 72dp of a 108dp adaptive canvas, and the Android 12+ splash screen masks
55the central two thirds of a 288dp canvas, so the Icon Composer exports cannot be used directly:
56their rounded-square silhouette gets framed again and the wordmark is cropped. The Android artwork
57is instead rendered from the same Icon Composer SVG sources by `vp run icons:export:android`:
58
59- `apps/mobile/assets/android-icon-foreground.png`: the shared transparent wordmark, sized to stay
60 inside the safe zone
61- `apps/mobile/assets/android-icon-background-dev.png` and `-nightly.png`: full-bleed variant
62 artwork (blueprint grid and annotations; night sky and clouds). Production uses a solid color.
63- `apps/mobile/assets/android-splash-icon-*.png`: the two layers composed into one 288dp image, so
64 the splash mask reproduces the launcher icon's framing.
65
66Rerun the export after changing a layer SVG. `android-icon-mark.png` remains a flat silhouette for
67Android's monochrome themed icon.
24`vp run icons:export:android` reads the same knife layer and backplate from the
25Icon Composer project. It creates `knife/android-foreground.png` and
26`knife/android-splash.png`, keeping the blade inside Android's circular safe
27area. The adaptive and monochrome icons share the foreground alpha silhouette.
assets/knife/android-foreground.png created
Binary files /dev/null and b/assets/knife/android-foreground.png differ
assets/knife/android-splash.png created
Binary files /dev/null and b/assets/knife/android-splash.png differ
assets/knife/app-icon.icon/Assets/knife.png created
Binary files /dev/null and b/assets/knife/app-icon.icon/Assets/knife.png differ
assets/knife/app-icon.icon/icon.json created+36
......@@ -0,0 +1,36 @@
1{
2 "fill": {
3 "solid": "srgb:0.09020,0.07843,0.13333,1.00000"
4 },
5 "groups": [
6 {
7 "layers": [
8 {
9 "image-name": "knife.png",
10 "name": "Knife",
11 "position": {
12 "scale": 0.65,
13 "translation-in-points": [
14 0,
15 0
16 ]
17 }
18 }
19 ],
20 "shadow": {
21 "kind": "neutral",
22 "opacity": 0.3
23 },
24 "translucency": {
25 "enabled": false,
26 "value": 0.0
27 }
28 }
29 ],
30 "supported-platforms": {
31 "circles": [
32 "watchOS"
33 ],
34 "squares": "shared"
35 }
36}
assets/knife/apple-touch-180.png created
Binary files /dev/null and b/assets/knife/apple-touch-180.png differ
assets/knife/favicon-16x16.png created
Binary files /dev/null and b/assets/knife/favicon-16x16.png differ
assets/knife/favicon-32x32.png created
Binary files /dev/null and b/assets/knife/favicon-32x32.png differ
assets/knife/icon-1024.png created
Binary files /dev/null and b/assets/knife/icon-1024.png differ
assets/knife/icon.ico created
Binary files /dev/null and b/assets/knife/icon.ico differ
assets/knife/macos-1024.png created
Binary files /dev/null and b/assets/knife/macos-1024.png differ
docs/operations/development.md+8
......@@ -206,3 +206,11 @@ passed as `--wsl-runtime`; see the
206206Add `--signed` after configuring the platform credentials in the
207207[release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the
208208[Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR.
209
210### Private fork releases for Apple Silicon
211
212Licensed Name Sans and Name Mono files live in the ignored
213`apps/web/public/fonts/private/` directory. Copy that directory into any other
214build workspace before building with these fonts. The web and desktop builds
215bundle the local files automatically; source checkouts without them use system
216fallback fonts. Keep the font binaries out of source history.
native/browser-secret/main.c+1-1
......@@ -16,7 +16,7 @@ static const SecretSchema chromium_schema = {
1616
1717int main(int argc, char **argv) {
1818 if (argc != 2 || argv[1][0] == '\0') return 64;
19 g_set_application_name("T3 Code");
19 g_set_application_name("The Knife");
2020
2121 GHashTable *attributes = secret_attributes_build(&chromium_schema, "application", argv[1], NULL);
2222 GError *error = NULL;
native/hyprland-snap-shot/src/ipc.rs+3-3
......@@ -139,7 +139,7 @@ pub fn destination(windows: Vec<Window>, pid: u32, title: &str) -> Result<Option
139139 .filter(|w| w.pid == pid && w.title == title && w.mapped && !w.hidden);
140140 let found = matches.next();
141141 if matches.next().is_some() {
142 return Err("More than one T3 Code window matches the capture destination.".into());
142 return Err("More than one The Knife window matches the capture destination.".into());
143143 }
144144 Ok(found)
145145}
......@@ -157,14 +157,14 @@ pub fn activate(pid: u32, title: &str) -> Result<()> {
157157 if request(&lua)?.trim() != "ok"
158158 && request(&format!("/dispatch focuswindow address:0x{address:x}"))?.trim() != "ok"
159159 {
160 return Err("Hyprland could not focus T3 Code.".into());
160 return Err("Hyprland could not focus The Knife.".into());
161161 }
162162 return Ok(());
163163 }
164164 events.get_ref().set_read_timeout(Some(
165165 deadline
166166 .checked_duration_since(Instant::now())
167 .ok_or("T3 Code did not become visible.")?,
167 .ok_or("The Knife did not become visible.")?,
168168 ))?;
169169 let mut event = Vec::new();
170170 if events.by_ref().take(8193).read_until(b'\n', &mut event)? == 0 || event.len() > 8192 {
native/kde-snap-shot/src/main.rs+1-1
......@@ -348,7 +348,7 @@ fn run() -> Result<()> {
348348 ),
349349 )?;
350350 if serde_json::from_str::<serde_json::Value>(&value)?["activated"] != true {
351 return Err("KDE could not identify the T3 Code window to activate".into());
351 return Err("KDE could not identify The Knife window to activate".into());
352352 }
353353 println!("{{\"activated\":true}}");
354354 }
packages/client-runtime/src/connection/compatibility.ts+1-1
......@@ -21,7 +21,7 @@ export function orchestrationProtocolCompatibilityError(
2121 })
2222 : new ConnectionBlockedError({
2323 reason: "unsupported",
24 detail: `This client requires a newer server. Update T3 Code on ${descriptor.label} to connect.`,
24 detail: `This client requires a newer server. Update the server on ${descriptor.label} to connect.`,
2525 ...(canSelfUpdate(descriptor) ? { serverUpdateRequired: true } : {}),
2626 });
2727}
packages/client-runtime/src/connection/outdatedHostUpdate.ts+2-2
......@@ -77,7 +77,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd
7777 ) {
7878 return yield* new OutdatedHostUpdateError({
7979 environmentId,
80 message: `Update T3 Code on ${descriptor.label} manually; it cannot update itself.`,
80 message: `Update the server on ${descriptor.label} manually; it cannot update itself.`,
8181 });
8282 }
8383
......@@ -189,7 +189,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd
189189 if (Option.isNone(resumed)) {
190190 return yield* new OutdatedHostUpdateError({
191191 environmentId,
192 message: `${descriptor.label} did not come back on a compatible T3 Code version.`,
192 message: `${descriptor.label} did not come back on a compatible server version.`,
193193 });
194194 }
195195
packages/client-runtime/src/connection/resolver.test.ts+1-1
......@@ -200,7 +200,7 @@ describe("ConnectionResolver", () => {
200200 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));
201201
202202 expect(error).toMatchObject({ reason: "unsupported" });
203 expect(error.message).toContain("Update T3 Code on Compatible environment");
203 expect(error.message).toContain("Update the server on Compatible environment");
204204 }),
205205 );
206206
packages/client-runtime/src/relay/errorPresentation.ts+1-1
......@@ -95,7 +95,7 @@ export function relayRequestFailureDetail(error: {
9595// coming back, which clears this reason. While it is still reported, the host
9696// is either still off or running a build too old to do that.
9797export const RELAY_TUNNEL_RELEASED_MESSAGE =
98 "Offline for a while, so its T3 Connect tunnel was removed. Start T3 Code on that computer and update it to the latest version to reconnect.";
98 "Offline for a while, so its T3 Connect tunnel was removed. Start the server on that computer and update it to the latest version to reconnect.";
9999
100100/** User-facing text for an offline status, or null when the relay gave no known reason. */
101101export function relayOfflineReasonMessage(
packages/client-runtime/src/state/pullRequestDiffHttp.test.ts+1-1
......@@ -112,7 +112,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {
112112 traceId: "trace-auth-test",
113113 });
114114 expect(error.message).toBe(
115 "This environment session is no longer valid (invalid_credential). Refresh the page or quit and reopen T3 Code.",
115 "This environment session is no longer valid (invalid_credential). Refresh the page or quit and reopen The Knife.",
116116 );
117117 }),
118118 );
packages/client-runtime/src/state/pullRequestDiffHttp.ts+1-1
......@@ -28,7 +28,7 @@ export class PullRequestDiffCredentialRejectedError extends Schema.TaggedError<P
2828 },
2929) {
3030 override get message(): string {
31 return "This environment session is no longer valid (invalid_credential). Refresh the page or quit and reopen T3 Code.";
31 return "This environment session is no longer valid (invalid_credential). Refresh the page or quit and reopen The Knife.";
3232 }
3333}
3434
scripts/build-desktop-artifact.test.ts+7-7
......@@ -273,8 +273,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
273273 });
274274
275275 it("switches desktop packaging product names to nightly for nightly builds", () => {
276 assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Alpha)");
277 assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)");
276 assert.equal(resolveDesktopProductName("0.0.17"), "The Knife");
277 assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "The Knife (Nightly)");
278278 });
279279
280280 it("switches desktop packaging icons to the nightly artwork for nightly versions", () => {
......@@ -702,7 +702,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
702702 "**/*.map",
703703 ]);
704704 assert.deepStrictEqual(mac.dmg, {
705 title: "T3 Code (Alpha) 1.2.3 Installer",
705 title: "The Knife 1.2.3 Installer",
706706 background: "dmg/dmg-background-latest.png",
707707 window: { width: 640, height: 432 },
708708 contents: [
......@@ -717,11 +717,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
717717 // Linux must register the renderer schemes so the generated .desktop
718718 // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links.
719719 assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [
720 { name: "T3 Code", schemes: ["t3code", "t3code-dev"] },
720 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
721721 ]);
722722 // macOS also offers itself as a web browser, so it can be chosen as the default.
723723 assert.deepStrictEqual((mac.mac as Record<string, unknown>).protocols, [
724 { name: "T3 Code", schemes: ["t3code", "t3code-dev"] },
724 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
725725 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
726726 ]);
727727 // macOS lists a default browser only when it also opens web pages as documents.
......@@ -746,7 +746,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
746746 assert.propertyVal(
747747 (mac.mac as Record<string, unknown>).extendInfo as Record<string, unknown>,
748748 "NSLocalNetworkUsageDescription",
749 "T3 Code connects to devices on your local network for remote environments and commands run by terminals and coding agents.",
749 "The Knife connects to devices on your local network for remote environments and commands run by terminals and coding agents.",
750750 );
751751 for (const config of [linux, win]) {
752752 assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);
......@@ -2141,7 +2141,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
21412141 assert.equal(mac.provisioningProfile, "/tmp/t3code.provisionprofile");
21422142 assert.match(String(mac.sign), /[\\/]scripts[\\/]sign-macos\.ts$/);
21432143 assert.deepStrictEqual(mac.protocols, [
2144 { name: "T3 Code", schemes: ["t3code", "t3code-dev"] },
2144 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
21452145 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
21462146 ]);
21472147 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
scripts/build-desktop-artifact.ts+11-11
......@@ -971,7 +971,7 @@ export const DESKTOP_FILE_EXCLUSIONS = [
971971 "!**/node_modules/@cursor/sdk-*/**/*",
972972 "!apps/desktop/prod-resources/cursor-sdk",
973973 "!apps/desktop/prod-resources/cursor-sdk/**/*",
974 // T3 Code always passes the user's installed Claude executable to the SDK,
974 // The Knife always passes the user's installed Claude executable to the SDK,
975975 // so the SDK's optional platform packages (each a ~200MB bundled executable)
976976 // are dead weight. The trailing dash keeps the SDK's own JS package.
977977 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
......@@ -2745,8 +2745,8 @@ export function resolvePackageManagerUserAgent(packageManager: string): string {
27452745
27462746export function resolveDesktopProductName(version: string): string {
27472747 return resolveDesktopUpdateChannel(version) === "nightly"
2748 ? "T3 Code (Nightly)"
2749 : (desktopPackageJson.productName ?? "T3 Code");
2748 ? "The Knife (Nightly)"
2749 : (desktopPackageJson.productName ?? "The Knife");
27502750}
27512751
27522752export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
......@@ -2771,7 +2771,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
27712771 const buildConfig: Record<string, unknown> = {
27722772 appId: DESKTOP_APP_ID,
27732773 productName: resolveDesktopProductName(version),
2774 artifactName: "T3-Code-${version}-${arch}.${ext}",
2774 artifactName: "The-Knife-${version}-${arch}.${ext}",
27752775 electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],
27762776 files: [
27772777 ...DESKTOP_FILE_EXCLUSIONS,
......@@ -2824,9 +2824,9 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
28242824 // macOS 26 offers Messages/Mail codes in every text field unless opted out.
28252825 NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac: true,
28262826 NSLocalNetworkUsageDescription:
2827 "T3 Code connects to devices on your local network for remote environments and commands run by terminals and coding agents.",
2827 "The Knife connects to devices on your local network for remote environments and commands run by terminals and coding agents.",
28282828 NSScreenCaptureUsageDescription:
2829 "T3 Code captures the active window when you use the window capture shortcut.",
2829 "The Knife captures the active window when you use the window capture shortcut.",
28302830 // macOS lists an app under Default web browser only when it opens web
28312831 // pages as documents as well as http and https links (see protocols).
28322832 CFBundleDocumentTypes: [
......@@ -2840,10 +2840,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
28402840 },
28412841 protocols: [
28422842 {
2843 name: "T3 Code",
2843 name: "The Knife",
28442844 schemes: ["t3code", "t3code-dev"],
28452845 },
2846 // Lets people choose T3 Code as their default web browser, which opens
2846 // Lets people choose The Knife as their default web browser, which opens
28472847 // each link in a new thread's browser panel.
28482848 {
28492849 name: "Web site URL",
......@@ -2907,7 +2907,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
29072907 // t3code:// OAuth callbacks to the app.
29082908 protocols: [
29092909 {
2910 name: "T3 Code",
2910 name: "The Knife",
29112911 schemes: ["t3code", "t3code-dev"],
29122912 },
29132913 ],
......@@ -3869,7 +3869,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
38693869 private: true,
38703870 packageManager: rootPackageJson.packageManager,
38713871 description:
3872 "T3 Code is an open-source desktop app for coding agents. Work with your existing agent subscriptions, review code changes, and run commands in your projects. Connect from desktop, web, or mobile to continue working remotely.",
3872 "The Knife is an open-source desktop app for coding agents. Work with your existing agent subscriptions, review code changes, and run commands in your projects. Connect from desktop, web, or mobile to continue working remotely.",
38733873 license: "MIT",
38743874 // Required by the .deb control file.
38753875 homepage: "https://t3.codes",
......@@ -4158,7 +4158,7 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", {
41584158 Flag.optional,
41594159 ),
41604160}).pipe(
4161 Command.withDescription("Build a desktop artifact for T3 Code."),
4161 Command.withDescription("Build a desktop artifact for The Knife."),
41624162 Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)),
41634163);
41644164
scripts/export-android-icons.ts+45-207
......@@ -1,18 +1,5 @@
11#!/usr/bin/env node
22
3// Renders the Android launcher and splash artwork from the Icon Composer SVG sources.
4//
5// Icon Composer exports already contain a rounded-square silhouette, and Android masks
6// the central 72dp of a 108dp adaptive canvas, so exporting them as a foreground produces
7// a double-framed icon with the letters cropped by the mask. Instead, each variant gets a
8// full-bleed background layer (the artwork behind the wordmark) and a shared transparent
9// foreground that keeps the wordmark inside the safe zone.
10//
11// The Android 12+ splash screen masks its icon to a circle covering the central two thirds
12// of a 288dp canvas, which is the same proportion the launcher crops. Composing the two
13// adaptive layers into one 288dp image therefore makes the splash frame the wordmark
14// exactly like the launcher icon does.
15
163import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
174import * as NodeServices from "@effect/platform-node/NodeServices";
185import * as Console from "effect/Console";
......@@ -22,211 +9,62 @@ import * as Path from "effect/Path";
229import * as Schema from "effect/Schema";
2310import sharp from "sharp";
2411
25type IconVariant = "dev" | "nightly" | "prod";
26
27// 108dp at xxxhdpi. Expo's prebuild derives every launcher density bucket from this.
28const ADAPTIVE_CANVAS = 432;
29// 288dp at xxxhdpi: the full Android 12+ splash canvas, so the icon needs no upscaling.
30const SPLASH_CANVAS = 1152;
31// Icon Composer's layer sources use a 128pt viewBox; the wordmark path spans this box.
32const TEXT = { x: 15.53, y: 37, width: 94.5, height: 57 };
33// Wordmark width as a fraction of the 108dp canvas. The visible area is 72dp (66dp
34// guaranteed), so 0.48 leaves the letters at ~72% of the mask with room for the
35// launcher's own zoom effects.
36const WORDMARK_FRACTION = 0.48;
37// Icon Composer positions layers on a 1024pt canvas, with translation relative to center.
38const COMPOSER_CANVAS_PT = 1024;
39const SVG_DENSITY = 300;
40const OUTPUT_DIRECTORY = "apps/mobile/assets";
41// Production has no background artwork, so its splash composes onto the adaptive color.
42const PRODUCTION_BACKGROUND_COLOR = "#000000";
12import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
4313
4414export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()(
4515 "AndroidIconRenderError",
46 { layer: Schema.String, cause: Schema.Defect() },
16 { cause: Schema.Defect() },
4717) {}
4818
49const wordmarkTransform = (size: number) => {
50 const scale = (size * WORDMARK_FRACTION) / TEXT.width;
51 const tx = (size - TEXT.width * scale) / 2 - TEXT.x * scale;
52 const ty = (size - TEXT.height * scale) / 2 - TEXT.y * scale;
53 return `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) scale(${scale.toFixed(4)})`;
54};
55
56const canvasSvg = (size: number, inner: string) =>
57 `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none">${inner}</svg>`;
58
59// The layer sources clip to a 10pt rounded rectangle for the iOS silhouette. Android
60// applies its own mask, so the layer must bleed to the canvas edge.
61const fullBleed = (svg: string) =>
62 svg.replace(/<rect width="128" height="128" rx="10"\/>/, '<rect width="128" height="128"/>');
63
64const rasterize = (layer: string, svg: string, width: number, height = width) =>
65 Effect.tryPromise({
66 try: () =>
67 sharp(Buffer.from(svg), { density: SVG_DENSITY }).resize(width, height).png().toBuffer(),
68 catch: (cause) => new AndroidIconRenderError({ layer, cause }),
69 });
70
71const composite = (
72 layer: string,
73 base: Buffer,
74 overlays: ReadonlyArray<{ input: Buffer; left?: number; top?: number }>,
75) =>
76 Effect.tryPromise({
77 try: () =>
78 sharp(base)
79 .composite([...overlays])
80 .png()
81 .toBuffer(),
82 catch: (cause) => new AndroidIconRenderError({ layer, cause }),
83 });
84
85const solidCanvas = (layer: string, size: number, background: string) =>
86 Effect.tryPromise({
87 try: () =>
88 sharp({ create: { width: size, height: size, channels: 4, background } })
89 .png()
90 .toBuffer(),
91 catch: (cause) => new AndroidIconRenderError({ layer, cause }),
92 });
93
94const readLayerSource = Effect.fn("androidIcons.readLayerSource")(function* (
95 repositoryRoot: string,
96 variant: IconVariant,
97 file: string,
98) {
99 const fs = yield* FileSystem.FileSystem;
100 const path = yield* Path.Path;
101 return yield* fs.readFileString(
102 path.join(repositoryRoot, "assets", variant, "app-icon.icon", "Assets", file),
103 );
104});
105
106const renderForeground = Effect.fn("androidIcons.renderForeground")(function* (
107 repositoryRoot: string,
108 size: number,
109) {
110 const text = yield* readLayerSource(repositoryRoot, "prod", "text.svg");
111 const paths = text.match(/<path[^>]*\/>/g) ?? [];
112 return yield* rasterize(
113 "foreground",
114 canvasSvg(size, `<g transform="${wordmarkTransform(size)}">${paths.join("")}</g>`),
115 size,
116 );
117});
118
119const renderDevelopmentBackground = Effect.fn("androidIcons.renderDevelopmentBackground")(
120 function* (repositoryRoot: string, size: number) {
121 // The annotation layer shares the wordmark's coordinate space, so it is scaled and
122 // centered the same way to keep the dimension lines around the letters.
123 const annotations = yield* readLayerSource(repositoryRoot, "dev", "annotations.svg");
124 const defs = annotations.match(/<defs>[\s\S]*?<\/defs>/)?.[0] ?? "";
125 const body = annotations.replace(/^[\s\S]*?<\/defs>/, "").replace(/<\/svg>\s*$/, "");
126 const paper = yield* readLayerSource(repositoryRoot, "dev", "background.svg");
127 const background = yield* rasterize("dev-background", fullBleed(paper), size);
128 const overlay = yield* rasterize(
129 "dev-annotations",
130 canvasSvg(size, `${defs}<g transform="${wordmarkTransform(size)}">${body}</g>`),
131 size,
132 );
133 return yield* composite("dev-background", background, [{ input: overlay }]);
134 },
19const decodeProject = Schema.decodeEffect(
20 Schema.fromJsonString(Schema.Struct({ fill: Schema.Struct({ solid: Schema.String }) })),
21);
22const decodeColor = Schema.decodeUnknownEffect(
23 Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]),
13524);
136
137const renderNightlyBackground = Effect.fn("androidIcons.renderNightlyBackground")(function* (
138 repositoryRoot: string,
139 size: number,
140) {
141 // Positions mirror assets/nightly/app-icon.icon/icon.json. The SVG blur filter is
142 // dropped because Icon Composer ignores it and it smears at this raster size.
143 const clouds = [
144 { file: "cloud-lower-left.svg", scale: 25, translation: [-309.6375, 268.66077693836917] },
145 {
146 file: "cloud-upper-right.svg",
147 scale: 15,
148 translation: [387.9605131881942, -134.30064713259117],
149 },
150 ] as const;
151 const k = size / COMPOSER_CANVAS_PT;
152 const overlays = yield* Effect.forEach(clouds, (cloud) =>
153 Effect.gen(function* () {
154 const width = Math.round(64 * cloud.scale * k);
155 const height = Math.round(32 * cloud.scale * k);
156 const left = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[0]) * k - width / 2);
157 const top = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[1]) * k - height / 2);
158 const source = yield* readLayerSource(repositoryRoot, "nightly", cloud.file);
159 const png = yield* rasterize(
160 cloud.file,
161 source.replace(/ filter="url\(#soft\)"/, ""),
162 width,
163 height,
164 );
165 const x0 = Math.max(0, left);
166 const y0 = Math.max(0, top);
167 const x1 = Math.min(size, left + width);
168 const y1 = Math.min(size, top + height);
169 const clipped = yield* Effect.tryPromise({
170 try: () =>
171 sharp(png)
172 .extract({ left: x0 - left, top: y0 - top, width: x1 - x0, height: y1 - y0 })
173 .png()
174 .toBuffer(),
175 catch: (cause) => new AndroidIconRenderError({ layer: cloud.file, cause }),
176 });
177 return { input: clipped, left: x0, top: y0 };
178 }),
179 );
180 const sky = yield* readLayerSource(repositoryRoot, "nightly", "background.svg");
181 const background = yield* rasterize("nightly-background", fullBleed(sky), size);
182 return yield* composite("nightly-background", background, overlays);
183});
184
185const renderBackground = Effect.fn("androidIcons.renderBackground")(function* (
186 repositoryRoot: string,
187 variant: IconVariant,
188 size: number,
189) {
190 switch (variant) {
191 case "dev":
192 return yield* renderDevelopmentBackground(repositoryRoot, size);
193 case "nightly":
194 return yield* renderNightlyBackground(repositoryRoot, size);
195 case "prod":
196 return yield* solidCanvas("prod-background", size, PRODUCTION_BACKGROUND_COLOR);
197 }
198});
199
200const renderSplashIcon = Effect.fn("androidIcons.renderSplashIcon")(function* (
201 repositoryRoot: string,
202 variant: IconVariant,
203) {
204 const background = yield* renderBackground(repositoryRoot, variant, SPLASH_CANVAS);
205 const foreground = yield* renderForeground(repositoryRoot, SPLASH_CANVAS);
206 return yield* composite(`${variant}-splash`, background, [{ input: foreground }]);
207});
20825
20926const exportAndroidIcons = Effect.gen(function* () {
21027 const fs = yield* FileSystem.FileSystem;
21128 const path = yield* Path.Path;
21229 const repositoryRoot = path.resolve(import.meta.dirname, "..");
213 const outputs = [
214 ["android-icon-foreground.png", yield* renderForeground(repositoryRoot, ADAPTIVE_CANVAS)],
215 [
216 "android-icon-background-dev.png",
217 yield* renderDevelopmentBackground(repositoryRoot, ADAPTIVE_CANVAS),
218 ],
219 [
220 "android-icon-background-nightly.png",
221 yield* renderNightlyBackground(repositoryRoot, ADAPTIVE_CANVAS),
222 ],
223 ["android-splash-icon-dev.png", yield* renderSplashIcon(repositoryRoot, "dev")],
224 ["android-splash-icon-nightly.png", yield* renderSplashIcon(repositoryRoot, "nightly")],
225 ["android-splash-icon-prod.png", yield* renderSplashIcon(repositoryRoot, "prod")],
226 ] as const;
227 for (const [name, contents] of outputs) {
228 yield* fs.writeFile(path.join(repositoryRoot, OUTPUT_DIRECTORY, name), contents);
229 yield* Console.log(`wrote ${OUTPUT_DIRECTORY}/${name}`);
30 const projectRoot = path.join(repositoryRoot, BRAND_ASSET_PATHS.productionIconComposerProject);
31 const project = yield* decodeProject(
32 yield* fs.readFileString(path.join(projectRoot, "icon.json")),
33 );
34 const [r, g, b, alpha] = yield* decodeColor(
35 project.fill.solid.slice("srgb:".length).split(",").map(Number),
36 );
37 const background = { r: r * 255, g: g * 255, b: b * 255, alpha };
38 const source = yield* fs.readFile(path.join(projectRoot, "Assets", "knife.png"));
39
40 for (const [name, size] of [
41 ["android-foreground.png", 432],
42 ["android-splash.png", 1152],
43 ] as const) {
44 const png = yield* Effect.tryPromise({
45 try: async () => {
46 // Android masks the central two thirds; the blade must stay inside that circle.
47 const foreground = await sharp(source)
48 .resize(Math.round(size * 0.57), Math.round(size * 0.57), { fit: "inside" })
49 .png()
50 .toBuffer();
51 return sharp({
52 create: {
53 width: size,
54 height: size,
55 channels: 4,
56 background: name === "android-foreground.png" ? "transparent" : background,
57 },
58 })
59 .composite([{ input: foreground, gravity: "centre" }])
60 .png()
61 .toBuffer();
62 },
63 catch: (cause) => new AndroidIconRenderError({ cause }),
64 });
65 const output = path.join(repositoryRoot, "assets", "knife", name);
66 yield* fs.writeFile(output, png);
67 yield* Console.log(`wrote ${output}`);
23068 }
23169});
23270
scripts/export-brand-icons.ts+35-63
......@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
1212import * as Stream from "effect/Stream";
1313import { Command, Flag } from "effect/cli";
1414import { ChildProcess, ChildProcessSpawner } from "effect/process";
15import sharp from "sharp";
1516
1617import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts";
1718import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts";
......@@ -41,7 +42,7 @@ const decodeIconComposerVersion = Schema.decodeUnknownEffect(
4142 Schema.fromJsonString(IconComposerVersion),
4243);
4344
44type IconPlatform = "iOS";
45type IconPlatform = "iOS" | "macOS";
4546
4647interface VariantOutputs {
4748 readonly ios: string;
......@@ -204,7 +205,7 @@ export class IconExportAssetsStaleError extends Schema.TaggedError<IconExportAss
204205
205206const ICON_VARIANTS = [
206207 {
207 label: "development",
208 label: "knife",
208209 source: BRAND_ASSET_PATHS.developmentIconComposerProject,
209210 outputs: {
210211 ios: BRAND_ASSET_PATHS.developmentIosIconPng,
......@@ -217,44 +218,8 @@ const ICON_VARIANTS = [
217218 windowsIco: BRAND_ASSET_PATHS.developmentWindowsIconIco,
218219 },
219220 },
220 {
221 label: "preview",
222 source: BRAND_ASSET_PATHS.nightlyIconComposerProject,
223 outputs: {
224 ios: BRAND_ASSET_PATHS.nightlyIosIconPng,
225 macos: BRAND_ASSET_PATHS.nightlyMacIconPng,
226 universal: BRAND_ASSET_PATHS.nightlyLinuxIconPng,
227 appleTouch: BRAND_ASSET_PATHS.nightlyWebAppleTouchIconPng,
228 favicon16: BRAND_ASSET_PATHS.nightlyWebFavicon16Png,
229 favicon32: BRAND_ASSET_PATHS.nightlyWebFavicon32Png,
230 faviconIco: BRAND_ASSET_PATHS.nightlyWebFaviconIco,
231 windowsIco: BRAND_ASSET_PATHS.nightlyWindowsIconIco,
232 },
233 },
234 {
235 label: "production",
236 source: BRAND_ASSET_PATHS.productionIconComposerProject,
237 outputs: {
238 ios: BRAND_ASSET_PATHS.productionIosIconPng,
239 macos: BRAND_ASSET_PATHS.productionMacIconPng,
240 universal: BRAND_ASSET_PATHS.productionLinuxIconPng,
241 appleTouch: BRAND_ASSET_PATHS.productionWebAppleTouchIconPng,
242 favicon16: BRAND_ASSET_PATHS.productionWebFavicon16Png,
243 favicon32: BRAND_ASSET_PATHS.productionWebFavicon32Png,
244 faviconIco: BRAND_ASSET_PATHS.productionWebFaviconIco,
245 windowsIco: BRAND_ASSET_PATHS.productionWindowsIconIco,
246 },
247 },
248221] as const satisfies ReadonlyArray<IconVariant>;
249222
250const MACOS_EXPORT_CODEX_PROMPT = [
251 "Use [@Computer](plugin://computer-use@openai-bundled) and the Icon Composer app to export the three macOS app icons in this repository.",
252 "For each project below, use Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, and Scale: 1×, then save the PNG to the exact destination:",
253 ...ICON_VARIANTS.map((variant) => `- ${variant.source} -> ${variant.outputs.macos}`),
254 "Do not resize, composite, or otherwise post-process the exported PNGs.",
255 "Verify every result is 1024×1024 and has the classic macOS safe area: an 824×824 opaque body inset 100px on every side, with only Icon Composer's native shadow extending beyond it.",
256];
257
258223const RepositoryRoot = Effect.service(Path.Path).pipe(
259224 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
260225 Effect.mapError(
......@@ -586,6 +551,36 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
586551 });
587552
588553 const ios = yield* render("iOS", 1024);
554 const macosFullBleed = yield* render("macOS", 1024);
555 const macos = yield* Effect.tryPromise({
556 try: async () => {
557 const body = await sharp(macosFullBleed).resize(824, 824).png().toBuffer();
558 const shadow = await sharp(body)
559 .ensureAlpha()
560 .linear([0, 0, 0, 0.2], [0, 0, 0, 0])
561 .extend({
562 top: 108,
563 bottom: 92,
564 left: 100,
565 right: 100,
566 background: "transparent",
567 })
568 .blur(12)
569 .png()
570 .toBuffer();
571 return sharp(shadow)
572 .composite([{ input: body, left: 100, top: 100 }])
573 .png()
574 .toBuffer();
575 },
576 catch: (cause) =>
577 new IconExportRenditionError({
578 sourcePath,
579 outputPath: variant.outputs.macos,
580 expectedSize: 1024,
581 cause,
582 }),
583 });
589584 const icoRenditions = yield* Effect.forEach(
590585 WINDOWS_ICON_SIZES,
591586 (size) => render("iOS", size).pipe(Effect.map((contents) => ({ size, contents }))),
......@@ -598,6 +593,7 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
598593
599594 return new Map<string, Buffer>([
600595 [variant.outputs.ios, ios],
596 [variant.outputs.macos, macos],
601597 [variant.outputs.universal, ios],
602598 [variant.outputs.appleTouch, yield* render("iOS", 180)],
603599 [variant.outputs.favicon16, yield* render("iOS", 16)],
......@@ -607,24 +603,6 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
607603 ]);
608604});
609605
610const logManualMacOsExportInstructions = Effect.fn("iconExport.logManualMacOsExportInstructions")(
611 function* () {
612 yield* Console.warn(
613 [
614 "macOS icons require Icon Composer's GUI-only pre-Tahoe preset and were not changed.",
615 "Export each source with Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, Scale: 1×:",
616 ...ICON_VARIANTS.map((variant) => `- ${variant.source} -> ${variant.outputs.macos}`),
617 "See assets/README.md for the complete workflow.",
618 "",
619 "Copy/paste this prompt into Codex to perform the native exports:",
620 "---",
621 ...MACOS_EXPORT_CODEX_PROMPT,
622 "---",
623 ].join("\n"),
624 );
625 },
626);
627
628606const writeAtomically = Effect.fn("iconExport.writeAtomically")(function* (
629607 repositoryRoot: string,
630608 relativePath: string,
......@@ -774,7 +752,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn
774752 });
775753 }
776754 yield* Console.log(`All ${generated.size} generated icon assets are current.`);
777 yield* logManualMacOsExportInstructions();
778755 return;
779756 }
780757
......@@ -784,7 +761,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn
784761 { concurrency: 1, discard: true },
785762 );
786763 yield* Console.log(`Updated ${generated.size} generated icon assets.`);
787 yield* logManualMacOsExportInstructions();
788764});
789765
790766export const exportBrandIconsCommand = Command.make(
......@@ -796,11 +772,7 @@ export const exportBrandIconsCommand = Command.make(
796772 ),
797773 },
798774 ({ check }) => exportBrandIcons(check).pipe(Effect.scoped),
799).pipe(
800 Command.withDescription(
801 "Export development, preview, and production assets from Icon Composer projects.",
802 ),
803);
775).pipe(Command.withDescription("Export The Knife assets from its Icon Composer project."));
804776
805777if (import.meta.main) {
806778 Command.run(exportBrandIconsCommand, { version: "0.0.0" }).pipe(
scripts/lib/brand-assets.test.ts-15
......@@ -83,19 +83,4 @@ describe("brand-assets", () => {
8383 expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly");
8484 expect(resolveWebAssetBrandForPackageVersion("0.0.29-preview.20260723.882")).toBe("nightly");
8585 });
86
87 it("keeps development, nightly, and production icon families separate", () => {
88 expect([
89 BRAND_ASSET_PATHS.developmentIconComposerProject,
90 BRAND_ASSET_PATHS.nightlyIconComposerProject,
91 BRAND_ASSET_PATHS.productionIconComposerProject,
92 ]).toEqual([
93 "assets/dev/app-icon.icon",
94 "assets/nightly/app-icon.icon",
95 "assets/prod/app-icon.icon",
96 ]);
97 expect(BRAND_ASSET_PATHS.developmentDesktopIconPng).toMatch(/^assets\/dev\/blueprint-/);
98 expect(BRAND_ASSET_PATHS.nightlyMacIconPng).toMatch(/^assets\/nightly\/nightly-/);
99 expect(BRAND_ASSET_PATHS.productionMacIconPng).toMatch(/^assets\/prod\/black-/);
100 });
10186});
scripts/lib/brand-assets.ts+27-30
......@@ -1,34 +1,31 @@
11export const BRAND_ASSET_PATHS = {
2 developmentIconComposerProject: "assets/dev/app-icon.icon",
3 developmentIosIconPng: "assets/dev/blueprint-ios-1024.png",
4 developmentUniversalIconPng: "assets/dev/blueprint-universal-1024.png",
5
6 productionIconComposerProject: "assets/prod/app-icon.icon",
7 productionIosIconPng: "assets/prod/black-ios-1024.png",
8 productionMacIconPng: "assets/prod/black-macos-1024.png",
9 productionLinuxIconPng: "assets/prod/black-universal-1024.png",
10 productionWindowsIconIco: "assets/prod/t3-black-windows.ico",
11 productionWebFaviconIco: "assets/prod/t3-black-web-favicon.ico",
12 productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png",
13 productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png",
14 productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png",
15
16 nightlyIconComposerProject: "assets/nightly/app-icon.icon",
17 nightlyIosIconPng: "assets/nightly/nightly-ios-1024.png",
18 nightlyMacIconPng: "assets/nightly/nightly-macos-1024.png",
19 nightlyLinuxIconPng: "assets/nightly/nightly-universal-1024.png",
20 nightlyWindowsIconIco: "assets/nightly/nightly-windows.ico",
21 nightlyWebFaviconIco: "assets/nightly/nightly-web-favicon.ico",
22 nightlyWebFavicon16Png: "assets/nightly/nightly-web-favicon-16x16.png",
23 nightlyWebFavicon32Png: "assets/nightly/nightly-web-favicon-32x32.png",
24 nightlyWebAppleTouchIconPng: "assets/nightly/nightly-web-apple-touch-180.png",
25
26 developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png",
27 developmentWindowsIconIco: "assets/dev/blueprint-windows.ico",
28 developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico",
29 developmentWebFavicon16Png: "assets/dev/blueprint-web-favicon-16x16.png",
30 developmentWebFavicon32Png: "assets/dev/blueprint-web-favicon-32x32.png",
31 developmentWebAppleTouchIconPng: "assets/dev/blueprint-web-apple-touch-180.png",
2 developmentIconComposerProject: "assets/knife/app-icon.icon",
3 developmentIosIconPng: "assets/knife/icon-1024.png",
4 developmentUniversalIconPng: "assets/knife/icon-1024.png",
5 productionIconComposerProject: "assets/knife/app-icon.icon",
6 productionIosIconPng: "assets/knife/icon-1024.png",
7 productionMacIconPng: "assets/knife/macos-1024.png",
8 productionLinuxIconPng: "assets/knife/icon-1024.png",
9 productionWindowsIconIco: "assets/knife/icon.ico",
10 productionWebFaviconIco: "assets/knife/icon.ico",
11 productionWebFavicon16Png: "assets/knife/favicon-16x16.png",
12 productionWebFavicon32Png: "assets/knife/favicon-32x32.png",
13 productionWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
14 nightlyIconComposerProject: "assets/knife/app-icon.icon",
15 nightlyIosIconPng: "assets/knife/icon-1024.png",
16 nightlyMacIconPng: "assets/knife/macos-1024.png",
17 nightlyLinuxIconPng: "assets/knife/icon-1024.png",
18 nightlyWindowsIconIco: "assets/knife/icon.ico",
19 nightlyWebFaviconIco: "assets/knife/icon.ico",
20 nightlyWebFavicon16Png: "assets/knife/favicon-16x16.png",
21 nightlyWebFavicon32Png: "assets/knife/favicon-32x32.png",
22 nightlyWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
23 developmentDesktopIconPng: "assets/knife/macos-1024.png",
24 developmentWindowsIconIco: "assets/knife/icon.ico",
25 developmentWebFaviconIco: "assets/knife/icon.ico",
26 developmentWebFavicon16Png: "assets/knife/favicon-16x16.png",
27 developmentWebFavicon32Png: "assets/knife/favicon-32x32.png",
28 developmentWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
3229} as const;
3330
3431export type WebAssetBrand = "development" | "nightly" | "production";
scripts/mobile-native-client.ts+3-3
......@@ -348,9 +348,9 @@ const main = Command.make(
348348 [
349349 "xcodebuild",
350350 "-workspace",
351 path.join(mobile, "ios/T3CodeDev.xcworkspace"),
351 path.join(mobile, "ios/TheKnifeDev.xcworkspace"),
352352 "-scheme",
353 "T3CodeDev",
353 "TheKnifeDev",
354354 "-configuration",
355355 "Debug",
356356 "-destination",
......@@ -367,7 +367,7 @@ const main = Command.make(
367367 "simctl",
368368 "install",
369369 device,
370 path.join(output, "Build/Products/Debug-iphonesimulator/T3CodeDev.app"),
370 path.join(output, "Build/Products/Debug-iphonesimulator/TheKnifeDev.app"),
371371 ],
372372 true,
373373 );
scripts/mobile-showcase.ts+3-3
......@@ -42,7 +42,7 @@ const SERVER_HOST = "0.0.0.0";
4242const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";
4343const IOS_APP_PATH = NodePath.join(
4444 MOBILE_ROOT,
45 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app",
45 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/TheKnife.app",
4646);
4747const ANDROID_APK_PATH = NodePath.join(
4848 MOBILE_ROOT,
......@@ -707,9 +707,9 @@ async function buildIos(): Promise<string> {
707707 "xcodebuild",
708708 [
709709 "-workspace",
710 NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"),
710 NodePath.join(MOBILE_ROOT, "ios/TheKnife.xcworkspace"),
711711 "-scheme",
712 "T3Code",
712 "TheKnife",
713713 "-configuration",
714714 "Debug",
715715 "-sdk",