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...@@ -26,7 +26,7 @@ Test with meaningful project and thread data. Read the shared
26[SQLite fixture reference](../test-t3-app/references/sqlite-fixtures.md) only26[SQLite fixture reference](../test-t3-app/references/sqlite-fixtures.md) only
27when inspecting or seeding SQLite. Stop the test server before fixture writes.27when inspecting or seeding SQLite. Stop the test server before fixture writes.
2828
29## Launch T3 Code Dev29## Launch The Knife Dev
3030
31From the checkout being tested on the selected device host, run:31From 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...@@ -57,7 +57,7 @@ arguments stored in `agent_device_command` and the Bash array
57 "$agent_device_command" "${agent_device_target_args[@]}"57 "$agent_device_command" "${agent_device_target_args[@]}"
58```58```
5959
60It issues a fresh credential and opens T3 Code Dev's existing pairing route60It issues a fresh credential and opens The Knife Dev's existing pairing route
61through AgentDevice. For a backend on the device host, use61through AgentDevice. For a backend on the device host, use
62`http://127.0.0.1:<server-port>` on iOS or `http://10.0.2.2:<server-port>`62`http://127.0.0.1:<server-port>` on iOS or `http://10.0.2.2:<server-port>`
63on Android. For a remote backend, use its reachable origin.63on Android. For a remote backend, use its reachable origin.
.gitignore+1
...@@ -45,3 +45,4 @@ node_modules/...@@ -45,3 +45,4 @@ node_modules/
4545
46# pnpm content-addressable store; never belongs in the repo.46# pnpm content-addressable store; never belongs in the repo.
47.pnpm-store/47.pnpm-store/
48/apps/web/public/fonts/private/
AGENTS.md+42-11
...@@ -1,4 +1,6 @@...@@ -1,4 +1,6 @@
1# T3 Code1# 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
3T3 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.5T3 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...@@ -104,16 +106,6 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
104106
105For 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.107For 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
117## Documentation109## Documentation
118110
119Most code changes do not need an internal documentation change. Agents can read the code.111Most 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...@@ -126,6 +118,45 @@ Most code changes do not need an internal documentation change. Agents can read
126- 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.118- 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.
127- `docs/operations/` holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides.119- `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
129## Plans and work artifacts160## Plans and work artifacts
130161
131- 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.162- 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,141 +1,7 @@
1# T3 Code1# 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- [Read patches](https://shale.paperclover.net/the-knife/log).
67- 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).
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).
apps/desktop/gnome-extension/captureFeedback.js+1-1
...@@ -110,7 +110,7 @@ export class CaptureFeedback {...@@ -110,7 +110,7 @@ export class CaptureFeedback {
110 check();110 check();
111 }));111 }));
112 if (this._owned(sender) !== session || !target)112 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.");
114 session.target = target;114 session.target = target;
115 Main.activateWindow(target, global.get_current_time());115 Main.activateWindow(target, global.get_current_time());
116 }116 }
apps/desktop/gnome-extension/captureService.js+1-1
...@@ -43,7 +43,7 @@ export class CaptureService {...@@ -43,7 +43,7 @@ export class CaptureService {
43 break;43 break;
44 }44 }
45 }45 }
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.");
47 const pid = options ? await this._getProcessId(sender) : undefined;47 const pid = options ? await this._getProcessId(sender) : undefined;
48 this._checkSession();48 this._checkSession();
49 const snapshot = await this._takeSnapshot(options?.animate ?? false);49 const snapshot = await this._takeSnapshot(options?.animate ?? false);
apps/desktop/gnome-extension/captureService.test.js+3-3
...@@ -23,7 +23,7 @@ function fixture(overrides = {}) {...@@ -23,7 +23,7 @@ function fixture(overrides = {}) {
2323
24it("rejects other clients before looking at the active window", async () => {24it("rejects other clients before looking at the active window", async () => {
25 const { service, snapshots } = fixture();25 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");
27 expect(snapshots()).toBe(0);27 expect(snapshots()).toBe(0);
28 expect(await service.capture(":1.23")).toBe("pixels");28 expect(await service.capture(":1.23")).toBe("pixels");
29});29});
...@@ -38,7 +38,7 @@ it("rechecks the name owner on each capture", async () => {...@@ -38,7 +38,7 @@ it("rechecks the name owner on each capture", async () => {
38 const { service } = fixture({ getNameOwner: async () => owner });38 const { service } = fixture({ getNameOwner: async () => owner });
39 expect(await service.capture(owner)).toBe("pixels");39 expect(await service.capture(owner)).toBe("pixels");
40 owner = ":1.24";40 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");
42});42});
4343
44it("rejects locked and non-Wayland sessions without taking a screenshot", async () => {44it("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 (...@@ -94,7 +94,7 @@ it("rejects concurrent requests and clears busy after a failed capture", async (
94 await expect(service.capture(":1.23")).rejects.toThrow("already in progress");94 await expect(service.capture(":1.23")).rejects.toThrow("already in progress");
95 finish(new Error("gone"));95 finish(new Error("gone"));
96 await expect(first).rejects.toThrow("gone");96 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");
98});98});
9999
100it("prepares focus/effects only after pixels and identity have been captured", async () => {100it("prepares focus/effects only after pixels and identity have been captured", async () => {
apps/desktop/gnome-extension/metadata.json+2-2
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1{1{
2 "uuid": "snap-shot@t3.codes",2 "uuid": "snap-shot@t3.codes",
3 "name": "T3 Code SnapShots",3 "name": "The Knife 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.",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.",
5 "shell-version": ["45", "46", "47", "48", "49", "50"],5 "shell-version": ["45", "46", "47", "48", "49", "50"],
6 "version": 2,6 "version": 2,
7 "url": "https://github.com/pingdotgg/t3code"7 "url": "https://github.com/pingdotgg/t3code"
apps/desktop/package.json+1-1
...@@ -42,5 +42,5 @@...@@ -42,5 +42,5 @@
42 "tailwindcss": "^4.0.0",42 "tailwindcss": "^4.0.0",
43 "vite-plus": "catalog:"43 "vite-plus": "catalog:"
44 },44 },
45 "productName": "T3 Code (Alpha)"45 "productName": "The Knife"
46}46}
apps/desktop/resources/dmg/dmg-background-latest.svg+1-1
...@@ -20,7 +20,7 @@...@@ -20,7 +20,7 @@
20 <svg x="32" y="23" width="30" height="18" viewBox="15.5309 37 94.3941 56.96" fill="#303034">20 <svg x="32" y="23" width="30" height="18" viewBox="15.5309 37 94.3941 56.96" fill="#303034">
21 <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"/>21 <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"/>
22 </svg>22 </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>
24 <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"/>24 <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"/>
25 <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>25 <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>
26</svg>26</svg>
apps/desktop/resources/dmg/dmg-background-nightly.svg+1-1
...@@ -27,7 +27,7 @@...@@ -27,7 +27,7 @@
27 <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"/>27 <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"/>
28 </svg>28 </svg>
29 <text x="608" y="38" fill="#C3C8E5" font-family="Helvetica" font-size="9" font-weight="400" text-anchor="end">NIGHTLY</text>29 <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>
31 <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"/>31 <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"/>
32 <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>32 <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>
33</svg>33</svg>
apps/desktop/resources/linux/com.t3tools.t3code.metainfo.xml+2-2
...@@ -3,14 +3,14 @@...@@ -3,14 +3,14 @@
3 <id>com.t3tools.t3code</id>3 <id>com.t3tools.t3code</id>
4 <metadata_license>CC0-1.0</metadata_license>4 <metadata_license>CC0-1.0</metadata_license>
5 <project_license>MIT</project_license>5 <project_license>MIT</project_license>
6 <name>T3 Code</name>6 <name>The Knife</name>
7 <summary>Desktop GUI for coding agents</summary>7 <summary>Desktop GUI for coding agents</summary>
8 <developer id="com.t3tools">8 <developer id="com.t3tools">
9 <name>T3 Tools Inc.</name>9 <name>T3 Tools Inc.</name>
10 </developer>10 </developer>
11 <description>11 <description>
12 <p>12 <p>
13 T3 Code is an open-source desktop app for coding agents. Work with your13 The Knife is an open-source desktop app for coding agents. Work with your
14 existing agent subscriptions, review code changes, and run commands in14 existing agent subscriptions, review code changes, and run commands in
15 your projects.15 your projects.
16 </p>16 </p>
apps/desktop/scripts/electron-launcher.mjs+9-18
...@@ -1,4 +1,4 @@...@@ -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
3import * as NodeChildProcess from "node:child_process";3import * as NodeChildProcess from "node:child_process";
4import * as NodeFS from "node:fs";4import * as NodeFS from "node:fs";
...@@ -15,19 +15,13 @@ const repoRoot = NodePath.resolve(desktopDir, "..", "..");...@@ -15,19 +15,13 @@ const repoRoot = NodePath.resolve(desktopDir, "..", "..");
15const devBundleIdSuffix = NodePath.basename(repoRoot)15const devBundleIdSuffix = NodePath.basename(repoRoot)
16 .toLowerCase()16 .toLowerCase()
17 .replaceAll(/[^a-z0-9]+/g, "");17 .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";
19const APP_BUNDLE_ID = isDevelopment19const APP_BUNDLE_ID = isDevelopment
20 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`20 ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
21 : "com.t3tools.t3code";21 : "com.t3tools.t3code";
22const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];22const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
23const LAUNCHER_VERSION = 20;23const LAUNCHER_VERSION = 20;
24const developmentMacIconPngPath = NodePath.join(24const macIconPngPath = NodePath.join(repoRoot, "assets", "knife", "macos-1024.png");
25 repoRoot,
26 "assets",
27 "dev",
28 "blueprint-macos-1024.png",
29);
30const productionMacIconPngPath = NodePath.join(repoRoot, "assets", "prod", "black-macos-1024.png");
31// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.25// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
32const hostPlatform = NodeOS.platform();26const hostPlatform = NodeOS.platform();
3327
...@@ -203,13 +197,10 @@ function registerMacLauncherBundle(appBundlePath) {...@@ -203,13 +197,10 @@ function registerMacLauncherBundle(appBundlePath) {
203}197}
204198
205// Bundle-internal paths are macOS paths whatever host builds them.199// Bundle-internal paths are macOS paths whatever host builds them.
206export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) {200export function resolveMacLauncherIconPaths(runtimeDir) {
207 return {201 return {
208 sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath,202 sourceIconPath: macIconPngPath,
209 generatedIconPath: NodePath.posix.join(203 generatedIconPath: NodePath.posix.join(runtimeDir, "icon.icns"),
210 runtimeDir,
211 development ? "icon-dev.icns" : "icon-prod.icns",
212 ),
213 };204 };
214}205}
215206
...@@ -270,8 +261,8 @@ export function resolveMacBundleInfoPlistStrings(executableName) {...@@ -270,8 +261,8 @@ export function resolveMacBundleInfoPlistStrings(executableName) {
270 CFBundleExecutable: executableName,261 CFBundleExecutable: executableName,
271 CFBundleIconFile: "icon.icns",262 CFBundleIconFile: "icon.icns",
272 NSScreenCaptureUsageDescription:263 NSScreenCaptureUsageDescription:
273 "T3 Code captures the active window when you use the snapshot shortcut.",264 "The Knife captures the active window when you use the snapshot shortcut.",
274 NSDocumentsFolderUsageDescription: "T3 Code reads project files you open in the desktop app.",265 NSDocumentsFolderUsageDescription: "The Knife reads project files you open in the desktop app.",
275 };266 };
276}267}
277268
...@@ -404,7 +395,7 @@ function buildMacLauncher(electronBinaryPath) {...@@ -404,7 +395,7 @@ function buildMacLauncher(electronBinaryPath) {
404 if (isDevelopment) {395 if (isDevelopment) {
405 // Keep Electron's native executable inside the branded bundle. Launching the396 // Keep Electron's native executable inside the branded bundle. Launching the
406 // node_modules copy makes macOS associate the process (and Dock label) with397 // 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.
408 // Its conventional executable name also keeps Electron's default-app runtime399 // Its conventional executable name also keeps Electron's default-app runtime
409 // in development mode instead of making app.isPackaged report true.400 // in development mode instead of making app.isPackaged report true.
410 writeDevelopmentEnvironmentScript();401 writeDevelopmentEnvironmentScript();
apps/desktop/scripts/electron-launcher.test.mjs+15-20
...@@ -76,18 +76,18 @@ describe("electron development launcher", () => {...@@ -76,18 +76,18 @@ describe("electron development launcher", () => {
7676
77 it("keeps the native Electron executable name inside the branded macOS bundle", () => {77 it("keeps the native Electron executable name inside the branded macOS bundle", () => {
78 const paths = resolveMacLauncherPaths(78 const paths = resolveMacLauncherPaths(
79 "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",79 "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app",
80 "T3 Code (Dev)",80 "The Knife (Dev)",
81 );81 );
8282
83 assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");83 assert.equal(paths.launcherExecutableName, "The Knife (Dev) Launcher");
84 assert.equal(84 assert.equal(
85 paths.launcherBinaryPath,85 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",
87 );87 );
88 assert.equal(88 assert.equal(
89 paths.runtimeElectronBinaryPath,89 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",
91 );91 );
9292
93 const script = makeDevelopmentLauncherScript({93 const script = makeDevelopmentLauncherScript({
...@@ -98,32 +98,32 @@ describe("electron development launcher", () => {...@@ -98,32 +98,32 @@ describe("electron development launcher", () => {
98 });98 });
99 assert.include(99 assert.include(
100 script,100 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'",
102 );102 );
103 assert.notInclude(script, "node_modules/electron");103 assert.notInclude(script, "node_modules/electron");
104 });104 });
105105
106 it("declares why the macOS app needs protected access", () => {106 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
109 assert.equal(109 assert.equal(
110 values.NSScreenCaptureUsageDescription,110 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.",
112 );112 );
113 assert.equal(113 assert.equal(
114 values.NSDocumentsFolderUsageDescription,114 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.",
116 );116 );
117 });117 });
118118
119 it("ad-hoc signs the complete development app bundle", () => {119 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"), [
121 "--force",121 "--force",
122 "--deep",122 "--deep",
123 "--sign",123 "--sign",
124 "-",124 "-",
125 "--timestamp=none",125 "--timestamp=none",
126 "/runtime/T3 Code (Dev).app",126 "/runtime/The Knife (Dev).app",
127 ]);127 ]);
128 });128 });
129129
...@@ -141,14 +141,9 @@ describe("electron development launcher", () => {...@@ -141,14 +141,9 @@ describe("electron development launcher", () => {
141 }141 }
142 });142 });
143143
144 it("derives launcher icons from canonical development and production assets", () => {144 it("derives launcher icons from the canonical Knife artwork", () => {
145 const development = resolveMacLauncherIconPaths("/runtime", true);145 const icon = resolveMacLauncherIconPaths("/runtime");
146 const production = resolveMacLauncherIconPaths("/runtime", false);146 assert.match(icon.sourceIconPath, /assets[\\/]knife[\\/]macos-1024\.png$/);
147147 assert.equal(icon.generatedIconPath, "/runtime/icon.icns");
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");
153 });148 });
154});149});
apps/desktop/scripts/verify-preload-bundle.mjs+1
...@@ -108,6 +108,7 @@ const executeBundle = (source, sandboxModules) => {...@@ -108,6 +108,7 @@ const executeBundle = (source, sandboxModules) => {
108 {108 {
109 process: sandboxProcess,109 process: sandboxProcess,
110 require: requireSandboxModule,110 require: requireSandboxModule,
111 window: { addEventListener: () => undefined },
111 },112 },
112 {113 {
113 filename: "desktop-preload.cjs",114 filename: "desktop-preload.cjs",
apps/desktop/src/app/DesktopApp.ts+1-1
...@@ -136,7 +136,7 @@ const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupErr...@@ -136,7 +136,7 @@ const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupErr
136 const wasQuitting = yield* Ref.getAndSet(state.quitting, true);136 const wasQuitting = yield* Ref.getAndSet(state.quitting, true);
137 if (!wasQuitting) {137 if (!wasQuitting) {
138 yield* electronDialog.showErrorBox(138 yield* electronDialog.showErrorBox(
139 "T3 Code failed to start",139 "The Knife failed to start",
140 `Stage: ${stage}\n${message}${detail}`,140 `Stage: ${stage}\n${message}${detail}`,
141 );141 );
142 }142 }
apps/desktop/src/app/DesktopAppActivation.ts+1-1
...@@ -171,7 +171,7 @@ export async function startDesktopAppControlServer(input: {...@@ -171,7 +171,7 @@ export async function startDesktopAppControlServer(input: {
171 activeRequestId = parsed.requestId;171 activeRequestId = parsed.requestId;
172 void input.handle(parsed).then(finish, () => {172 void input.handle(parsed).then(finish, () => {
173 finish(173 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."),
175 );175 );
176 });176 });
177 });177 });
apps/desktop/src/app/DesktopAppActivationBroker.ts+4-4
...@@ -45,7 +45,7 @@ export class DesktopAppActivationBroker {...@@ -45,7 +45,7 @@ export class DesktopAppActivationBroker {
45 request(request: DesktopAppActivationRequest): Promise<DesktopAppActivationResponse> {45 request(request: DesktopAppActivationRequest): Promise<DesktopAppActivationResponse> {
46 if (this.#closed) {46 if (this.#closed) {
47 return Promise.resolve(47 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."),
49 );49 );
50 }50 }
51 if (this.#pending.has(request.requestId)) {51 if (this.#pending.has(request.requestId)) {
...@@ -90,7 +90,7 @@ export class DesktopAppActivationBroker {...@@ -90,7 +90,7 @@ export class DesktopAppActivationBroker {
90 failure(90 failure(
91 pending.request.requestId,91 pending.request.requestId,
92 "renderer-unavailable",92 "renderer-unavailable",
93 "The T3 Code window closed before it opened the project.",93 "The Knife closed before opening the project.",
94 ),94 ),
95 );95 );
96 }96 }
...@@ -103,7 +103,7 @@ export class DesktopAppActivationBroker {...@@ -103,7 +103,7 @@ export class DesktopAppActivationBroker {
103103
104 cancel(requestId: string): void {104 cancel(requestId: string): void {
105 this.#settle(105 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."),
107 );107 );
108 }108 }
109109
...@@ -112,7 +112,7 @@ export class DesktopAppActivationBroker {...@@ -112,7 +112,7 @@ export class DesktopAppActivationBroker {
112 this.#renderer = null;112 this.#renderer = null;
113 for (const pending of this.#pending.values()) {113 for (const pending of this.#pending.values()) {
114 this.#settle(114 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."),
116 );116 );
117 }117 }
118 }118 }
apps/desktop/src/app/DesktopAppIdentity.test.ts+27-21
...@@ -218,7 +218,7 @@ describe("DesktopAppIdentity", () => {...@@ -218,7 +218,7 @@ describe("DesktopAppIdentity", () => {
218 yield* identity.configure;218 yield* identity.configure;
219219
220 assert.deepEqual(calls.setName, ["T3 Code Alpha"]);220 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");
222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");222 assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");223 assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
224 // Packaged: the bundle's own icon stands, so a custom one the user224 // Packaged: the bundle's own icon stands, so a custom one the user
...@@ -247,28 +247,34 @@ describe("DesktopAppIdentity", () => {...@@ -247,28 +247,34 @@ describe("DesktopAppIdentity", () => {
247 stage: "Dev",247 stage: "Dev",
248 environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },248 environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
249 },249 },
250 ])("uses a valid native User-Agent product name for $stage", ({ stage, environment }) => {250 ])(
251 const calls: ElectronAppCalls = {251 "preserves the credential identity and valid native User-Agent for $stage",
252 setAboutPanelOptions: [],252 ({ stage, environment }) => {
253 setDockIcon: [],253 const calls: ElectronAppCalls = {
254 setName: [],254 setAboutPanelOptions: [],
255 };255 setDockIcon: [],
256 setName: [],
257 };
256258
257 return withIdentity(259 return withIdentity(
258 Effect.gen(function* () {260 Effect.gen(function* () {
259 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;261 const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
260 yield* identity.configure;262 yield* identity.configure;
261263
262 const runtimeName = calls.setName[0];264 const runtimeName = calls.setName[0];
263 assert.isDefined(runtimeName);265 assert.isDefined(runtimeName);
264 assert.equal(runtimeName, `T3 Code ${stage}`);266 assert.equal(runtimeName, `T3 Code ${stage}`);
265 // RFC 9110's token grammar, after Electron removes ASCII spaces.267 // RFC 9110's token grammar, after Electron removes ASCII spaces.
266 assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);268 assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
267 assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`);269 assert.equal(
268 }),270 calls.setAboutPanelOptions[0]?.applicationName,
269 { calls, environment },271 stage === "Alpha" ? "The Knife" : `The Knife (${stage})`,
270 );272 );
271 });273 }),
274 { calls, environment },
275 );
276 },
277 );
272278
273 it.effect("sets the dock icon only when running unpackaged", () => {279 it.effect("sets the dock icon only when running unpackaged", () => {
274 const calls: ElectronAppCalls = {280 const calls: ElectronAppCalls = {
apps/desktop/src/app/DesktopAppIdentity.ts+2-6
...@@ -91,12 +91,8 @@ export const make = Effect.gen(function* () {...@@ -91,12 +91,8 @@ export const make = Effect.gen(function* () {
9191
92 const configure = Effect.gen(function* () {92 const configure = Effect.gen(function* () {
93 const commitHash = yield* resolveAboutCommitHash;93 const commitHash = yield* resolveAboutCommitHash;
94 // Electron removes spaces from this name to build the native User-Agent94 // Electron's runtime name also identifies the shipped Safe Storage/keyring entry.
95 // product token, but leaves parentheses intact. Keep the runtime name valid95 yield* electronApp.setName(`T3 Code ${environment.branding.stageLabel}`);
96 // without rewriting preview sessions (which breaks Turnstile, #7110).
97 yield* electronApp.setName(
98 `${environment.branding.baseName} ${environment.branding.stageLabel}`,
99 );
100 yield* electronApp.setAboutPanelOptions({96 yield* electronApp.setAboutPanelOptions({
101 applicationName: environment.displayName,97 applicationName: environment.displayName,
102 applicationVersion: environment.appVersion,98 applicationVersion: environment.appVersion,
apps/desktop/src/app/DesktopAssets.test.ts+3-3
...@@ -50,7 +50,7 @@ describe("DesktopAssets", () => {...@@ -50,7 +50,7 @@ describe("DesktopAssets", () => {
50 ),50 ),
51 );51 );
52 const layerFileSystem = FileSystem.layerNoop({52 const layerFileSystem = FileSystem.layerNoop({
53 exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")),53 exists: (path) => Effect.succeed(String(path).includes("/assets/knife/")),
54 });54 });
55 const assets = yield* DesktopAssets.DesktopAssets.pipe(55 const assets = yield* DesktopAssets.DesktopAssets.pipe(
56 Effect.provide(56 Effect.provide(
...@@ -62,8 +62,8 @@ describe("DesktopAssets", () => {...@@ -62,8 +62,8 @@ describe("DesktopAssets", () => {
6262
63 const icons = yield* assets.iconPaths;63 const icons = yield* assets.iconPaths;
6464
65 assert.match(Option.getOrThrow(icons.ico), /assets\/dev\/blueprint-windows\.ico$/);65 assert.match(Option.getOrThrow(icons.ico), /assets\/knife\/icon\.ico$/);
66 assert.match(Option.getOrThrow(icons.png), /assets\/dev\/blueprint-universal-1024\.png$/);66 assert.match(Option.getOrThrow(icons.png), /assets\/knife\/icon-1024\.png$/);
67 assert.isTrue(Option.isNone(icons.icns));67 assert.isTrue(Option.isNone(icons.icns));
68 }),68 }),
69 );69 );
apps/desktop/src/app/DesktopAssets.ts+4-19
...@@ -61,33 +61,18 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func...@@ -61,33 +61,18 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func
61 return Option.none<string>();61 return Option.none<string>();
62});62});
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
77function resolveSourceTreeIconPath(64function resolveSourceTreeIconPath(
78 environment: DesktopEnvironment.DesktopEnvironment["Service"],65 environment: DesktopEnvironment.DesktopEnvironment["Service"],
79 ext: keyof DesktopIconPaths,66 ext: keyof DesktopIconPaths,
80): string | undefined {67): string | undefined {
81 if (environment.isPackaged || ext === "icns") return undefined;68 if (environment.isPackaged || ext === "icns") return undefined;
82 const brand = environment.isDevelopment ? "dev" : "prod";
83 const fileNames = sourceTreeIconFileNames[brand];
84 const fileName =69 const fileName =
85 ext === "ico"70 ext === "ico"
86 ? fileNames.ico71 ? "icon.ico"
87 : environment.platform === "darwin"72 : environment.platform === "darwin"
88 ? fileNames.macPng73 ? "macos-1024.png"
89 : fileNames.universalPng;74 : "icon-1024.png";
90 return environment.path.join(environment.rootDir, "assets", brand, fileName);75 return environment.path.join(environment.rootDir, "assets", "knife", fileName);
91}76}
9277
93const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (78const 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("'", `'"'"'`)}'`;...@@ -24,7 +24,7 @@ const shellWord = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
24const cmdText = (value: string) => value.replaceAll("%", "%%");24const cmdText = (value: string) => value.replaceAll("%", "%%");
25const cmdWord = (value: string) => `"${cmdText(value)}"`;25const 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
29export type CliShimTarget =29export type CliShimTarget =
30 | { readonly kind: "appimage"; readonly appImage: string; readonly executableName: string }30 | { 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<...@@ -93,7 +93,7 @@ export class DesktopEnvironment extends Context.Service<
93 }93 }
94>()("@t3tools/desktop/app/DesktopEnvironment") {}94>()("@t3tools/desktop/app/DesktopEnvironment") {}
9595
96const APP_BASE_NAME = "T3 Code";96const APP_BASE_NAME = "The Knife";
9797
98function resolveDesktopAppStageLabel(input: {98function resolveDesktopAppStageLabel(input: {
99 readonly isDevelopment: boolean;99 readonly isDevelopment: boolean;
...@@ -114,7 +114,7 @@ export function resolveDesktopAppBranding(input: {...@@ -114,7 +114,7 @@ export function resolveDesktopAppBranding(input: {
114 return {114 return {
115 baseName: APP_BASE_NAME,115 baseName: APP_BASE_NAME,
116 stageLabel,116 stageLabel,
117 displayName: `${APP_BASE_NAME} (${stageLabel})`,117 displayName: stageLabel === "Alpha" ? APP_BASE_NAME : `${APP_BASE_NAME} (${stageLabel})`,
118 };118 };
119}119}
120120
apps/desktop/src/app/DesktopWebLinks.ts+2-2
...@@ -6,14 +6,14 @@ import * as Option from "effect/Option";...@@ -6,14 +6,14 @@ import * as Option from "effect/Option";
6import * as ElectronWindow from "../electron/ElectronWindow.ts";6import * as ElectronWindow from "../electron/ElectronWindow.ts";
7import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";7import { 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. */
10export const isWebLink = (value: string) => {10export const isWebLink = (value: string) => {
11 if (!URL.canParse(value)) return false;11 if (!URL.canParse(value)) return false;
12 const { protocol } = new URL(value);12 const { protocol } = new URL(value);
13 return protocol === "http:" || protocol === "https:";13 return protocol === "http:" || protocol === "https:";
14};14};
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. */
17export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path);17export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path);
1818
19/**19/**
apps/desktop/src/backend/DesktopBackendPool.ts+3-3
...@@ -247,7 +247,7 @@ export const layer = Layer.effect(...@@ -247,7 +247,7 @@ export const layer = Layer.effect(
247 );247 );
248 yield* electronDialog.showErrorBox(248 yield* electronDialog.showErrorBox(
249 "WSL backend is still unavailable",249 "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.`,
251 );251 );
252 yield* appSettings.applyWslWindowsFallbackInMemory;252 yield* appSettings.applyWslWindowsFallbackInMemory;
253 return true;253 return true;
...@@ -258,7 +258,7 @@ export const layer = Layer.effect(...@@ -258,7 +258,7 @@ export const layer = Layer.effect(
258 });258 });
259 yield* electronDialog.showErrorBox(259 yield* electronDialog.showErrorBox(
260 "WSL backend couldn't start",260 "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.`,
262 );262 );
263 // Fully disable the WSL backend — both flags, matching the "Switch to263 // Fully disable the WSL backend — both flags, matching the "Switch to
264 // Windows" recovery path — so the manager's next restart re-resolves the264 // Windows" recovery path — so the manager's next restart re-resolves the
...@@ -309,7 +309,7 @@ export const layer = Layer.effect(...@@ -309,7 +309,7 @@ export const layer = Layer.effect(
309 electronDialog309 electronDialog
310 .showErrorBox(310 .showErrorBox(
311 "This T3 home is unavailable",311 "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.",
313 )313 )
314 .pipe(Effect.andThen(electronApp.quit)),314 .pipe(Effect.andThen(electronApp.quit)),
315 });315 });
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts+1-1
...@@ -104,7 +104,7 @@ export const make = Effect.gen(function* () {...@@ -104,7 +104,7 @@ export const make = Effect.gen(function* () {
104 httpBaseUrl: config.httpBaseUrl.href,104 httpBaseUrl: config.httpBaseUrl.href,
105 credential,105 credential,
106 clientMetadata: {106 clientMetadata: {
107 label: "T3 Code Desktop",107 label: "The Knife Desktop",
108 deviceType: "desktop",108 deviceType: "desktop",
109 },109 },
110 }).pipe(110 }).pipe(
apps/desktop/src/electron/WindowsForeground.ts+1-1
...@@ -216,7 +216,7 @@ export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> {...@@ -216,7 +216,7 @@ export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> {
216export async function activateWindowsForeground(handleBuffer: Buffer): Promise<void> {216export async function activateWindowsForeground(handleBuffer: Buffer): Promise<void> {
217 const api = await loadWindowsForegroundApi();217 const api = await loadWindowsForegroundApi();
218 if (activateWindowsForegroundWithApi(handleBuffer, api)) return;218 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.");
220}220}
221221
222export async function isWindowsShellHostedForeground(): Promise<boolean> {222export async function isWindowsShellHostedForeground(): Promise<boolean> {
apps/desktop/src/permissions/MacPermissionHelper.test.ts+8-8
...@@ -61,7 +61,7 @@ vi.mock("electron", async () => {...@@ -61,7 +61,7 @@ vi.mock("electron", async () => {
61 }61 }
62 return {62 return {
63 app: {63 app: {
64 getPath: () => "/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code",64 getPath: () => "/Applications/The Knife (Nightly).app/Contents/MacOS/The Knife",
65 },65 },
66 nativeImage: { createFromPath: mocks.createFromPath },66 nativeImage: { createFromPath: mocks.createFromPath },
67 BrowserWindow: class extends MockWindow {},67 BrowserWindow: class extends MockWindow {},
...@@ -124,11 +124,11 @@ function send(action: string, trusted = true) {...@@ -124,11 +124,11 @@ function send(action: string, trusted = true) {
124124
125describe("macAppBundlePath", () => {125describe("macAppBundlePath", () => {
126 it("resolves bundles with spaces and refuses non-bundle executables", () => {126 it("resolves bundles with spaces and refuses non-bundle executables", () => {
127 expect(macAppBundlePath("/Applications/T3 Code.app/Contents/MacOS/T3 Code")).toBe(127 expect(macAppBundlePath("/Applications/The Knife.app/Contents/MacOS/The Knife")).toBe(
128 "/Applications/T3 Code.app",128 "/Applications/The Knife.app",
129 );129 );
130 expect(macAppBundlePath("/usr/local/bin/electron")).toBeUndefined();130 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();
132 });132 });
133});133});
134it("drags the running app bundle only for the helper's own renderer", async () => {134it("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 () =...@@ -138,11 +138,11 @@ it("drags the running app bundle only for the helper's own renderer", async () =
138 send("drag");138 send("drag");
139 expect(mocks.createFromPath).toHaveBeenCalledWith("/bundle/prod-resources/icon.png");139 expect(mocks.createFromPath).toHaveBeenCalledWith("/bundle/prod-resources/icon.png");
140 expect(mocks.startDrag).toHaveBeenCalledWith({140 expect(mocks.startDrag).toHaveBeenCalledWith({
141 file: "/Applications/T3 Code (Nightly).app",141 file: "/Applications/The Knife (Nightly).app",
142 icon: mocks.createFromPath.mock.results[0]!.value.resize(),142 icon: mocks.createFromPath.mock.results[0]!.value.resize(),
143 });143 });
144 send("finder");144 send("finder");
145 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");145 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/The Knife (Nightly).app");
146});146});
147it("rechecks permissions and releases resources when granted", async () => {147it("rechecks permissions and releases resources when granted", async () => {
148 await open();148 await open();
...@@ -168,7 +168,7 @@ it("does not open for a permission already granted", async () => {...@@ -168,7 +168,7 @@ it("does not open for a permission already granted", async () => {
168});168});
169it("does not show a helper with a missing packaged icon", async () => {169it("does not show a helper with a missing packaged icon", async () => {
170 mocks.createFromPath.mockReturnValueOnce({ isEmpty: () => true });170 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");
172 expect(windows).toHaveLength(0);172 expect(windows).toHaveLength(0);
173});173});
174it("cleans up when the helper page fails to load", async () => {174it("cleans up when the helper page fails to load", async () => {
...@@ -184,7 +184,7 @@ it("offers the Finder fallback when native dragging fails", async () => {...@@ -184,7 +184,7 @@ it("offers the Finder fallback when native dragging fails", async () => {
184 throw new Error("drag failed");184 throw new Error("drag failed");
185 });185 });
186 send("drag");186 send("drag");
187 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");187 expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/The Knife (Nightly).app");
188 expect(windows[0]!.destroyed).toBe(false);188 expect(windows[0]!.destroyed).toBe(false);
189});189});
190190
apps/desktop/src/permissions/MacPermissionHelper.ts+3-3
...@@ -60,8 +60,8 @@ button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; }...@@ -60,8 +60,8 @@ button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; }
60img { width: 32px; height: 32px; pointer-events: none; }60img { width: 32px; height: 32px; pointer-events: none; }
61</style></head><body><main id="panel">61</style></head><body><main id="panel">
62<button id="close" aria-label="Close permission helper">×</button>62<button id="close" aria-label="Close permission helper">×</button>
63<header>↑ Drag T3 Code into the list above</header>63<header>↑ Drag The Knife 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>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>
65</main></body></html>`;65</main></body></html>`;
66}66}
6767
...@@ -95,7 +95,7 @@ export class MacPermissionHelper {...@@ -95,7 +95,7 @@ export class MacPermissionHelper {
95 const appIcon = iconPaths95 const appIcon = iconPaths
96 .map((iconPath) => Electron.nativeImage.createFromPath(iconPath))96 .map((iconPath) => Electron.nativeImage.createFromPath(iconPath))
97 .find((image) => !image.isEmpty());97 .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.");
99 const icon = appIcon.resize({ width: 64, height: 64 });99 const icon = appIcon.resize({ width: 64, height: 64 });
100 const window = new Electron.BrowserWindow({100 const window = new Electron.BrowserWindow({
101 width: 560,101 width: 560,
apps/desktop/src/snapShot/DesktopSnapShot.test.ts+3-3
...@@ -1630,7 +1630,7 @@ it.effect(...@@ -1630,7 +1630,7 @@ it.effect(
1630 const warning = logs.find(1630 const warning = logs.find(
1631 (message) =>1631 (message) =>
1632 Array.isArray(message) &&1632 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",
1634 );1634 );
1635 assert.strictEqual(Array.isArray(warning) ? warning[1] : undefined, activationFailure);1635 assert.strictEqual(Array.isArray(warning) ? warning[1] : undefined, activationFailure);
1636 const pending = yield* decodePendingMetadata(saved);1636 const pending = yield* decodePendingMetadata(saved);
...@@ -3161,7 +3161,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re...@@ -3161,7 +3161,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re
3161 const revoked = yield* service.state;3161 const revoked = yield* service.state;
3162 assert.equal(3162 assert.equal(
3163 revoked.message,3163 revoked.message,
3164 "Allow Screen Recording in System Settings, then restart T3 Code.",3164 "Allow Screen Recording in System Settings, then restart The Knife.",
3165 );3165 );
3166 assert.deepEqual(revoked.macPermissions, { screenRecording: false, accessibility: true });3166 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...@@ -3174,7 +3174,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re
3174 const blocked = yield* service.state;3174 const blocked = yield* service.state;
3175 assert.equal(3175 assert.equal(
3176 blocked.message,3176 blocked.message,
3177 "Allow Screen Recording in System Settings, then restart T3 Code.",3177 "Allow Screen Recording in System Settings, then restart The Knife.",
3178 );3178 );
3179 assert.isFalse(blocked.shortcutRegistered);3179 assert.isFalse(blocked.shortcutRegistered);
31803180
apps/desktop/src/snapShot/DesktopSnapShot.ts+8-8
...@@ -101,11 +101,11 @@ const FLASH_FRAME_INTERVAL_MS = 16;...@@ -101,11 +101,11 @@ const FLASH_FRAME_INTERVAL_MS = 16;
101const FLASH_PEAK_OPACITY = 0.08;101const FLASH_PEAK_OPACITY = 0.08;
102const MAC_SCREEN_CAPTURE_SETTINGS_URL = MAC_PERMISSION_SETTINGS_URLS["screen-recording"];102const MAC_SCREEN_CAPTURE_SETTINGS_URL = MAC_PERMISSION_SETTINGS_URLS["screen-recording"];
103const MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE =103const 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.";
105const MAC_ACCESSIBILITY_PERMISSION_MESSAGE =105const MAC_ACCESSIBILITY_PERMISSION_MESSAGE =
106 "Allow Accessibility in System Settings, then restart T3 Code.";106 "Allow Accessibility in System Settings, then restart The Knife.";
107const MAC_BOTH_PERMISSIONS_MESSAGE =107const 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.";
109const MAC_PERMISSION_MESSAGES = new Set([109const MAC_PERMISSION_MESSAGES = new Set([
110 MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE,110 MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE,
111 MAC_ACCESSIBILITY_PERMISSION_MESSAGE,111 MAC_ACCESSIBILITY_PERMISSION_MESSAGE,
...@@ -187,7 +187,7 @@ export class DesktopSnapShot extends Context.Service<...@@ -187,7 +187,7 @@ export class DesktopSnapShot extends Context.Service<
187 shortcut: SnapShotShortcut,187 shortcut: SnapShotShortcut,
188 ) => Effect.Effect<DesktopSnapShotShortcutAvailability>;188 ) => Effect.Effect<DesktopSnapShotShortcutAvailability>;
189 readonly setShortcutSuppressed: (suppressed: boolean) => Effect.Effect<void>;189 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. */
191 readonly capture: Effect.Effect<void, DesktopSnapShotError>;191 readonly capture: Effect.Effect<void, DesktopSnapShotError>;
192 readonly listPending: Effect.Effect<192 readonly listPending: Effect.Effect<
193 ReadonlyArray<DesktopPendingSnapShot>,193 ReadonlyArray<DesktopPendingSnapShot>,
...@@ -898,7 +898,7 @@ export const make = Effect.gen(function* () {...@@ -898,7 +898,7 @@ export const make = Effect.gen(function* () {
898 const capturedAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));898 const capturedAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
899 if (snapshot.linuxActivationFailure) {899 if (snapshot.linuxActivationFailure) {
900 yield* Effect.logWarning(900 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",
902 snapshot.linuxActivationFailure.cause,902 snapshot.linuxActivationFailure.cause,
903 );903 );
904 }904 }
...@@ -1018,7 +1018,7 @@ export const make = Effect.gen(function* () {...@@ -1018,7 +1018,7 @@ export const make = Effect.gen(function* () {
1018 if (mode === "portal" && niriSocketPath()) {1018 if (mode === "portal" && niriSocketPath()) {
1019 return {1019 return {
1020 available: false,1020 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.",
1022 };1022 };
1023 }1023 }
1024 if (mode === "portal" && isHyprlandCaptureSession()) {1024 if (mode === "portal" && isHyprlandCaptureSession()) {
...@@ -1168,7 +1168,7 @@ export const make = Effect.gen(function* () {...@@ -1168,7 +1168,7 @@ export const make = Effect.gen(function* () {
1168 const { startNiriCaptureShortcut } = await import("./NiriCaptureShortcut.ts");1168 const { startNiriCaptureShortcut } = await import("./NiriCaptureShortcut.ts");
1169 return startNiriCaptureShortcut(linuxAppId, onCurrentShortcut, () => {1169 return startNiriCaptureShortcut(linuxAppId, onCurrentShortcut, () => {
1170 void runPromise(1170 void runPromise(
1171 setShortcutFailure("The Niri capture endpoint disconnected. Restart T3 Code."),1171 setShortcutFailure("The Niri capture endpoint disconnected. Restart The Knife."),
1172 ).catch(() => undefined);1172 ).catch(() => undefined);
1173 });1173 });
1174 }).pipe(1174 }).pipe(
...@@ -1190,7 +1190,7 @@ export const make = Effect.gen(function* () {...@@ -1190,7 +1190,7 @@ export const make = Effect.gen(function* () {
1190 shortcutActionRegistered: registered,1190 shortcutActionRegistered: registered,
1191 shortcutMessage: registered1191 shortcutMessage: registered
1192 ? "Set up the shortcut to add it to your Niri config."1192 ? "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.",
1194 message: null,1194 message: null,
1195 });1195 });
1196 return;1196 return;
apps/desktop/src/snapShot/GnomeCaptureSetup.ts+5-5
...@@ -56,7 +56,7 @@ export async function installGnomeCaptureBundle({ bundle, dataHome }: SetupPaths...@@ -56,7 +56,7 @@ export async function installGnomeCaptureBundle({ bundle, dataHome }: SetupPaths
56 await NodeFSP.readFile(NodePath.join(target, "metadata.json"), "utf8"),56 await NodeFSP.readFile(NodePath.join(target, "metadata.json"), "utf8"),
57 );57 );
58 if (installed.version > metadata.version)58 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.");
60 }60 }
61 const staged = await NodeFSP.mkdtemp(NodePath.join(parent, ".t3-capture-install-"));61 const staged = await NodeFSP.mkdtemp(NodePath.join(parent, ".t3-capture-install-"));
62 let backup: string | undefined;62 let backup: string | undefined;
...@@ -184,7 +184,7 @@ export class GnomeCaptureSetup {...@@ -184,7 +184,7 @@ export class GnomeCaptureSetup {
184 return {184 return {
185 status: "restart-required",185 status: "restart-required",
186 message:186 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.",
188 };188 };
189 if ((installed?.version ?? info.version?.value ?? 0) < bundled.version)189 if ((installed?.version ?? info.version?.value ?? 0) < bundled.version)
190 return {190 return {
...@@ -196,12 +196,12 @@ export class GnomeCaptureSetup {...@@ -196,12 +196,12 @@ export class GnomeCaptureSetup {
196 return {196 return {
197 status: "extensions-disabled",197 status: "extensions-disabled",
198 message:198 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.",
200 };200 };
201 if (info.state?.value === 1)201 if (info.state?.value === 1)
202 return {202 return {
203 status: "enabled",203 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.",
205 };205 };
206 if (info.state?.value === 3 || info.state?.value === 4)206 if (info.state?.value === 3 || info.state?.value === 4)
207 return {207 return {
...@@ -213,7 +213,7 @@ export class GnomeCaptureSetup {...@@ -213,7 +213,7 @@ export class GnomeCaptureSetup {
213 return {213 return {
214 status: "disabled",214 status: "disabled",
215 message:215 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.",
217 };217 };
218 } catch (error) {218 } catch (error) {
219 return {219 return {
apps/desktop/src/snapShot/HyprlandSnapShot.ts+1-1
...@@ -96,7 +96,7 @@ export class HyprlandCaptureSetup {...@@ -96,7 +96,7 @@ export class HyprlandCaptureSetup {
96 const bundle = await regularFile(this.paths.bundle);96 const bundle = await regularFile(this.paths.bundle);
97 if (!bundle)97 if (!bundle)
98 throw new Error(98 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.",
100 );100 );
101 if (!installed.equals(bundle))101 if (!installed.equals(bundle))
102 return {102 return {
apps/desktop/src/snapShot/KdeSnapShot.ts+3-3
...@@ -33,7 +33,7 @@ export function kdeCaptureDesktopEntry(executable: string): string {...@@ -33,7 +33,7 @@ export function kdeCaptureDesktopEntry(executable: string): string {
33 return [33 return [
34 "[Desktop Entry]",34 "[Desktop Entry]",
35 "Type=Application",35 "Type=Application",
36 "Name=T3 Code SnapShots",36 "Name=The Knife SnapShots",
37 "NoDisplay=true",37 "NoDisplay=true",
38 `Exec=${escapeDesktopEntryExecArgument(executable)} check`,38 `Exec=${escapeDesktopEntryExecArgument(executable)} check`,
39 // KService reads this custom property as a KConfig list, not an XDG ';' list.39 // KService reads this custom property as a KConfig list, not an XDG ';' list.
...@@ -116,7 +116,7 @@ export class KdeCaptureSetup {...@@ -116,7 +116,7 @@ export class KdeCaptureSetup {
116 if (!bundle)116 if (!bundle)
117 return {117 return {
118 status: "error",118 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.",
120 };120 };
121 if (!installed.equals(bundle) || entry.toString() !== kdeCaptureDesktopEntry(executable))121 if (!installed.equals(bundle) || entry.toString() !== kdeCaptureDesktopEntry(executable))
122 return {122 return {
...@@ -162,7 +162,7 @@ export class KdeCaptureSetup {...@@ -162,7 +162,7 @@ export class KdeCaptureSetup {
162 const bundle = await regularFile(this.paths.bundle);162 const bundle = await regularFile(this.paths.bundle);
163 if (!bundle)163 if (!bundle)
164 throw new Error(164 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.",
166 );166 );
167 await NodeFSP.mkdir(directory, { recursive: true });167 await NodeFSP.mkdir(directory, { recursive: true });
168 await NodeFSP.mkdir(NodePath.dirname(desktop), { recursive: true });168 await NodeFSP.mkdir(NodePath.dirname(desktop), { recursive: true });
apps/desktop/src/snapShot/LinuxSnapShot.ts+1-1
...@@ -306,7 +306,7 @@ export class LinuxCaptureConnection {...@@ -306,7 +306,7 @@ export class LinuxCaptureConnection {
306 this.bus.requestName(`${appId}.SnapShot`, NameFlag.DO_NOT_QUEUE),306 this.bus.requestName(`${appId}.SnapShot`, NameFlag.DO_NOT_QUEUE),
307 );307 );
308 if (result !== RequestNameReply.PRIMARY_OWNER) {308 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.");
310 }310 }
311 const withFeedback = this.feedbackAvailable && options !== undefined;311 const withFeedback = this.feedbackAvailable && options !== undefined;
312 const reply = await this.call({312 const reply = await this.call({
apps/desktop/src/snapShot/NiriCaptureShortcut.ts+1-1
...@@ -55,7 +55,7 @@ export async function startNiriCaptureShortcut(...@@ -55,7 +55,7 @@ export async function startNiriCaptureShortcut(
55 }),55 }),
56 ]);56 ]);
57 if (result !== RequestNameReply.PRIMARY_OWNER)57 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.");
59 return close;59 return close;
60 } catch (error) {60 } catch (error) {
61 close();61 close();
apps/desktop/src/snapShot/NiriSnapShot.ts+1-1
...@@ -175,7 +175,7 @@ async function activateNiriWindow(path: string, title: string, signal: AbortSign...@@ -175,7 +175,7 @@ async function activateNiriWindow(path: string, title: string, signal: AbortSign
175 (window) => window.pid === process.pid && window.title === title,175 (window) => window.pid === process.pid && window.title === title,
176 );176 );
177 if (matches.length > 1)177 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.");
179 return matches[0];179 return matches[0];
180 });180 });
181 connection.send("EventStream");181 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...@@ -318,7 +318,7 @@ it("guides users to manual desktop settings when the portal cannot open them", a
318 expect(client.hasSession).toBe(true);318 expect(client.hasSession).toBe(true);
319 expect(client.state.shortcutCanRetry).toBe(false);319 expect(client.state.shortcutCanRetry).toBe(false);
320 expect(client.state.shortcutMessage).toBe(320 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.",
322 );322 );
323 await expect(client.configure()).rejects.toThrow("Open your desktop's shortcut settings");323 await expect(client.configure()).rejects.toThrow("Open your desktop's shortcut settings");
324 expect(bus.calls.some((message) => message.member === "ConfigureShortcuts")).toBe(false);324 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 {...@@ -135,7 +135,7 @@ export class PortalCaptureShortcut {
135 throw new Error("Change the capture binding in your Hyprland config, then save it.");135 throw new Error("Change the capture binding in your Hyprland config, then save it.");
136 if (!this.hasSession || this.version < 2)136 if (!this.hasSession || this.version < 2)
137 throw new Error(137 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.",
139 );139 );
140 await this.call({140 await this.call({
141 destination: this.owner,141 destination: this.owner,
...@@ -164,7 +164,7 @@ export class PortalCaptureShortcut {...@@ -164,7 +164,7 @@ export class PortalCaptureShortcut {
164 // This failed session is closing, so retry can register a fresh one.164 // This failed session is closing, so retry can register a fresh one.
165 shortcutCanRetry: !this.managedByHyprland,165 shortcutCanRetry: !this.managedByHyprland,
166 shortcutMessage: this.managedByHyprland166 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."
168 : error instanceof Error168 : error instanceof Error
169 ? error.message169 ? error.message
170 : "Could not register the capture shortcut.",170 : "Could not register the capture shortcut.",
...@@ -304,7 +304,7 @@ export class PortalCaptureShortcut {...@@ -304,7 +304,7 @@ export class PortalCaptureShortcut {
304 shortcutMessage:304 shortcutMessage:
305 this.version >= 2305 this.version >= 2
306 ? "Shortcut permission wasn't granted. Open shortcut permissions to allow it."306 ? "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.",
308 });308 });
309 return undefined;309 return undefined;
310 }310 }
...@@ -327,7 +327,7 @@ export class PortalCaptureShortcut {...@@ -327,7 +327,7 @@ export class PortalCaptureShortcut {
327 shortcutPending: false,327 shortcutPending: false,
328 shortcutMessage: shortcut328 shortcutMessage: shortcut
329 ? "Managed by Hyprland. Add the binding to your config and save it."329 ? "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.",
331 });331 });
332 return;332 return;
333 }333 }
apps/desktop/src/snapShot/SnapShotTransition.ts+1-1
...@@ -114,7 +114,7 @@ function createWindow(...@@ -114,7 +114,7 @@ function createWindow(
114 resizable: false,114 resizable: false,
115 show: false,115 show: false,
116 skipTaskbar: true,116 skipTaskbar: true,
117 title: "T3 Code Snapshot Animation",117 title: "The Knife Snapshot Animation",
118 transparent: true,118 transparent: true,
119 webPreferences: {119 webPreferences: {
120 backgroundThrottling: false,120 backgroundThrottling: false,
apps/desktop/src/snapShot/snapShot.ts+1-1
...@@ -447,7 +447,7 @@ export function hideAndWaitForBlur(window: {...@@ -447,7 +447,7 @@ export function hideAndWaitForBlur(window: {
447 return new Promise((resolve, reject) => {447 return new Promise((resolve, reject) => {
448 const timeout = setTimeout(() => {448 const timeout = setTimeout(() => {
449 window.removeListener("blur", onBlur);449 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."));
451 }, WINDOW_BLUR_TIMEOUT_MS);451 }, WINDOW_BLUR_TIMEOUT_MS);
452 const onBlur = () => {452 const onBlur = () => {
453 clearTimeout(timeout);453 clearTimeout(timeout);
apps/desktop/src/ssh/DesktopSshEnvironment.test.ts+1-1
...@@ -30,7 +30,7 @@ describe("sshEnvironment", () => {...@@ -30,7 +30,7 @@ describe("sshEnvironment", () => {
30 assert.equal(cause.message, "Failed to present SSH password prompt for devbox.");30 assert.equal(cause.message, "Failed to present SSH password prompt for devbox.");
31 assert.equal(31 assert.equal(
32 DesktopSshEnvironment.toSshPasswordPromptError(cause).message,32 DesktopSshEnvironment.toSshPasswordPromptError(cause).message,
33 "T3 Code window is not available for SSH authentication.",33 "The Knife window is not available for SSH authentication.",
34 );34 );
35 });35 });
3636
apps/desktop/src/ssh/DesktopSshEnvironment.ts+1-1
...@@ -97,7 +97,7 @@ export function toSshPasswordPromptError(...@@ -97,7 +97,7 @@ export function toSshPasswordPromptError(
97 break;97 break;
98 case "DesktopSshPromptWindowUnavailableError":98 case "DesktopSshPromptWindowUnavailableError":
99 case "DesktopSshPromptPresentationError":99 case "DesktopSshPromptPresentationError":
100 message = "T3 Code window is not available for SSH authentication.";100 message = "The Knife window is not available for SSH authentication.";
101 break;101 break;
102 case "DesktopSshPromptTimedOutError":102 case "DesktopSshPromptTimedOutError":
103 message = `SSH authentication timed out for ${cause.destination}.`;103 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...@@ -63,7 +63,7 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedError<D
63) {63) {
64 override get message(): string {64 override get message(): string {
65 const request = this.requestId === null ? "before a request id was assigned" : this.requestId;65 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}).`;
67 }67 }
68}68}
6969
apps/desktop/src/window/DesktopApplicationMenu.test.ts+5-5
...@@ -29,7 +29,7 @@ const environmentInput = {...@@ -29,7 +29,7 @@ const environmentInput = {
2929
30const layerElectronApp = Layer.succeed(ElectronApp.ElectronApp, {30const layerElectronApp = Layer.succeed(ElectronApp.ElectronApp, {
31 metadata: Effect.die("unexpected metadata read"),31 metadata: Effect.die("unexpected metadata read"),
32 name: Effect.succeed("T3 Code"),32 name: Effect.succeed("The Knife"),
33 systemLocale: Effect.succeed("en-US"),33 systemLocale: Effect.succeed("en-US"),
34 whenReady: Effect.void,34 whenReady: Effect.void,
35 quit: Effect.void,35 quit: Effect.void,
...@@ -142,21 +142,21 @@ describe("DesktopApplicationMenu", () => {...@@ -142,21 +142,21 @@ describe("DesktopApplicationMenu", () => {
142 const template = yield* Deferred.await(applicationMenuTemplate);142 const template = yield* Deferred.await(applicationMenuTemplate);
143 const applicationMenu = template[0];143 const applicationMenu = template[0];
144 assert.isDefined(applicationMenu);144 assert.isDefined(applicationMenu);
145 assert.equal(applicationMenu.label, "T3 Code (Nightly)");145 assert.equal(applicationMenu.label, "The Knife (Nightly)");
146 if (!Array.isArray(applicationMenu.submenu)) {146 if (!Array.isArray(applicationMenu.submenu)) {
147 throw new Error("Expected application menu submenu to be an array.");147 throw new Error("Expected application menu submenu to be an array.");
148 }148 }
149 assert.equal(149 assert.equal(
150 applicationMenu.submenu.find((item) => item.role === "about")?.label,150 applicationMenu.submenu.find((item) => item.role === "about")?.label,
151 "About T3 Code (Nightly)",151 "About The Knife (Nightly)",
152 );152 );
153 assert.equal(153 assert.equal(
154 applicationMenu.submenu.find((item) => item.role === "hide")?.label,154 applicationMenu.submenu.find((item) => item.role === "hide")?.label,
155 "Hide T3 Code (Nightly)",155 "Hide The Knife (Nightly)",
156 );156 );
157 assert.equal(157 assert.equal(
158 applicationMenu.submenu.find((item) => item.role === "quit")?.label,158 applicationMenu.submenu.find((item) => item.role === "quit")?.label,
159 "Quit T3 Code (Nightly)",159 "Quit The Knife (Nightly)",
160 );160 );
161 }),161 }),
162 );162 );
apps/desktop/src/window/DesktopApplicationMenu.ts+1-1
...@@ -74,7 +74,7 @@ const checkForUpdatesFromMenu = Effect.gen(function* () {...@@ -74,7 +74,7 @@ const checkForUpdatesFromMenu = Effect.gen(function* () {
74 yield* electronDialog.showMessageBox({74 yield* electronDialog.showMessageBox({
75 type: "info",75 type: "info",
76 title: "You're up to date!",76 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.`,
78 buttons: ["OK"],78 buttons: ["OK"],
79 });79 });
80 } else if (updateState.status === "error") {80 } else if (updateState.status === "error") {
apps/desktop/src/wsl/DesktopWslBackend.test.ts+1-1
...@@ -172,7 +172,7 @@ describe("DesktopWslBackend", () => {...@@ -172,7 +172,7 @@ describe("DesktopWslBackend", () => {
172172
173 yield* recordOwnershipFailure();173 yield* recordOwnershipFailure();
174 const refusal = yield* backend.lastPreflightError;174 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");
176 yield* backend.reconcile;176 yield* backend.reconcile;
177 assert.equal(startCount, 1);177 assert.equal(startCount, 1);
178 assert.deepEqual(yield* backend.lastPreflightError, refusal);178 assert.deepEqual(yield* backend.lastPreflightError, refusal);
apps/desktop/src/wsl/DesktopWslBackend.ts+1-1
...@@ -177,7 +177,7 @@ export const layer = Layer.effect(...@@ -177,7 +177,7 @@ export const layer = Layer.effect(
177 Ref.set(177 Ref.set(
178 preflightErrorRef,178 preflightErrorRef,
179 Option.some(179 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.",
181 ),181 ),
182 ),182 ),
183 ),183 ),
apps/desktop/src/wsl/DesktopWslEnvironment.ts+1-1
...@@ -505,7 +505,7 @@ const NODE_PTY_BINARY_MISSING_EXIT_CODE = 4;...@@ -505,7 +505,7 @@ const NODE_PTY_BINARY_MISSING_EXIT_CODE = 4;
505505
506const formatNodePtyProbeFailureReason = (exitCode: number): string | null =>506const formatNodePtyProbeFailureReason = (exitCode: number): string | null =>
507 exitCode === NODE_PTY_BINARY_MISSING_EXIT_CODE507 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."
509 : null;509 : null;
510510
511// Captures the login-shell PATH as `resolvedPath:` so the launch can forward the511// Captures the login-shell PATH as `resolvedPath:` so the launch can forward the
apps/mobile/README.md+5-5
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1# T3 Code Mobile1# The Knife Mobile
22
3> [!WARNING]3> [!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
6## Quickstart6## Quickstart
77
...@@ -10,9 +10,9 @@...@@ -10,9 +10,9 @@
1010
11This app has three variants:11This app has three variants:
1212
13- `development`: Expo dev client, installable side-by-side as `T3 Code Dev`13- `development`: Expo dev client, installable side-by-side as `The Knife Dev`
14- `preview`: persistent internal preview build, installable side-by-side as `T3 Code Preview`14- `preview`: persistent internal preview build, installable side-by-side as `The Knife Preview`
15- `production`: store/release build as `T3 Code`15- `production`: store/release build as `The Knife`
1616
17Run commands from `apps/mobile`.17Run 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-]+)+$/;...@@ -19,8 +19,8 @@ const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
1919
20const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;20const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;
21// Android layers are rendered by scripts/export-android-icons.ts from the Icon Composer sources.21// 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.22// The knife sits inside the adaptive icon safe zone.
23const androidAdaptiveForeground = "./assets/android-icon-foreground.png";23const androidAdaptiveForeground = "../../assets/knife/android-foreground.png";
2424
25if (25if (
26 isIosPersonalTeamBuild &&26 isIosPersonalTeamBuild &&
...@@ -37,11 +37,11 @@ const DEVELOPMENT_ASSETS = {...@@ -37,11 +37,11 @@ const DEVELOPMENT_ASSETS = {
37 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject),37 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject),
38 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),38 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),
39 androidAdaptiveForeground,39 androidAdaptiveForeground,
40 androidAdaptiveBackgroundColor: "#347FF8",40 androidAdaptiveBackgroundColor: "#171422",
41 androidAdaptiveBackgroundImage: "./assets/android-icon-background-dev.png",41 androidAdaptiveBackgroundImage: undefined,
42 androidSplashIcon: "./assets/android-splash-icon-dev.png",42 androidSplashIcon: "../../assets/knife/android-splash.png",
43 androidMonochromeIcon: "./assets/android-icon-mark.png",43 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
44 androidNotificationIcon: "./assets/android-notification-icon.png",44 androidNotificationIcon: "../../assets/knife/android-foreground.png",
45 androidNotificationColor: "#00639B",45 androidNotificationColor: "#00639B",
46} as const;46} as const;
4747
...@@ -50,11 +50,11 @@ const PREVIEW_ASSETS = {...@@ -50,11 +50,11 @@ const PREVIEW_ASSETS = {
50 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject),50 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject),
51 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),51 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),
52 androidAdaptiveForeground,52 androidAdaptiveForeground,
53 androidAdaptiveBackgroundColor: "#111533",53 androidAdaptiveBackgroundColor: "#171422",
54 androidAdaptiveBackgroundImage: "./assets/android-icon-background-nightly.png",54 androidAdaptiveBackgroundImage: undefined,
55 androidSplashIcon: "./assets/android-splash-icon-nightly.png",55 androidSplashIcon: "../../assets/knife/android-splash.png",
56 androidMonochromeIcon: "./assets/android-icon-mark.png",56 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
57 androidNotificationIcon: "./assets/android-notification-icon.png",57 androidNotificationIcon: "../../assets/knife/android-foreground.png",
58 androidNotificationColor: "#7565C7",58 androidNotificationColor: "#7565C7",
59} as const;59} as const;
6060
...@@ -63,17 +63,17 @@ const RELEASE_ASSETS = {...@@ -63,17 +63,17 @@ const RELEASE_ASSETS = {
63 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject),63 iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject),
64 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),64 splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),
65 androidAdaptiveForeground,65 androidAdaptiveForeground,
66 androidAdaptiveBackgroundColor: "#000000",66 androidAdaptiveBackgroundColor: "#171422",
67 androidAdaptiveBackgroundImage: undefined,67 androidAdaptiveBackgroundImage: undefined,
68 androidSplashIcon: "./assets/android-splash-icon-prod.png",68 androidSplashIcon: "../../assets/knife/android-splash.png",
69 androidMonochromeIcon: "./assets/android-icon-mark.png",69 androidMonochromeIcon: "../../assets/knife/android-foreground.png",
70 androidNotificationIcon: "./assets/android-notification-icon.png",70 androidNotificationIcon: "../../assets/knife/android-foreground.png",
71 androidNotificationColor: "#FFFFFF",71 androidNotificationColor: "#FFFFFF",
72} as const;72} as const;
7373
74const VARIANT_CONFIG = {74const VARIANT_CONFIG = {
75 development: {75 development: {
76 appName: "T3 Code Dev",76 appName: "The Knife Dev",
77 scheme: "t3code-dev",77 scheme: "t3code-dev",
78 iosBundleIdentifier: "com.t3tools.t3code.dev",78 iosBundleIdentifier: "com.t3tools.t3code.dev",
79 androidPackage: "com.t3tools.t3code.dev",79 androidPackage: "com.t3tools.t3code.dev",
...@@ -81,7 +81,7 @@ const VARIANT_CONFIG = {...@@ -81,7 +81,7 @@ const VARIANT_CONFIG = {
81 assets: DEVELOPMENT_ASSETS,81 assets: DEVELOPMENT_ASSETS,
82 },82 },
83 preview: {83 preview: {
84 appName: "T3 Code Preview",84 appName: "The Knife Preview",
85 scheme: "t3code-preview",85 scheme: "t3code-preview",
86 iosBundleIdentifier: "com.t3tools.t3code.preview",86 iosBundleIdentifier: "com.t3tools.t3code.preview",
87 androidPackage: "com.t3tools.t3code.preview",87 androidPackage: "com.t3tools.t3code.preview",
...@@ -89,7 +89,7 @@ const VARIANT_CONFIG = {...@@ -89,7 +89,7 @@ const VARIANT_CONFIG = {
89 assets: PREVIEW_ASSETS,89 assets: PREVIEW_ASSETS,
90 },90 },
91 production: {91 production: {
92 appName: "T3 Code",92 appName: "The Knife",
93 scheme: "t3code",93 scheme: "t3code",
94 iosBundleIdentifier: "com.t3tools.t3code",94 iosBundleIdentifier: "com.t3tools.t3code",
95 androidPackage: "com.t3tools.t3code",95 androidPackage: "com.t3tools.t3code",
...@@ -134,7 +134,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [...@@ -134,7 +134,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
134 {134 {
135 name: "SubscriptionUsage",135 name: "SubscriptionUsage",
136 displayName: "Subscription usage",136 displayName: "Subscription usage",
137 description: "Subscription quotas from your connected T3 Code environments.",137 description: "Subscription quotas from your connected environments.",
138 ios: {138 ios: {
139 configuration: {139 configuration: {
140 title: "Subscription usage",140 title: "Subscription usage",
...@@ -185,7 +185,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [...@@ -185,7 +185,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
185 {185 {
186 name: "AgentActivity",186 name: "AgentActivity",
187 displayName: "Agent Activity",187 displayName: "Agent Activity",
188 description: "Shows the current state of active T3 Code agents.",188 description: "Shows the current state of your active agents.",
189 // Live Activity companion; there is no Android presentation for it.189 // Live Activity companion; there is no Android presentation for it.
190 android: null,190 android: null,
191 ios: { supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"] },191 ios: { supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"] },
...@@ -261,7 +261,7 @@ const config: ExpoConfig = {...@@ -261,7 +261,7 @@ const config: ExpoConfig = {
261 `webcredentials:${variant.relyingParty}`,261 `webcredentials:${variant.relyingParty}`,
262 ],262 ],
263 entitlements: {263 entitlements: {
264 "keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],264 "keychain-access-groups": [`$(AppIdentifierPrefix)${iosBundleIdentifier}`],
265 },265 },
266 infoPlist: {266 infoPlist: {
267 UISupportedInterfaceOrientations: [267 UISupportedInterfaceOrientations: [
...@@ -273,8 +273,8 @@ const config: ExpoConfig = {...@@ -273,8 +273,8 @@ const config: ExpoConfig = {
273 NSAllowsArbitraryLoads: true,273 NSAllowsArbitraryLoads: true,
274 },274 },
275 NSLocalNetworkUsageDescription:275 NSLocalNetworkUsageDescription:
276 "Allow T3 Code to connect to T3 Code servers on your local network or tailnet.",276 "Allow The Knife to connect to The Knife servers on your local network or tailnet.",
277 NSPhotoLibraryAddUsageDescription: "Allow T3 Code to save images to your photo library.",277 NSPhotoLibraryAddUsageDescription: "Allow The Knife to save images to your photo library.",
278 // "Audio, AirPlay, and Picture in Picture": the browser screen's system278 // "Audio, AirPlay, and Picture in Picture": the browser screen's system
279 // picture in picture needs it to start and to stay up outside the app.279 // picture in picture needs it to start and to stay up outside the app.
280 UIBackgroundModes: ["audio"],280 UIBackgroundModes: ["audio"],
...@@ -321,6 +321,7 @@ const config: ExpoConfig = {...@@ -321,6 +321,7 @@ const config: ExpoConfig = {
321 favicon: variant.assets.appIcon,321 favicon: variant.assets.appIcon,
322 },322 },
323 plugins: [323 plugins: [
324 ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
324 "expo-asset",325 "expo-asset",
325 [326 [
326 "expo-font",327 "expo-font",
...@@ -359,8 +360,6 @@ const config: ExpoConfig = {...@@ -359,8 +360,6 @@ const config: ExpoConfig = {
359 mode: APP_VARIANT === "development" ? "development" : "production",360 mode: APP_VARIANT === "development" ? "development" : "production",
360 },361 },
361 ],362 ],
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.
364 ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],363 ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],
365 "expo-web-browser",364 "expo-web-browser",
366 [365 [
...@@ -382,7 +381,7 @@ const config: ExpoConfig = {...@@ -382,7 +381,7 @@ const config: ExpoConfig = {
382 [381 [
383 "expo-audio",382 "expo-audio",
384 {383 {
385 microphonePermission: "Allow T3 Code to use your microphone for voice input.",384 microphonePermission: "Allow The Knife to use your microphone for voice input.",
386 recordAudioAndroid: false,385 recordAudioAndroid: false,
387 enableBackgroundPlayback: false,386 enableBackgroundPlayback: false,
388 enableBackgroundRecording: false,387 enableBackgroundRecording: false,
...@@ -391,7 +390,7 @@ const config: ExpoConfig = {...@@ -391,7 +390,7 @@ const config: ExpoConfig = {
391 [390 [
392 "expo-camera",391 "expo-camera",
393 {392 {
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.",
395 microphonePermission: false,394 microphonePermission: false,
396 barcodeScannerEnabled: true,395 barcodeScannerEnabled: true,
397 recordAudioAndroid: false,396 recordAudioAndroid: false,
...@@ -466,7 +465,6 @@ const config: ExpoConfig = {...@@ -466,7 +465,6 @@ const config: ExpoConfig = {
466 "./plugins/withAndroidModernAlertDialog.cjs",465 "./plugins/withAndroidModernAlertDialog.cjs",
467 "./plugins/withAndroidPredictiveBackCompat.cjs",466 "./plugins/withAndroidPredictiveBackCompat.cjs",
468 "./plugins/withAndroidTabletOrientation.cjs",467 "./plugins/withAndroidTabletOrientation.cjs",
469 ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
470 ],468 ],
471 extra: {469 extra: {
472 appVariant: APP_VARIANT,470 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 {...@@ -18,7 +18,7 @@ internal object T3ComposerClipboard {
18 }18 }
19 val records = payload?.optJSONArray("records")19 val records = payload?.optJSONArray("records")
20 if (payload == null || records == null) {20 if (payload == null || records == null) {
21 clipboard.setPrimaryClip(ClipData.newPlainText("T3 Code", text))21 clipboard.setPrimaryClip(ClipData.newPlainText("The Knife", text))
22 return22 return
23 }23 }
24 val all = (0 until records.length()).map { records.getJSONObject(it) }24 val all = (0 until records.length()).map { records.getJSONObject(it) }
...@@ -36,12 +36,12 @@ internal object T3ComposerClipboard {...@@ -36,12 +36,12 @@ internal object T3ComposerClipboard {
36 clipboard.setPrimaryClip(36 clipboard.setPrimaryClip(
37 if (selected.isEmpty()) {37 if (selected.isEmpty()) {
38 ClipData.newPlainText(38 ClipData.newPlainText(
39 "T3 Code",39 "The Knife",
40 text40 text
41 )41 )
42 } else {42 } else {
43 ClipData.newHtmlText(43 ClipData.newHtmlText(
44 "T3 Code",44 "The Knife",
45 text,45 text,
46 "<pre data-t3-context-fragment=\"$encoded\">$escaped</pre>"46 "<pre data-t3-context-fragment=\"$encoded\">$escaped</pre>"
47 )47 )
apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift+2-2
...@@ -14,7 +14,7 @@ final class T3LayoutMetricsView: ExpoView {...@@ -14,7 +14,7 @@ final class T3LayoutMetricsView: ExpoView {
14 registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { (view: T3LayoutMetricsView, _: UITraitCollection) in14 registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { (view: T3LayoutMetricsView, _: UITraitCollection) in
15 view.publishMetrics()15 view.publishMetrics()
16 }16 }
17 #if compiler(>=6.4)17 #if T3_IOS_27_1_SDK
18 if #available(iOS 27.1, *) {18 if #available(iOS 27.1, *) {
19 hasHinge = nil19 hasHinge = nil
20 addInteraction(UIHingeInteraction { [weak self] _, update in20 addInteraction(UIHingeInteraction { [weak self] _, update in
...@@ -67,7 +67,7 @@ final class T3LayoutMetricsView: ExpoView {...@@ -67,7 +67,7 @@ final class T3LayoutMetricsView: ExpoView {
67 }67 }
68 var verticalBarEdge = "none"68 var verticalBarEdge = "none"
69 var regions: [[String: Any]] = []69 var regions: [[String: Any]] = []
70 #if compiler(>=6.4)70 #if T3_IOS_27_1_SDK
71 if #available(iOS 27.1, *) {71 if #available(iOS 27.1, *) {
72 let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft72 let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft
73 switch traitCollection.verticalBarEdge {73 switch traitCollection.verticalBarEdge {
apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec+6
...@@ -15,5 +15,11 @@ Pod::Spec.new do |s|...@@ -15,5 +15,11 @@ Pod::Spec.new do |s|
15 s.pod_target_xcconfig = {15 s.pod_target_xcconfig = {
16 'DEFINES_MODULE' => 'YES',16 'DEFINES_MODULE' => 'YES',
17 }17 }
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
18 s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'24 s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'
19end25end
apps/mobile/plugins/withWidgetLogoAsset.cjs+10-8
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1"use strict";1"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.
4//4//
5// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and5// 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 into6// has no asset support, so this plugin (a) writes a template image set into
7// the generated widget asset catalog and (b) wires that catalog into the widget7// the generated widget asset catalog and (b) wires that catalog into the widget
8// target with an actool build phase.8// target with an actool build phase.
9//9//
...@@ -22,17 +22,16 @@ const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs");...@@ -22,17 +22,16 @@ const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs");
2222
23const TARGET_NAME = "ExpoWidgetsTarget";23const TARGET_NAME = "ExpoWidgetsTarget";
24const CATALOG_NAME = "Assets.xcassets";24const CATALOG_NAME = "Assets.xcassets";
25const IMAGE_SET = "T3Mark.imageset";25const IMAGE_SET = "KnifeMark.imageset";
26const SVG_NAME = "T3Mark.svg";26const IMAGE_NAME = "knife.png";
2727
28const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n";28const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n";
29const IMAGE_SET_CONTENTS =29const IMAGE_SET_CONTENTS =
30 JSON.stringify(30 JSON.stringify(
31 {31 {
32 images: [{ idiom: "universal", filename: SVG_NAME }],32 images: [{ idiom: "universal", filename: IMAGE_NAME }],
33 info: { author: "expo", version: 1 },33 info: { author: "expo", version: 1 },
34 properties: {34 properties: {
35 "preserves-vector-representation": true,
36 "template-rendering-intent": "template",35 "template-rendering-intent": "template",
37 },36 },
38 },37 },
...@@ -44,13 +43,16 @@ function withAssetFiles(config) {...@@ -44,13 +43,16 @@ function withAssetFiles(config) {
44 return withDangerousMod(config, [43 return withDangerousMod(config, [
45 "ios",44 "ios",
46 (cfg) => {45 (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 );
48 const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME);50 const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME);
49 const imageSetDir = path.join(catalogDir, IMAGE_SET);51 const imageSetDir = path.join(catalogDir, IMAGE_SET);
50 fs.mkdirSync(imageSetDir, { recursive: true });52 fs.mkdirSync(imageSetDir, { recursive: true });
51 fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS);53 fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS);
52 fs.writeFileSync(path.join(imageSetDir, "Contents.json"), IMAGE_SET_CONTENTS);54 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));
54 return cfg;56 return cfg;
55 },57 },
56 ]);58 ]);
apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs+1
...@@ -4,6 +4,7 @@ module.exports = function withoutIosPersonalTeamCapabilities(config) {...@@ -4,6 +4,7 @@ module.exports = function withoutIosPersonalTeamCapabilities(config) {
4 return withEntitlementsPlist(config, (modConfig) => {4 return withEntitlementsPlist(config, (modConfig) => {
5 delete modConfig.modResults["aps-environment"];5 delete modConfig.modResults["aps-environment"];
6 delete modConfig.modResults["com.apple.developer.applesignin"];6 delete modConfig.modResults["com.apple.developer.applesignin"];
7 delete modConfig.modResults["com.apple.developer.associated-domains"];
7 delete modConfig.modResults["com.apple.security.application-groups"];8 delete modConfig.modResults["com.apple.security.application-groups"];
8 return modConfig;9 return modConfig;
9 });10 });
apps/mobile/src/Stack.tsx+1-1
...@@ -280,7 +280,7 @@ const SettingsContentStack = createV5SheetStackNavigator({...@@ -280,7 +280,7 @@ const SettingsContentStack = createV5SheetStackNavigator({
280 SettingsAbout: createNativeStackScreen({280 SettingsAbout: createNativeStackScreen({
281 screen: SettingsAboutRouteScreen,281 screen: SettingsAboutRouteScreen,
282 linking: "about",282 linking: "about",
283 options: { title: "About T3 Code" },283 options: { title: "About The Knife" },
284 }),284 }),
285 SettingsEnvironmentNew: createNativeStackScreen({285 SettingsEnvironmentNew: createNativeStackScreen({
286 screen: ConnectionsNewRouteScreen,286 screen: ConnectionsNewRouteScreen,
apps/mobile/src/components/BrandMark.tsx+3-4
...@@ -3,7 +3,7 @@ import { Image } from "expo-image";...@@ -3,7 +3,7 @@ import { Image } from "expo-image";
3import { View } from "react-native";3import { View } from "react-native";
44
5import { AppText as Text } from "./AppText";5import { AppText as Text } from "./AppText";
6import { T3_CODE_BRAND_MARK_SOURCE } from "./brandAssets";6import { KNIFE_BRAND_MARK_SOURCE } from "./brandAssets";
77
8const appVariant = Constants.expoConfig?.extra?.appVariant;8const appVariant = Constants.expoConfig?.extra?.appVariant;
9const DEFAULT_STAGE_LABEL =9const DEFAULT_STAGE_LABEL =
...@@ -17,17 +17,16 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab...@@ -17,17 +17,16 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab
17 return (17 return (
18 <View className="flex-row items-center gap-3">18 <View className="flex-row items-center gap-3">
19 <Image19 <Image
20 source={T3_CODE_BRAND_MARK_SOURCE}20 source={KNIFE_BRAND_MARK_SOURCE}
21 accessibilityIgnoresInvertColors21 accessibilityIgnoresInvertColors
22 style={{22 style={{
23 width: iconSize,23 width: iconSize,
24 height: iconSize,24 height: iconSize,
25 borderRadius: compact ? 10 : 14,
26 }}25 }}
27 />26 />
28 <View className="gap-1">27 <View className="gap-1">
29 <View className="flex-row items-center gap-2">28 <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>
31 <View className="rounded-full bg-subtle px-2 py-1">30 <View className="rounded-full bg-subtle px-2 py-1">
32 <Text className="text-3xs font-t3-bold tracking-[1.1px] uppercase text-foreground-muted">31 <Text className="text-3xs font-t3-bold tracking-[1.1px] uppercase text-foreground-muted">
33 {stageLabel}32 {stageLabel}
apps/mobile/src/components/CompactBrandTitle.tsx+4-4
...@@ -4,7 +4,7 @@ import type { NativeStackNavigationOptions } from "@react-navigation/native-stac...@@ -4,7 +4,7 @@ import type { NativeStackNavigationOptions } from "@react-navigation/native-stac
4import { Platform, View } from "react-native";4import { Platform, View } from "react-native";
55
6import { AppText as Text } from "./AppText";6import { AppText as Text } from "./AppText";
7import { T3Wordmark } from "./T3Wordmark";7import { KnifeMark } from "./KnifeMark";
8import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";8import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
9import { resolveMobileStageLabel } from "../lib/mobileBranding";9import { resolveMobileStageLabel } from "../lib/mobileBranding";
10import { useAndroidControlSizing } from "./useAndroidControlSizing";10import { useAndroidControlSizing } from "./useAndroidControlSizing";
...@@ -34,19 +34,19 @@ export function CompactBrandTitle(...@@ -34,19 +34,19 @@ export function CompactBrandTitle(
34 return (34 return (
35 <View35 <View
36 aria-level={1}36 aria-level={1}
37 accessibilityLabel={props.accessibilityLabel ?? "T3 Code, Threads"}37 accessibilityLabel={props.accessibilityLabel ?? "The Knife, Threads"}
38 accessible38 accessible
39 role="heading"39 role="heading"
40 className="flex-row items-center gap-1.5"40 className="flex-row items-center gap-1.5"
41 style={[{ marginLeft: titleOffset }, Platform.OS === "android" && { gap: 5.25 * scale }]}41 style={[{ marginLeft: titleOffset }, Platform.OS === "android" && { gap: 5.25 * scale }]}
42 >42 >
43 <T3Wordmark colorClassName="accent-icon" height={Math.round(15 * scale)} />43 <KnifeMark height={Math.round(26 * scale)} />
44 <Text44 <Text
45 allowFontScaling={props.allowFontScaling}45 allowFontScaling={props.allowFontScaling}
46 className="font-t3-medium text-foreground-muted"46 className="font-t3-medium text-foreground-muted"
47 style={{ fontSize: 21 * scale, letterSpacing: -0.5 * scale }}47 style={{ fontSize: 21 * scale, letterSpacing: -0.5 * scale }}
48 >48 >
49 Code49 The Knife
50 </Text>50 </Text>
51 <View51 <View
52 className="rounded-full bg-subtle px-1.5 py-0.5"52 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 @@...@@ -1,10 +1 @@
1import Constants from "expo-constants";1export const KNIFE_BRAND_MARK_SOURCE = require("../../../../assets/knife/apple-touch-180.png");
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");
apps/mobile/src/features/agent-awareness/remoteRegistration.ts+1-1
...@@ -534,7 +534,7 @@ function armAgentAwarenessLiveActivityForLocalWorkNow(input: {...@@ -534,7 +534,7 @@ function armAgentAwarenessLiveActivityForLocalWorkNow(input: {
534 const nowIso = new Date(Date.now()).toISOString();534 const nowIso = new Date(Date.now()).toISOString();
535 const activity = startAgentLiveActivity(535 const activity = startAgentLiveActivity(
536 {536 {
537 title: "T3 Code",537 title: "The Knife",
538 subtitle: "Agent work in progress",538 subtitle: "Agent work in progress",
539 activeCount: 1,539 activeCount: 1,
540 updatedAt: nowIso,540 updatedAt: nowIso,
apps/mobile/src/features/diagnostics/crash-log-model.test.ts+2-2
...@@ -61,14 +61,14 @@ describe("formatStartupCrashReport", () => {...@@ -61,14 +61,14 @@ describe("formatStartupCrashReport", () => {
61 version: "1.1.1",61 version: "1.1.1",
62 build: "56",62 build: "56",
63 });63 });
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);
65 expect(report).toContain("2026-09-13T05:35:52.000Z");65 expect(report).toContain("2026-09-13T05:35:52.000Z");
66 expect(report).toContain("at NewTaskFlowProvider");66 expect(report).toContain("at NewTaskFlowProvider");
67 });67 });
6868
69 it("says so when nothing was recorded", () => {69 it("says so when nothing was recorded", () => {
70 expect(formatStartupCrashReport([], { version: "1.1.1", build: "56" })).toBe(70 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.",
72 );72 );
73 });73 });
74});74});
apps/mobile/src/features/diagnostics/crash-log-model.ts+1-1
...@@ -72,7 +72,7 @@ export function formatStartupCrashReport(...@@ -72,7 +72,7 @@ export function formatStartupCrashReport(
72 records: ReadonlyArray<StartupCrashRecord>,72 records: ReadonlyArray<StartupCrashRecord>,
73 app: { readonly version: string; readonly build: string },73 app: { readonly version: string; readonly build: string },
74): string {74): string {
75 const header = `T3 Code ${app.version} (${app.build})`;75 const header = `The Knife ${app.version} (${app.build})`;
76 if (records.length === 0) return `${header}\nNo startup crashes recorded.`;76 if (records.length === 0) return `${header}\nNo startup crashes recorded.`;
77 return [77 return [
78 header,78 header,
apps/mobile/src/features/pull-requests/MaterialPullRequestToolbar.android.tsx+1-1
...@@ -69,7 +69,7 @@ export function MaterialPullRequestToolbar(props: { readonly onRequestVisibility...@@ -69,7 +69,7 @@ export function MaterialPullRequestToolbar(props: { readonly onRequestVisibility
69 brand={69 brand={
70 <CompactBrandTitle70 <CompactBrandTitle
71 allowFontScaling={false}71 allowFontScaling={false}
72 accessibilityLabel="T3 Code, Pull requests"72 accessibilityLabel="The Knife, Pull requests"
73 />73 />
74 }74 }
75 onPress={() =>75 onPress={() =>
apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts+1-1
...@@ -23,7 +23,7 @@ export function pullRequestAgentPrompt(task: PullRequestAgentTask): string {...@@ -23,7 +23,7 @@ export function pullRequestAgentPrompt(task: PullRequestAgentTask): string {
23 case "conflicts":23 case "conflicts":
24 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.";24 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.";
25 case "watch":25 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.";
27 }27 }
28}28}
2929
apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx+1-1
...@@ -20,7 +20,7 @@ export function SettingsAboutRouteScreen() {...@@ -20,7 +20,7 @@ export function SettingsAboutRouteScreen() {
20 const insets = useSafeAreaInsets();20 const insets = useSafeAreaInsets();
2121
22 return (22 return (
23 <SettingsScreen title="About T3 Code">23 <SettingsScreen title="About The Knife">
24 <ScrollView24 <ScrollView
25 contentInsetAdjustmentBehavior="automatic"25 contentInsetAdjustmentBehavior="automatic"
26 showsVerticalScrollIndicator={false}26 showsVerticalScrollIndicator={false}
apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx+3-3
...@@ -113,7 +113,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm...@@ -113,7 +113,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
113 return;113 return;
114 Alert.alert(114 Alert.alert(
115 `Update ${environment?.environmentLabel ?? "environment"}?`,115 `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.`,
117 [117 [
118 { text: "Cancel", style: "cancel" },118 { text: "Cancel", style: "cancel" },
119 {119 {
...@@ -227,7 +227,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm...@@ -227,7 +227,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
227 {notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null}227 {notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null}
228 {config ? (228 {config ? (
229 <>229 <>
230 <SettingsSection title="T3 Code">230 <SettingsSection title="The Knife">
231 <View className="gap-1 p-4">231 <View className="gap-1 p-4">
232 <Text className="text-base text-foreground">Version {version}</Text>232 <Text className="text-base text-foreground">Version {version}</Text>
233 {running ? (233 {running ? (
...@@ -252,7 +252,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm...@@ -252,7 +252,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
252 <Text className="text-sm text-foreground-muted">252 <Text className="text-sm text-foreground-muted">
253 {capabilities?.serverSelfUpdate === "desktop-managed"253 {capabilities?.serverSelfUpdate === "desktop-managed"
254 ? "Update the desktop app on this machine."254 ? "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."}
256 </Text>256 </Text>
257 ) : null}257 ) : null}
258 </View>258 </View>
apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx+1-1
...@@ -166,7 +166,7 @@ export function SettingsMicrophoneRouteScreen() {...@@ -166,7 +166,7 @@ export function SettingsMicrophoneRouteScreen() {
166 <Text className="px-2 text-sm text-foreground-muted">166 <Text className="px-2 text-sm text-foreground-muted">
167 {permission === "ask"167 {permission === "ask"
168 ? "Voice input needs microphone access before it can record or list your microphones."168 ? "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."}
170 </Text>170 </Text>
171 </>171 </>
172 ) : null}172 ) : null}
apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx+2-2
...@@ -338,7 +338,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {...@@ -338,7 +338,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {
338338
339 Alert.alert(339 Alert.alert(
340 "Disable notifications",340 "Disable notifications",
341 "Open system Settings to disable notifications for T3 Code.",341 "Open system Settings to disable notifications for The Knife.",
342 [342 [
343 { text: "Cancel", style: "cancel" },343 { text: "Cancel", style: "cancel" },
344 { text: "Open Settings", onPress: () => void Linking.openSettings() },344 { text: "Open Settings", onPress: () => void Linking.openSettings() },
...@@ -492,7 +492,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {...@@ -492,7 +492,7 @@ function ConfiguredSettingsNotificationsRouteScreen() {
492 void openAndroidLiveUpdateSettings().catch(() => {492 void openAndroidLiveUpdateSettings().catch(() => {
493 Alert.alert(493 Alert.alert(
494 "Couldn't open Settings",494 "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.",
496 );496 );
497 });497 });
498 }}498 }}
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+1-1
...@@ -215,7 +215,7 @@ function SettingsIndexSections() {...@@ -215,7 +215,7 @@ function SettingsIndexSections() {
215215
216 <SettingsSection title="App">216 <SettingsSection title="App">
217 <SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />217 <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" />
219 </SettingsSection>219 </SettingsSection>
220 </>220 </>
221 );221 );
apps/mobile/src/features/showcase/showcaseAgentActivity.ts+1-1
...@@ -74,7 +74,7 @@ export function buildShowcaseAgentActivity(...@@ -74,7 +74,7 @@ export function buildShowcaseAgentActivity(
74 });74 });
75 }75 }
76 return {76 return {
77 title: "T3 Code",77 title: "The Knife",
78 subtitle: "Agent work in progress",78 subtitle: "Agent work in progress",
79 activeCount: rows.filter((row) => ACTIVE_PHASES.has(row.phase)).length,79 activeCount: rows.filter((row) => ACTIVE_PHASES.has(row.phase)).length,
80 updatedAt: new Date(now).toISOString(),80 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...@@ -636,7 +636,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
636 armAgentAwarenessLiveActivityForLocalWork({636 armAgentAwarenessLiveActivityForLocalWork({
637 environmentId: props.environmentId,637 environmentId: props.environmentId,
638 threadTitle: props.selectedThread.title,638 threadTitle: props.selectedThread.title,
639 projectTitle: props.environmentLabel ?? "T3 Code",639 projectTitle: props.environmentLabel ?? "The Knife",
640 });640 });
641 } finally {641 } finally {
642 inFlightThreadIdsRef.current.delete(threadKey);642 inFlightThreadIdsRef.current.delete(threadKey);
apps/mobile/src/features/threads/ThreadFeed.tsx+1-1
...@@ -1200,7 +1200,7 @@ function renderFeedEntry(...@@ -1200,7 +1200,7 @@ function renderFeedEntry(
1200 </Text>1200 </Text>
1201 ) : presentation.attribution === "t3code" ? (1201 ) : presentation.attribution === "t3code" ? (
1202 <Text className="mb-1 pr-1 font-t3-medium text-2xs text-foreground-muted opacity-60">1202 <Text className="mb-1 pr-1 font-t3-medium text-2xs text-foreground-muted opacity-60">
1203 Sent by T3 Code1203 Sent by The Knife
1204 </Text>1204 </Text>
1205 ) : presentation.attribution === "agent" ? (1205 ) : presentation.attribution === "agent" ? (
1206 <AgentMessageAttribution1206 <AgentMessageAttribution
apps/mobile/src/features/threads/thread-work-log.tsx+2-2
...@@ -57,7 +57,7 @@ import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity";...@@ -57,7 +57,7 @@ import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity";
57import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";57import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";
5858
59import { AppText as Text } from "../../components/AppText";59import { AppText as Text } from "../../components/AppText";
60import { T3Wordmark } from "../../components/T3Wordmark";60import { KnifeMark } from "../../components/KnifeMark";
61import { BrowserAppIcon } from "../../components/BrowserAppIcon";61import { BrowserAppIcon } from "../../components/BrowserAppIcon";
62import { cn } from "../../lib/cn";62import { cn } from "../../lib/cn";
63import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";63import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";
...@@ -131,7 +131,7 @@ function WorkLogIcon(props: {...@@ -131,7 +131,7 @@ function WorkLogIcon(props: {
131 const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName;131 const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName;
132 if (props.icon === "t3-code") {132 if (props.icon === "t3-code") {
133 return (133 return (
134 <T3Wordmark height={10} {...(colorClassName ? { colorClassName } : { color: props.color })} />134 <KnifeMark height={14} />
135 );135 );
136 }136 }
137 if (props.icon === "computer") {137 if (props.icon === "computer") {
apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx+2-2
...@@ -42,7 +42,7 @@ function WorkspaceRootTitle(props: {...@@ -42,7 +42,7 @@ function WorkspaceRootTitle(props: {
42 <View style={{ width: props.maxWidth, minHeight: 32, justifyContent: "center" }}>42 <View style={{ width: props.maxWidth, minHeight: 32, justifyContent: "center" }}>
43 <WorkspaceConnectionTitle43 <WorkspaceConnectionTitle
44 brand={44 brand={
45 <CompactBrandTitle accessibilityLabel={`T3 Code, ${TAB_TITLES[state.selectedTab]}`} />45 <CompactBrandTitle accessibilityLabel={`The Knife, ${TAB_TITLES[state.selectedTab]}`} />
46 }46 }
47 maxWidth={props.maxWidth}47 maxWidth={props.maxWidth}
48 onPress={props.onOpenEnvironments}48 onPress={props.onOpenEnvironments}
...@@ -114,7 +114,7 @@ function WorkspaceRootContent() {...@@ -114,7 +114,7 @@ function WorkspaceRootContent() {
114 />114 />
115 ) : (115 ) : (
116 <AndroidScreenHeader116 <AndroidScreenHeader
117 title="T3 Code"117 title="The Knife"
118 titleContent={renderHeaderTitle()}118 titleContent={renderHeaderTitle()}
119 actions={[119 actions={[
120 {120 {
apps/mobile/src/lib/authClientMetadata.ts+1-1
...@@ -7,7 +7,7 @@ export function authClientMetadata(appVersion?: string): AuthClientPresentationM...@@ -7,7 +7,7 @@ export function authClientMetadata(appVersion?: string): AuthClientPresentationM
7 const deviceModel = Device.modelName?.trim();7 const deviceModel = Device.modelName?.trim();
88
9 return {9 return {
10 label: "T3 Code Mobile",10 label: "The Knife Mobile",
11 deviceType:11 deviceType:
12 Device.deviceType === Device.DeviceType.TABLET12 Device.deviceType === Device.DeviceType.TABLET
13 ? "tablet"13 ? "tablet"
apps/mobile/src/lib/connection.test.ts+1-1
...@@ -40,7 +40,7 @@ describe("mobile remote connection records", () => {...@@ -40,7 +40,7 @@ describe("mobile remote connection records", () => {
4040
41 it("identifies mobile token exchanges for authorized-client presentation", () => {41 it("identifies mobile token exchanges for authorized-client presentation", () => {
42 expect(authClientMetadata()).toEqual({42 expect(authClientMetadata()).toEqual({
43 label: "T3 Code Mobile",43 label: "The Knife Mobile",
44 deviceType: "mobile",44 deviceType: "mobile",
45 os: "iOS",45 os: "iOS",
46 osMajorVersion: 18,46 osMajorVersion: 18,
apps/mobile/src/lib/mobileTheme.ts+1-1
...@@ -26,7 +26,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{...@@ -26,7 +26,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
26 readonly id: MobileThemeId;26 readonly id: MobileThemeId;
27 readonly label: string;27 readonly label: string;
28}> = [28}> = [
29 { id: DEFAULT_MOBILE_THEME_ID, label: "T3 Code" },29 { id: DEFAULT_MOBILE_THEME_ID, label: "The Knife" },
30 { id: "material-you", label: "Material You" },30 { id: "material-you", label: "Material You" },
31 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),31 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
32];32];
apps/mobile/src/widgets/AgentActivity.tsx+2-8
...@@ -244,15 +244,9 @@ export function AgentActivity(...@@ -244,15 +244,9 @@ export function AgentActivity(
244 </HStack>244 </HStack>
245 );245 );
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.
253 const renderLogo = (height: number, color: Foreground) => (247 const renderLogo = (height: number, color: Foreground) => (
254 <HStack modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(color)]}>248 <HStack modifiers={[frame({ width: height, height }), foregroundStyle(color)]}>
255 <Image assetName="T3Mark" modifiers={[resizable()]} />249 <Image assetName="KnifeMark" modifiers={[resizable()]} />
256 </HStack>250 </HStack>
257 );251 );
258252
apps/mobile/src/widgets/SubscriptionUsage.android.tsx+17-5
...@@ -29,8 +29,20 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi...@@ -29,8 +29,20 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
29 });29 });
30 const muted = colors.onSurfaceVariant;30 const muted = colors.onSurfaceVariant;
31 const providers = props.providers ?? [31 const providers = props.providers ?? [
32 { name: "Codex", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 },32 {
33 { name: "Claude", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 },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 },
34 ];46 ];
35 return (47 return (
36 // The card is one Button so a tap reaches the app's interaction listener,48 // 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...@@ -60,7 +72,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
60 </Text>72 </Text>
61 {shown.length === 0 ? (73 {shown.length === 0 ? (
62 <Text color={muted} maxLines={1} style={{ fontSize: 11 }}>74 <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}
64 </Text>76 </Text>
65 ) : null}77 ) : null}
66 {shown.map((window) => {78 {shown.map((window) => {
...@@ -90,7 +102,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi...@@ -90,7 +102,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
90 })}102 })}
91 {hidden > 0 ? (103 {hidden > 0 ? (
92 <Text color={muted} maxLines={1} style={{ fontSize: 10 }}>104 <Text color={muted} maxLines={1} style={{ fontSize: 10 }}>
93 {`${hidden} more in T3`}105 {`${hidden} more in The Knife`}
94 </Text>106 </Text>
95 ) : null}107 ) : null}
96 </Column>108 </Column>
...@@ -104,7 +116,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi...@@ -104,7 +116,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi
104 >116 >
105 {props.checkedAt117 {props.checkedAt
106 ? `As of ${new Date(props.checkedAt).toLocaleString(undefined, { hour: "numeric", minute: "2-digit", month: "short", day: "numeric" })}`118 ? `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"}
108 </Text>120 </Text>
109 </Column>121 </Column>
110 </Button>122 </Button>
apps/mobile/src/widgets/SubscriptionUsage.tsx+6-6
...@@ -39,8 +39,8 @@ function SubscriptionUsage(...@@ -39,8 +39,8 @@ function SubscriptionUsage(
39 const monochrome =39 const monochrome =
40 environment.widgetRenderingMode !== "fullColor" || environment.isLuminanceReduced;40 environment.widgetRenderingMode !== "fullColor" || environment.isLuminanceReduced;
41 const providers = props.providers ?? [41 const providers = props.providers ?? [
42 { name: "Codex", 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 T3 to connect", windows: [], expiresAt: 0 },43 { name: "Claude", detail: "Open The Knife to connect", windows: [], expiresAt: 0 },
44 ];44 ];
45 const columns = providers.map((provider) => {45 const columns = providers.map((provider) => {
46 const stale = provider.windows.length > 0 && now >= provider.expiresAt;46 const stale = provider.windows.length > 0 && now >= provider.expiresAt;
...@@ -73,7 +73,7 @@ function SubscriptionUsage(...@@ -73,7 +73,7 @@ function SubscriptionUsage(
73 ].slice(0, limit)73 ].slice(0, limit)
74 : windows.slice(0, limit);74 : windows.slice(0, limit);
75 const detail = stale75 const detail = stale
76 ? "Open T3 to refresh"76 ? "Open The Knife to refresh"
77 : period !== "auto" && windows.length === 0 && provider.windows.length > 077 : period !== "auto" && windows.length === 0 && provider.windows.length > 0
78 ? `No ${period} limit reported`78 ? `No ${period} limit reported`
79 : provider.detail;79 : provider.detail;
...@@ -121,7 +121,7 @@ function SubscriptionUsage(...@@ -121,7 +121,7 @@ function SubscriptionUsage(
121 ? `${tightest.remaining}% left`121 ? `${tightest.remaining}% left`
122 : period !== "auto" && !stale && provider.windows.length > 0122 : period !== "auto" && !stale && provider.windows.length > 0
123 ? "N/A"123 ? "N/A"
124 : "Open T3"}124 : "Open The Knife"}
125 </Text>125 </Text>
126 </HStack>126 </HStack>
127 {tightest ? (127 {tightest ? (
...@@ -223,7 +223,7 @@ function SubscriptionUsage(...@@ -223,7 +223,7 @@ function SubscriptionUsage(
223 >223 >
224 {(period === "auto" ? (provider.totalWindows ?? windows.length) : windows.length) -224 {(period === "auto" ? (provider.totalWindows ?? windows.length) : windows.length) -
225 limit}{" "}225 limit}{" "}
226 more in T3226 more in The Knife
227 </Text>227 </Text>
228 ) : null}228 ) : null}
229 </VStack>229 </VStack>
...@@ -255,7 +255,7 @@ function SubscriptionUsage(...@@ -255,7 +255,7 @@ function SubscriptionUsage(
255 >255 >
256 {props.checkedAt256 {props.checkedAt
257 ? `As of ${new Date(props.checkedAt).toLocaleString(undefined, { hour: "numeric", minute: "2-digit", month: "short", day: "numeric" })}`257 ? `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"}
259 </Text>259 </Text>
260 ) : null}260 ) : null}
261 </VStack>261 </VStack>
apps/mobile/src/widgets/publishSubscriptionUsage.android.ts+1-2
...@@ -22,8 +22,7 @@ export async function publishSubscriptionUsage(snapshot: SubscriptionUsageSnapsh...@@ -22,8 +22,7 @@ export async function publishSubscriptionUsage(snapshot: SubscriptionUsageSnapsh
22 });22 });
23 }23 }
24 widget.updateSnapshot(snapshot);24 widget.updateSnapshot(snapshot);
25 // Android has no timeline; an alarm re-renders the stored snapshot at each25 // Android has no timeline; an alarm re-renders the stored snapshot at each deadline.
26 // deadline so stale readings flip to "Open T3 to refresh" unattended.
27 requireOptionalNativeModule<{ schedule: (name: string, deadlines: number[]) => void }>(26 requireOptionalNativeModule<{ schedule: (name: string, deadlines: number[]) => void }>(
28 "T3WidgetExpiry",27 "T3WidgetExpiry",
29 )?.schedule(28 )?.schedule(
apps/mobile/src/widgets/subscriptionUsageSnapshot.ts+2-2
...@@ -94,7 +94,7 @@ function subscriptionUsageProps(...@@ -94,7 +94,7 @@ function subscriptionUsageProps(
94 return {94 return {
95 name,95 name,
96 detail: !fresh96 detail: !fresh
97 ? "Open T3 to refresh"97 ? "Open The Knife to refresh"
98 : pool.accounts.length > 198 : pool.accounts.length > 1
99 ? `${pool.accounts.length} accounts · pooled`99 ? `${pool.accounts.length} accounts · pooled`
100 : "Subscription remaining",100 : "Subscription remaining",
...@@ -161,7 +161,7 @@ export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, n...@@ -161,7 +161,7 @@ export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, n
161 ...snapshot,161 ...snapshot,
162 providers: snapshot.providers.map((provider) =>162 providers: snapshot.providers.map((provider) =>
163 provider.windows.length > 0 && provider.expiresAt <= date163 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 }
165 : provider,165 : provider,
166 ),166 ),
167 },167 },
apps/web/index.html+3-3
...@@ -477,13 +477,13 @@...@@ -477,13 +477,13 @@
477 cursor: pointer;477 cursor: pointer;
478 }478 }
479 </style>479 </style>
480 <title>T3 Code (Alpha)</title>480 <title>The Knife</title>
481 </head>481 </head>
482 <body>482 <body>
483 <div id="root">483 <div id="root">
484 <div id="boot-shell">484 <div id="boot-shell">
485 <div id="boot-shell-card" aria-label="T3 Code splash screen">485 <div id="boot-shell-card" aria-label="The Knife splash screen">
486 <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" />486 <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="The Knife" />
487 </div>487 </div>
488 </div>488 </div>
489 </div>489 </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 @@...@@ -1,4 +1,6 @@
1{1{
2 "name": "The Knife",
3 "short_name": "The Knife",
2 "id": "/",4 "id": "/",
3 "start_url": "/",5 "start_url": "/",
4 "scope": "/",6 "scope": "/",
apps/web/src/bootstrap.test.ts+2-2
...@@ -64,7 +64,7 @@ describe("app startup failures", () => {...@@ -64,7 +64,7 @@ describe("app startup failures", () => {
64 await import("./bootstrap");64 await import("./bootstrap");
65 await vi.dynamicImportSettled();65 await vi.dynamicImportSettled();
6666
67 expect(bootShell?.text).toContain("T3 Code could not load.");67 expect(bootShell?.text).toContain("The Knife could not load.");
68 const reloadButton = bootShell?.children[0]?.children.find(68 const reloadButton = bootShell?.children[0]?.children.find(
69 (element) => element.tagName === "button",69 (element) => element.tagName === "button",
70 );70 );
...@@ -78,7 +78,7 @@ describe("app startup failures", () => {...@@ -78,7 +78,7 @@ describe("app startup failures", () => {
7878
79 showBootError(new Error("internal module path"));79 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.");
82 expect(bootShell?.text.includes("internal module path")).toBe(dev);82 expect(bootShell?.text.includes("internal module path")).toBe(dev);
83 });83 });
8484
apps/web/src/branding.test.ts+15-15
...@@ -24,9 +24,9 @@ describe("branding", () => {...@@ -24,9 +24,9 @@ describe("branding", () => {
24 value: {24 value: {
25 desktopBridge: {25 desktopBridge: {
26 getAppBranding: () => ({26 getAppBranding: () => ({
27 baseName: "T3 Code",27 baseName: "The Knife",
28 stageLabel: "Nightly",28 stageLabel: "Nightly",
29 displayName: "T3 Code (Nightly)",29 displayName: "The Knife (Nightly)",
30 }),30 }),
31 },31 },
32 },32 },
...@@ -34,9 +34,9 @@ describe("branding", () => {...@@ -34,9 +34,9 @@ describe("branding", () => {
3434
35 const branding = await import("./branding");35 const branding = await import("./branding");
3636
37 expect(branding.APP_BASE_NAME).toBe("T3 Code");37 expect(branding.APP_BASE_NAME).toBe("The Knife");
38 expect(branding.APP_STAGE_LABEL).toBe("Nightly");38 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)");
40 });40 });
4141
42 it("normalizes hosted app channel metadata", async () => {42 it("normalizes hosted app channel metadata", async () => {
...@@ -47,7 +47,7 @@ describe("branding", () => {...@@ -47,7 +47,7 @@ describe("branding", () => {
47 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");47 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");
48 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");48 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");
49 expect(branding.APP_STAGE_LABEL).toBe("Nightly");49 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)");
51 });51 });
5252
53 it("does not label the latest hosted app channel", async () => {53 it("does not label the latest hosted app channel", async () => {
...@@ -58,7 +58,7 @@ describe("branding", () => {...@@ -58,7 +58,7 @@ describe("branding", () => {
58 expect(branding.HOSTED_APP_CHANNEL).toBe("latest");58 expect(branding.HOSTED_APP_CHANNEL).toBe("latest");
59 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest");59 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest");
60 expect(branding.APP_STAGE_LABEL).toBe("Latest");60 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");
62 });62 });
6363
64 it("ignores unknown hosted app channels", async () => {64 it("ignores unknown hosted app channels", async () => {
...@@ -84,33 +84,33 @@ describe("branding logic", () => {...@@ -84,33 +84,33 @@ describe("branding logic", () => {
84 it("updates the display name for nightly primary server versions", () => {84 it("updates the display name for nightly primary server versions", () => {
85 expect(85 expect(
86 resolveServerBackedAppDisplayName({86 resolveServerBackedAppDisplayName({
87 baseName: "T3 Code",87 baseName: "The Knife",
88 fallbackDisplayName: "T3 Code (Alpha)",88 fallbackDisplayName: "The Knife (Alpha)",
89 fallbackStageLabel: "Alpha",89 fallbackStageLabel: "Alpha",
90 primaryServerVersion: "0.0.28-nightly.20260616.12",90 primaryServerVersion: "0.0.28-nightly.20260616.12",
91 }),91 }),
92 ).toBe("T3 Code (Nightly)");92 ).toBe("The Knife (Nightly)");
93 });93 });
9494
95 it("keeps the fallback display name for stable primary server versions", () => {95 it("keeps the fallback display name for stable primary server versions", () => {
96 expect(96 expect(
97 resolveServerBackedAppDisplayName({97 resolveServerBackedAppDisplayName({
98 baseName: "T3 Code",98 baseName: "The Knife",
99 fallbackDisplayName: "T3 Code (Alpha)",99 fallbackDisplayName: "The Knife (Alpha)",
100 fallbackStageLabel: "Alpha",100 fallbackStageLabel: "Alpha",
101 primaryServerVersion: "0.0.27",101 primaryServerVersion: "0.0.27",
102 }),102 }),
103 ).toBe("T3 Code (Alpha)");103 ).toBe("The Knife (Alpha)");
104 });104 });
105105
106 it("keeps the fallback display name for malformed nightly primary server versions", () => {106 it("keeps the fallback display name for malformed nightly primary server versions", () => {
107 expect(107 expect(
108 resolveServerBackedAppDisplayName({108 resolveServerBackedAppDisplayName({
109 baseName: "T3 Code",109 baseName: "The Knife",
110 fallbackDisplayName: "T3 Code (Alpha)",110 fallbackDisplayName: "The Knife (Alpha)",
111 fallbackStageLabel: "Alpha",111 fallbackStageLabel: "Alpha",
112 primaryServerVersion: "0.0.28-nightly.20260616",112 primaryServerVersion: "0.0.28-nightly.20260616",
113 }),113 }),
114 ).toBe("T3 Code (Alpha)");114 ).toBe("The Knife (Alpha)");
115 });115 });
116});116});
apps/web/src/branding.ts+2-2
...@@ -16,11 +16,11 @@ export const HOSTED_APP_CHANNEL =...@@ -16,11 +16,11 @@ export const HOSTED_APP_CHANNEL =
16 hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null;16 hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null;
17export const HOSTED_APP_CHANNEL_LABEL =17export const HOSTED_APP_CHANNEL_LABEL =
18 HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null;18 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";
20export const APP_STAGE_LABEL =20export const APP_STAGE_LABEL =
21 injectedDesktopAppBranding?.stageLabel ??21 injectedDesktopAppBranding?.stageLabel ??
22 HOSTED_APP_CHANNEL_LABEL ??22 HOSTED_APP_CHANNEL_LABEL ??
23 (import.meta.env.DEV ? "Dev" : "Alpha");23 (import.meta.env.DEV ? "Dev" : "Latest");
24export const APP_DISPLAY_NAME =24export const APP_DISPLAY_NAME =
25 injectedDesktopAppBranding?.displayName ??25 injectedDesktopAppBranding?.displayName ??
26 formatAppDisplayName({ baseName: APP_BASE_NAME, stageLabel: APP_STAGE_LABEL });26 formatAppDisplayName({ baseName: APP_BASE_NAME, stageLabel: APP_STAGE_LABEL });
apps/web/src/cloud/linkEnvironment.ts+2-2
...@@ -59,7 +59,7 @@ function ensureRelayClientAvailable(...@@ -59,7 +59,7 @@ function ensureRelayClientAvailable(
59 if (status.status === "available") return;59 if (status.status === "available") return;
60 if (status.status === "unsupported") {60 if (status.status === "unsupported") {
61 return yield* new CloudEnvironmentLinkError({61 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}.`,
63 });63 });
64 }64 }
6565
...@@ -95,7 +95,7 @@ function ensureRelayClientAvailable(...@@ -95,7 +95,7 @@ function ensureRelayClientAvailable(
95 return yield* new CloudEnvironmentLinkError({95 return yield* new CloudEnvironmentLinkError({
96 message:96 message:
97 installedStatus.status === "unsupported"97 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}.`
99 : "The relay client is still unavailable after installation.",99 : "The relay client is still unavailable after installation.",
100 });100 });
101 }101 }
apps/web/src/components/ChatView.tsx+1-1
...@@ -11178,7 +11178,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -11178,7 +11178,7 @@ export default function ChatView(props: ChatViewProps) {
11178 ) : renderedRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? (11178 ) : renderedRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? (
11179 <PullRequestsUnavailableState11179 <PullRequestsUnavailableState
11180 title="Pull requests unavailable"11180 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."
11182 />11182 />
11183 ) : renderedRightPanelSurface?.kind === "pull-request" ? (11183 ) : renderedRightPanelSurface?.kind === "pull-request" ? (
11184 // No onClose: the surface tab's own X owns closing here, and a second X in the header11184 // 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(...@@ -171,7 +171,7 @@ export function shouldOpenDefaultBrowserProfileFromMenuClick(
171}171}
172172
173const SURFACE_DISABLED_REASONS = {173const 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.",
175 terminal: "Terminal surfaces are only available from a project thread.",175 terminal: "Terminal surfaces are only available from a project thread.",
176 files: "Files are only available when a project is open.",176 files: "Files are only available when a project is open.",
177 diff: "Diff is only available for server threads in Git repositories.",177 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({...@@ -157,7 +157,7 @@ export function ServerUpdatesAction({
157 if (desktopTargets.length > 0) {157 if (desktopTargets.length > 0) {
158 const confirmed =158 const confirmed =
159 (await requestConfirmDialog(159 (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.`,
161 )) ?? true;161 )) ?? true;
162 if (!confirmed) return;162 if (!confirmed) return;
163 }163 }
...@@ -279,7 +279,7 @@ export function ServerUpdateAction({...@@ -279,7 +279,7 @@ export function ServerUpdateAction({
279 // remote machine installs without asking anyone there.279 // remote machine installs without asking anyone there.
280 const confirmed =280 const confirmed =
281 (await requestConfirmDialog(281 (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.`,
283 )) ?? true;283 )) ?? true;
284 if (!confirmed) {284 if (!confirmed) {
285 return;285 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";...@@ -119,7 +119,7 @@ import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";
119import ReactMarkdown from "react-markdown";119import ReactMarkdown from "react-markdown";
120import remarkGfm from "remark-gfm";120import remarkGfm from "remark-gfm";
121import type { Root, RootContent } from "mdast";121import type { Root, RootContent } from "mdast";
122import { T3Wordmark } from "../T3Wordmark";122import { KnifeMark } from "../KnifeMark";
123import { ThreadContextChip } from "../ThreadContextChip";123import { ThreadContextChip } from "../ThreadContextChip";
124import {124import {
125 BotIcon,125 BotIcon,
...@@ -2462,7 +2462,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"...@@ -2462,7 +2462,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
2462 className="me-1 text-2xs text-muted-foreground/70"2462 className="me-1 text-2xs text-muted-foreground/70"
2463 data-user-message-attribution="t3code"2463 data-user-message-attribution="t3code"
2464 >2464 >
2465 Sent by T3 Code2465 Sent by The Knife
2466 </p>2466 </p>
2467 ) : userMessage.attribution === "agent" ? (2467 ) : userMessage.attribution === "agent" ? (
2468 <p className="me-1 text-2xs text-muted-foreground/70" data-user-message-attribution="agent">2468 <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...@@ -2837,7 +2837,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
2837 return (2837 return (
2838 <>2838 <>
2839 <div className="relative min-w-0 px-1 py-0.5">2839 <div className="relative min-w-0 px-1 py-0.5">
2840 <MessageAuthorHeading>T3 Code</MessageAuthorHeading>2840 <MessageAuthorHeading>The Knife</MessageAuthorHeading>
2841 <div data-thread-find-text="true">2841 <div data-thread-find-text="true">
2842 <AssistantCitationSource2842 <AssistantCitationSource
2843 messageId={row.message.id}2843 messageId={row.message.id}
...@@ -5236,7 +5236,7 @@ function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className...@@ -5236,7 +5236,7 @@ function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className
5236 case "device":5236 case "device":
5237 return <SmartphoneIcon className={className} aria-hidden />;5237 return <SmartphoneIcon className={className} aria-hidden />;
5238 case "t3-code":5238 case "t3-code":
5239 return <T3Wordmark className={className} aria-hidden />;5239 return <KnifeMark className={className} aria-hidden />;
5240 case "check":5240 case "check":
5241 return <CheckIcon className={className} aria-hidden />;5241 return <CheckIcon className={className} aria-hidden />;
5242 case "circle-alert":5242 case "circle-alert":
apps/web/src/components/chat/V2LifecycleRow.tsx+2-2
...@@ -48,7 +48,7 @@ import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"...@@ -48,7 +48,7 @@ import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"
48import { cn } from "~/lib/utils";48import { cn } from "~/lib/utils";
49import { TimelineSystemDivider } from "./TimelineSystemDivider";49import { TimelineSystemDivider } from "./TimelineSystemDivider";
50import { Button, InlineButton } from "../ui/button";50import { Button, InlineButton } from "../ui/button";
51import { T3Wordmark } from "../T3Wordmark";51import { KnifeMark } from "../KnifeMark";
5252
53const LIFECYCLE_TYPES = new Set<OrchestrationV2TurnItem["type"]>([53const LIFECYCLE_TYPES = new Set<OrchestrationV2TurnItem["type"]>([
54 "run_interrupt_request",54 "run_interrupt_request",
...@@ -170,7 +170,7 @@ export function V2LifecycleRow(props: {...@@ -170,7 +170,7 @@ export function V2LifecycleRow(props: {
170 return (170 return (
171 <WorkLogRow171 <WorkLogRow
172 data-v2-item-type={item.type}172 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 />}
174 label={<>Created thread{item.title ? ` · ${item.title}` : ""}</>}174 label={<>Created thread{item.title ? ` · ${item.title}` : ""}</>}
175 trailing={175 trailing={
176 <InlineButton176 <InlineButton
apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.test.ts+3-3
...@@ -31,7 +31,7 @@ describe("mobile client presentation", () => {...@@ -31,7 +31,7 @@ describe("mobile client presentation", () => {
31 it("describes the client platform and enabled notification events", () => {31 it("describes the client platform and enabled notification events", () => {
32 const client = device();32 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");
35 expect(mobileClientNotificationDetail(client)).toBe(35 expect(mobileClientNotificationDetail(client)).toBe(
36 "Alerts enabled for approvals, completions.",36 "Alerts enabled for approvals, completions.",
37 );37 );
...@@ -42,7 +42,7 @@ describe("mobile client presentation", () => {...@@ -42,7 +42,7 @@ describe("mobile client presentation", () => {
42 mobileClientPlatformLabel(42 mobileClientPlatformLabel(
43 device({ platform: "android", iosMajorVersion: null, androidApiLevel: 36 }),43 device({ platform: "android", iosMajorVersion: null, androidApiLevel: 36 }),
44 ),44 ),
45 ).toBe("Android · T3 Code 1.2.3");45 ).toBe("Android · The Knife 1.2.3");
46 });46 });
4747
48 it("distinguishes disabled notifications from an empty event selection", () => {48 it("distinguishes disabled notifications from an empty event selection", () => {
...@@ -68,7 +68,7 @@ describe("mobile client presentation", () => {...@@ -68,7 +68,7 @@ describe("mobile client presentation", () => {
6868
69 it("handles missing app versions and invalid update timestamps", () => {69 it("handles missing app versions and invalid update timestamps", () => {
70 expect(mobileClientPlatformLabel(device({ iosMajorVersion: null }))).toBe(70 expect(mobileClientPlatformLabel(device({ iosMajorVersion: null }))).toBe(
71 "iOS · T3 Code 1.2.3",71 "iOS · The Knife 1.2.3",
72 );72 );
73 expect(mobileClientPlatformLabel(device({ appVersion: null }))).toBe("iOS 18");73 expect(mobileClientPlatformLabel(device({ appVersion: null }))).toBe("iOS 18");
74 expect(mobileClientUpdatedAtLabel("not-a-date")).toBe("Update time unavailable");74 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...@@ -21,7 +21,7 @@ export function mobileClientPlatformLabel(device: RelayClientDeviceRecord): stri
21 : device.iosMajorVersion === null21 : device.iosMajorVersion === null
22 ? "iOS"22 ? "iOS"
23 : `iOS ${device.iosMajorVersion}`;23 : `iOS ${device.iosMajorVersion}`;
24 return `${platform}${device.appVersion ? ` · T3 Code ${device.appVersion}` : ""}`;24 return `${platform}${device.appVersion ? ` · The Knife ${device.appVersion}` : ""}`;
25}25}
2626
27export function mobileClientNotificationDetail(device: RelayClientDeviceRecord): string {27export function mobileClientNotificationDetail(device: RelayClientDeviceRecord): string {
apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx+1-1
...@@ -94,7 +94,7 @@ function EmptyMobileClients() {...@@ -94,7 +94,7 @@ function EmptyMobileClients() {
94 <EmptyHeader>94 <EmptyHeader>
95 <EmptyTitle>No mobile clients</EmptyTitle>95 <EmptyTitle>No mobile clients</EmptyTitle>
96 <EmptyDescription>96 <EmptyDescription>
97 Install T3 Code on your phone and sign in to T3 Connect to get push notifications and Live97 Install The Knife on your phone and sign in to T3 Connect to get push notifications and Live
98 Activities.98 Activities.
99 </EmptyDescription>99 </EmptyDescription>
100 </EmptyHeader>100 </EmptyHeader>
apps/web/src/components/cloud/RelayClientInstallDialog.tsx+4-4
...@@ -69,8 +69,8 @@ export function RelayClientInstallDialog() {...@@ -69,8 +69,8 @@ export function RelayClientInstallDialog() {
69 </DialogTitle>69 </DialogTitle>
70 <DialogDescription>70 <DialogDescription>
71 {isInstalling71 {isInstalling
72 ? "T3 Code is preparing this environment for secure access through T3 Connect."72 ? "The Knife 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."}73 : "The Knife needs the relay client to make this environment available through T3 Connect."}
74 </DialogDescription>74 </DialogDescription>
75 </DialogHeader>75 </DialogHeader>
76 <DialogPanel scrollFade={false}>76 <DialogPanel scrollFade={false}>
...@@ -91,14 +91,14 @@ export function RelayClientInstallDialog() {...@@ -91,14 +91,14 @@ export function RelayClientInstallDialog() {
91 value={activeStepIndex + 1}91 value={activeStepIndex + 1}
92 />92 />
93 <p className="text-xs leading-relaxed text-muted-foreground">93 <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.
95 </p>95 </p>
96 </div>96 </div>
97 ) : (97 ) : (
98 <div className="rounded-xl border border-border/70 bg-muted/35 p-3">98 <div className="rounded-xl border border-border/70 bg-muted/35 p-3">
99 <p className="text-sm font-medium text-foreground">Managed relay client</p>99 <p className="text-sm font-medium text-foreground">Managed relay client</p>
100 <p className="mt-1 text-sm leading-relaxed text-muted-foreground">100 <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{" "}
102 {view.status === "confirming" ? view.version : ""} locally.102 {view.status === "confirming" ? view.version : ""} locally.
103 </p>103 </p>
104 </div>104 </div>
apps/web/src/components/desktop/DesktopAppActivationCoordinator.tsx+1-1
...@@ -63,7 +63,7 @@ export function DesktopAppActivationCoordinator() {...@@ -63,7 +63,7 @@ export function DesktopAppActivationCoordinator() {
63 });63 });
64 if (result._tag === "Failure") {64 if (result._tag === "Failure") {
65 const error = squashAtomCommandFailure(result);65 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.");
67 }67 }
68 return projectId;68 return projectId;
69 },69 },
apps/web/src/components/desktop/SshPasswordPromptDialog.tsx+1-1
...@@ -161,7 +161,7 @@ function ActiveSshPasswordPrompt({...@@ -161,7 +161,7 @@ function ActiveSshPasswordPrompt({
161 <DialogTitle>SSH Password Required</DialogTitle>161 <DialogTitle>SSH Password Required</DialogTitle>
162 <DialogDescription>162 <DialogDescription>
163 T3 needs your SSH password to connect to <code>{target}</code>. The password is passed163 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.
165 </DialogDescription>165 </DialogDescription>
166 </DialogHeader>166 </DialogHeader>
167 <DialogPanel scrollFade={false}>167 <DialogPanel scrollFade={false}>
apps/web/src/components/desktopUpdate.logic.test.ts+3-3
...@@ -279,7 +279,7 @@ describe("desktop update UI helpers", () => {...@@ -279,7 +279,7 @@ describe("desktop update UI helpers", () => {
279 availableVersion: "1.1.0",279 availableVersion: "1.1.0",
280 downloadedVersion: "1.1.1",280 downloadedVersion: "1.1.1",
281 }),281 }),
282 ).toContain("Install update 1.1.1 and restart T3 Code?");282 ).toContain("Install update 1.1.1 and restart The Knife?");
283 });283 });
284284
285 it("falls back to generic install confirmation copy when no version is available", () => {285 it("falls back to generic install confirmation copy when no version is available", () => {
...@@ -288,7 +288,7 @@ describe("desktop update UI helpers", () => {...@@ -288,7 +288,7 @@ describe("desktop update UI helpers", () => {
288 availableVersion: null,288 availableVersion: null,
289 downloadedVersion: null,289 downloadedVersion: null,
290 }),290 }),
291 ).toContain("Install update and restart T3 Code?");291 ).toContain("Install update and restart The Knife?");
292 });292 });
293293
294 it("keeps the same install confirmation copy across desktop platforms", () => {294 it("keeps the same install confirmation copy across desktop platforms", () => {
...@@ -298,7 +298,7 @@ describe("desktop update UI helpers", () => {...@@ -298,7 +298,7 @@ describe("desktop update UI helpers", () => {
298 downloadedVersion: "1.1.0",298 downloadedVersion: "1.1.0",
299 }),299 }),
300 ).toBe(300 ).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.",
302 );302 );
303 });303 });
304});304});
apps/web/src/components/desktopUpdate.logic.ts+4-4
...@@ -62,12 +62,12 @@ export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState):...@@ -62,12 +62,12 @@ export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState):
6262
63 const action = resolveDesktopUpdateButtonAction(state);63 const action = resolveDesktopUpdateButtonAction(state);
64 if (action === "download") {64 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.";
66 }66 }
67 if (action === "install") {67 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.";
69 }69 }
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.";
71}71}
7272
73export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {73export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {
...@@ -108,7 +108,7 @@ export function getDesktopUpdateInstallConfirmationMessage(...@@ -108,7 +108,7 @@ export function getDesktopUpdateInstallConfirmationMessage(
108 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,108 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
109): string {109): string {
110 const version = state.downloadedVersion ?? state.availableVersion;110 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.`;
112}112}
113113
114export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {114export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {
apps/web/src/components/onboarding/FirstRunGate.tsx+1-1
...@@ -214,7 +214,7 @@ function FirstRunRecovery({...@@ -214,7 +214,7 @@ function FirstRunRecovery({
214 <p className="mt-2 text-sm text-muted-foreground">214 <p className="mt-2 text-sm text-muted-foreground">
215 {settingsReadFailed215 {settingsReadFailed
216 ? "Your saved settings could not be loaded."216 ? "Your saved settings could not be loaded."
217 : "T3 Code could not confirm this workspace."}217 : "The Knife could not confirm this workspace."}
218 </p>218 </p>
219 <Button219 <Button
220 className="mt-5"220 className="mt-5"
apps/web/src/components/onboarding/WelcomeWizard.tsx+9-9
...@@ -84,7 +84,7 @@ import { TerminalViewport } from "../ThreadTerminalDrawer";...@@ -84,7 +84,7 @@ import { TerminalViewport } from "../ThreadTerminalDrawer";
84import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";84import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
85import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation";85import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation";
86import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";86import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
87import { T3Wordmark } from "../T3Wordmark";87import { KnifeMark } from "../KnifeMark";
88import { Alert, AlertDescription } from "../ui/alert";88import { Alert, AlertDescription } from "../ui/alert";
89import { Button } from "../ui/button";89import { Button } from "../ui/button";
90import { CommandBlock } from "../CommandBlock";90import { CommandBlock } from "../CommandBlock";
...@@ -233,12 +233,12 @@ export function WelcomeWizard({...@@ -233,12 +233,12 @@ export function WelcomeWizard({
233 initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true}233 initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true}
234 >234 >
235 <WizardHeader235 <WizardHeader
236 title="Set up T3 Code"236 title="Set up The Knife"
237 identity={237 identity={
238 <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code">238 <div className="flex items-center gap-2" role="img" aria-label="The Knife">
239 <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden />239 <KnifeMark className="size-8 shrink-0" aria-hidden />
240 <span className="text-2xl font-medium tracking-tight text-muted-foreground">240 <span className="text-2xl font-medium tracking-tight text-muted-foreground">
241 Code241 The Knife
242 </span>242 </span>
243 </div>243 </div>
244 }244 }
...@@ -432,8 +432,8 @@ function ConnectionStep({...@@ -432,8 +432,8 @@ function ConnectionStep({
432 ) : null}432 ) : null}
433 <div className="mt-6 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">433 <div className="mt-6 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
434 <p className="min-w-0 text-xs leading-relaxed text-muted-foreground">434 <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 your435 Anonymous usage data helps improve T3 Code. To read more about how your data is used and
436 data is used and how to opt out, see our{" "}436 how to opt out, see our{" "}
437 <a437 <a
438 className="underline underline-offset-2 hover:text-foreground"438 className="underline underline-offset-2 hover:text-foreground"
439 href={PRIVACY_POLICY_URL}439 href={PRIVACY_POLICY_URL}
...@@ -530,7 +530,7 @@ function ConnectAccountOption({...@@ -530,7 +530,7 @@ function ConnectAccountOption({
530 </p>530 </p>
531 <CommandBlock command="npx t3 connect" className="mt-3" />531 <CommandBlock command="npx t3 connect" className="mt-3" />
532 <p className="mt-3 text-xs text-muted-foreground">532 <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.
534 </p>534 </p>
535 </div>535 </div>
536 </CollapsiblePanel>536 </CollapsiblePanel>
...@@ -644,7 +644,7 @@ function PairingForm({...@@ -644,7 +644,7 @@ function PairingForm({
644 </p>644 </p>
645 <CommandBlock command="npx t3 pair" className="mt-2" />645 <CommandBlock command="npx t3 pair" className="mt-2" />
646 <p className="mt-2 text-xs leading-relaxed text-muted-foreground">646 <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{" "}
648 <code className="font-mono">--tailscale</code> to use your tailnet.648 <code className="font-mono">--tailscale</code> to use your tailnet.
649 </p>649 </p>
650 </CollapsiblePanel>650 </CollapsiblePanel>
apps/web/src/components/preview/PreviewPanel.tsx+1-1
...@@ -41,7 +41,7 @@ export function PreviewPanel({...@@ -41,7 +41,7 @@ export function PreviewPanel({
41 <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center">41 <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
42 <p className="max-w-sm text-sm text-muted-foreground">42 <p className="max-w-sm text-sm text-muted-foreground">
43 {canOperatePreview43 {canOperatePreview
44 ? "Preview is only available in the T3 Code desktop app."44 ? "Preview is only available in The Knife desktop app."
45 : "Pair this client again with preview access to control browser previews."}45 : "Pair this client again with preview access to control browser previews."}
46 </p>46 </p>
47 </div>47 </div>
apps/web/src/components/preview/PreviewView.tsx+1-1
...@@ -1097,7 +1097,7 @@ export function PreviewView({...@@ -1097,7 +1097,7 @@ export function PreviewView({
1097 ) : (1097 ) : (
1098 <div className="flex h-full items-center justify-center p-8 text-center">1098 <div className="flex h-full items-center justify-center p-8 text-center">
1099 <p className="max-w-sm text-sm text-muted-foreground">1099 <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.
1101 </p>1101 </p>
1102 </div>1102 </div>
1103 )1103 )
apps/web/src/components/promptLinks/HostedPromptLinkLanding.tsx+1-1
...@@ -146,7 +146,7 @@ function HostedPromptLinkLanding({...@@ -146,7 +146,7 @@ function HostedPromptLinkLanding({
146 <Alert className="mt-5" variant="error">146 <Alert className="mt-5" variant="error">
147 <AlertTitle>This prompt had hidden characters, it may be malicious</AlertTitle>147 <AlertTitle>This prompt had hidden characters, it may be malicious</AlertTitle>
148 <AlertDescription>148 <AlertDescription>
149 T3 Code removed {prompt.hiddenCharacters} hidden character149 The Knife removed {prompt.hiddenCharacters} hidden character
150 {prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it.150 {prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it.
151 </AlertDescription>151 </AlertDescription>
152 </Alert>152 </Alert>
apps/web/src/components/promptLinks/NewThreadLinkView.tsx+2-2
...@@ -99,7 +99,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {...@@ -99,7 +99,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
99 replace: true,99 replace: true,
100 }).catch(() => null);100 }).catch(() => null);
101 if (opened === null) {101 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." });
103 return;103 return;
104 }104 }
105 setState({ status: "opened" });105 setState({ status: "opened" });
...@@ -169,7 +169,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {...@@ -169,7 +169,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
169 setState({169 setState({
170 status: "failed",170 status: "failed",
171 message:171 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.",
173 });173 });
174 return;174 return;
175 }175 }
apps/web/src/components/promptLinks/linkedPrompt.ts+1-1
...@@ -10,7 +10,7 @@ export function warnAboutHiddenCharacters(prompt: LinkedPrompt): void {...@@ -10,7 +10,7 @@ export function warnAboutHiddenCharacters(prompt: LinkedPrompt): void {
10 toastManager.add({10 toastManager.add({
11 type: "warning",11 type: "warning",
12 title: "This prompt had hidden characters, it may be malicious",12 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.`,
14 timeout: 0,14 timeout: 0,
15 });15 });
16}16}
apps/web/src/components/settings/BrowserImportWizard.tsx+4-4
...@@ -285,9 +285,9 @@ function FullDiskAccessStep({...@@ -285,9 +285,9 @@ function FullDiskAccessStep({
285 return (285 return (
286 <>286 <>
287 <DialogHeader>287 <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>
289 <DialogDescription>289 <DialogDescription>
290 To import cookies from {source.name}, T3 Code needs Full Disk Access. Turn it on in System290 To import cookies from {source.name}, The Knife needs Full Disk Access. Turn it on in System
291 Settings, then come back to finish the import — you can revoke it again once the import is291 Settings, then come back to finish the import — you can revoke it again once the import is
292 done.292 done.
293 </DialogDescription>293 </DialogDescription>
...@@ -319,8 +319,8 @@ function FullDiskAccessStep({...@@ -319,8 +319,8 @@ function FullDiskAccessStep({
319 {!permission.isReady(["fullDiskAccess"]) ? (319 {!permission.isReady(["fullDiskAccess"]) ? (
320 <p className="mt-3 text-xs text-muted-foreground">320 <p className="mt-3 text-xs text-muted-foreground">
321 {stillRequired321 {stillRequired
322 ? "Access is still required. Quit and reopen T3 Code if you just allowed it, 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 T3 Code, then retry the import."}323 : "If access doesn't update after you allow it, quit and reopen The Knife, then retry the import."}
324 </p>324 </p>
325 ) : null}325 ) : null}
326 </DialogPanel>326 </DialogPanel>
apps/web/src/components/settings/CaptureShortcutConfig.tsx+5-5
...@@ -217,7 +217,7 @@ export function CaptureShortcutConfig({...@@ -217,7 +217,7 @@ export function CaptureShortcutConfig({
217 ) : (217 ) : (
218 <>218 <>
219 <p className="text-muted-foreground">219 <p className="text-muted-foreground">
220 Allow T3 Code to read your desktop settings. You'll review any changes here before220 Allow The Knife to read your desktop settings. You'll review any changes here before
221 saving.221 saving.
222 </p>222 </p>
223 <Button223 <Button
...@@ -229,7 +229,7 @@ export function CaptureShortcutConfig({...@@ -229,7 +229,7 @@ export function CaptureShortcutConfig({
229 </Button>229 </Button>
230 {!supported ? (230 {!supported ? (
231 <p className="text-xs text-muted-foreground">231 <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.
233 </p>233 </p>
234 ) : null}234 ) : null}
235 </>235 </>
...@@ -243,7 +243,7 @@ export function CaptureShortcutConfig({...@@ -243,7 +243,7 @@ export function CaptureShortcutConfig({
243 <p role="status" className="text-muted-foreground">243 <p role="status" className="text-muted-foreground">
244 {state.shortcutPending244 {state.shortcutPending
245 ? "Connecting to your desktop…"245 ? "Connecting to your desktop…"
246 : "Restart T3 Code to finish connecting your shortcut."}246 : "Restart The Knife to finish connecting your shortcut."}
247 </p>247 </p>
248 ) : null}248 ) : null}
249 <details className="text-xs text-muted-foreground">249 <details className="text-xs text-muted-foreground">
...@@ -262,7 +262,7 @@ export function CaptureShortcutConfig({...@@ -262,7 +262,7 @@ export function CaptureShortcutConfig({
262 state.shortcutConfigPath ??262 state.shortcutConfigPath ??
263 (niri ? "~/.config/niri/config.kdl" : "~/.config/hypr/hyprland.conf")}263 (niri ? "~/.config/niri/config.kdl" : "~/.config/hypr/hyprland.conf")}
264 </p>264 </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}
266 {preview && preview.resolvedPath !== preview.path ? (266 {preview && preview.resolvedPath !== preview.path ? (
267 <p className="break-all">Linked to {preview.resolvedPath}. The link will be kept.</p>267 <p className="break-all">Linked to {preview.resolvedPath}. The link will be kept.</p>
268 ) : null}268 ) : null}
...@@ -323,7 +323,7 @@ export function CaptureShortcutConfig({...@@ -323,7 +323,7 @@ export function CaptureShortcutConfig({
323 {isCopied ? "Copied" : "Copy shortcut"}323 {isCopied ? "Copied" : "Copy shortcut"}
324 </Button>324 </Button>
325 <p>325 <p>
326 Turn capture off in T3 Code to stop it. Remove the shortcut from {desktop} to free up326 Turn capture off in The Knife to stop it. Remove the shortcut from {desktop} to free up
327 the keys.327 the keys.
328 </p>328 </p>
329 {state.shortcutActionRegistered === false ? (329 {state.shortcutActionRegistered === false ? (
apps/web/src/components/settings/ChatGptWelcomeCoordinator.tsx+1-1
...@@ -66,7 +66,7 @@ export function ChatGptWelcomeCoordinator() {...@@ -66,7 +66,7 @@ export function ChatGptWelcomeCoordinator() {
66 <OpenAI className="mb-2 size-8" aria-hidden="true" />66 <OpenAI className="mb-2 size-8" aria-hidden="true" />
67 <DialogTitle>Your ChatGPT plan is connected</DialogTitle>67 <DialogTitle>Your ChatGPT plan is connected</DialogTitle>
68 <DialogDescription>68 <DialogDescription>
69 Eligible usage in T3 Code uses your ChatGPT plan. Manage your shared usage and any69 Eligible usage in The Knife uses your ChatGPT plan. Manage your shared usage and any
70 credit settings in ChatGPT.70 credit settings in ChatGPT.
71 </DialogDescription>71 </DialogDescription>
72 <p className="text-xs text-muted-foreground">72 <p className="text-xs text-muted-foreground">
apps/web/src/components/settings/CliCommandSettingsRow.tsx+2-2
...@@ -52,9 +52,9 @@ export function CliCommandSettingsRow() {...@@ -52,9 +52,9 @@ export function CliCommandSettingsRow() {
52 if (!bridge || !state?.supported) return null;52 if (!bridge || !state?.supported) return null;
53 const installed = state.installedPath !== null;53 const installed = state.installedPath !== null;
54 const description = state.shadowedBy54 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.`
56 : !installed56 : !installed
57 ? "Run T3 Code's CLI as `t3` from any terminal."57 ? "Run The Knife's CLI as `t3` from any terminal."
58 : state.onPath58 : state.onPath
59 ? `Installed at ${state.installedPath}. Open a new terminal to use it.`59 ? `Installed at ${state.installedPath}. Open a new terminal to use it.`
60 : `Installed at ${state.installedPath}, which is not on your PATH yet. Add its folder to your PATH to run \`t3\`.`;60 : `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({...@@ -577,8 +577,8 @@ function ManagedCodexSetup({
577 : installation?.phase === "verifying"577 : installation?.phase === "verifying"
578 ? "Checking Codex."578 ? "Checking Codex."
579 : installed579 : installed
580 ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by T3 Code"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.`580 ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by The Knife"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.`
581 : (installation?.message ?? "T3 Code downloads and manages Codex for you.");581 : (installation?.message ?? "The Knife downloads and manages Codex for you.");
582 const accountDescription = finishingSignIn ? (582 const accountDescription = finishingSignIn ? (
583 "Finishing sign-in..."583 "Finishing sign-in..."
584 ) : installActive ? (584 ) : installActive ? (
...@@ -655,7 +655,7 @@ function ManagedCodexSetup({...@@ -655,7 +655,7 @@ function ManagedCodexSetup({
655 const callbackCompletion =655 const callbackCompletion =
656 !handoff && auth?.phase === "waiting" && url ? (656 !handoff && auth?.phase === "waiting" && url ? (
657 <div className="flex w-full flex-col gap-3 text-xs leading-relaxed text-muted-foreground">657 <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>
659 <form659 <form
660 className="flex flex-col gap-2 sm:flex-row sm:items-center"660 className="flex flex-col gap-2 sm:flex-row sm:items-center"
661 onSubmit={(event) => {661 onSubmit={(event) => {
...@@ -1032,7 +1032,7 @@ export function CodexManagedRuntimeFields({...@@ -1032,7 +1032,7 @@ export function CodexManagedRuntimeFields({
1032 <>1032 <>
1033 <SettingsRow1033 <SettingsRow
1034 title="Binary path"1034 title="Binary path"
1035 description="Selected by T3 Code."1035 description="Selected by The Knife."
1036 control={1036 control={
1037 <div className="w-full sm:w-80">1037 <div className="w-full sm:w-80">
1038 <Input1038 <Input
apps/web/src/components/settings/ConnectionsSettings.tsx+10-10
...@@ -3480,7 +3480,7 @@ export function ConnectionsSettings() {...@@ -3480,7 +3480,7 @@ export function ConnectionsSettings() {
3480 {desktopWslState.enabled ? (3480 {desktopWslState.enabled ? (
3481 <SettingsRow3481 <SettingsRow
3482 title="WSL only"3482 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."
3484 className="bg-muted/20 pl-7 sm:pl-8"3484 className="bg-muted/20 pl-7 sm:pl-8"
3485 control={3485 control={
3486 <Switch3486 <Switch
...@@ -3805,8 +3805,8 @@ export function ConnectionsSettings() {...@@ -3805,8 +3805,8 @@ export function ConnectionsSettings() {
3805 </AlertDialogTitle>3805 </AlertDialogTitle>
3806 <AlertDialogDescription>3806 <AlertDialogDescription>
3807 {pendingDesktopServerExposureMode === "network-accessible"3807 {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."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. T3 Code 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."}
3810 </AlertDialogDescription>3810 </AlertDialogDescription>
3811 </AlertDialogHeader>3811 </AlertDialogHeader>
3812 <AlertDialogFooter>3812 <AlertDialogFooter>
...@@ -3862,15 +3862,15 @@ export function ConnectionsSettings() {...@@ -3862,15 +3862,15 @@ export function ConnectionsSettings() {
3862 <AlertDialogDescription>3862 <AlertDialogDescription>
3863 {pendingWslChange?.kind === "disable"3863 {pendingWslChange?.kind === "disable"
3864 ? pendingWslChange.wasWslOnly3864 ? 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."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 T3 Code until 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."
3867 : pendingWslChange?.kind === "distro"3867 : 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."
3869 : pendingWslChange?.kind === "enable"3869 : pendingWslChange?.kind === "enable"
3870 ? "Run the WSL backend alongside the Windows one, or stop the Windows backend and use only WSL? You can change this later from Settings."3870 ? "Run the WSL backend alongside the Windows one, or stop the Windows backend and use only WSL? You can change this later from Settings."
3871 : pendingWslChange?.nextValue3871 : 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."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 : "T3 Code will restart and bring the Windows backend back up alongside WSL."}3873 : "The Knife will restart and bring the Windows backend back up alongside WSL."}
3874 </AlertDialogDescription>3874 </AlertDialogDescription>
3875 </AlertDialogHeader>3875 </AlertDialogHeader>
3876 <AlertDialogFooter>3876 <AlertDialogFooter>
...@@ -3956,7 +3956,7 @@ export function ConnectionsSettings() {...@@ -3956,7 +3956,7 @@ export function ConnectionsSettings() {
3956 <AlertDialogHeader>3956 <AlertDialogHeader>
3957 <AlertDialogTitle>Disable Tailscale HTTPS?</AlertDialogTitle>3957 <AlertDialogTitle>Disable Tailscale HTTPS?</AlertDialogTitle>
3958 <AlertDialogDescription>3958 <AlertDialogDescription>
3959 T3 Code will restart the local backend without Tailscale Serve.3959 The Knife will restart the local backend without Tailscale Serve.
3960 </AlertDialogDescription>3960 </AlertDialogDescription>
3961 </AlertDialogHeader>3961 </AlertDialogHeader>
3962 <AlertDialogFooter>3962 <AlertDialogFooter>
...@@ -3994,7 +3994,7 @@ export function ConnectionsSettings() {...@@ -3994,7 +3994,7 @@ export function ConnectionsSettings() {
3994 <DialogHeader>3994 <DialogHeader>
3995 <DialogTitle>Set up Tailscale HTTPS?</DialogTitle>3995 <DialogTitle>Set up Tailscale HTTPS?</DialogTitle>
3996 <DialogDescription>3996 <DialogDescription>
3997 T3 Code will restart the local backend with Tailscale Serve enabled and ask3997 The Knife will restart the local backend with Tailscale Serve enabled and ask
3998 Tailscale to proxy HTTPS traffic to this backend.3998 Tailscale to proxy HTTPS traffic to this backend.
3999 </DialogDescription>3999 </DialogDescription>
4000 </DialogHeader>4000 </DialogHeader>
apps/web/src/components/settings/IntegrationsSettings.tsx+1-1
...@@ -577,7 +577,7 @@ function BrowserRecordingFrameRateSetting({ disabled }: { readonly disabled: boo...@@ -577,7 +577,7 @@ function BrowserRecordingFrameRateSetting({ disabled }: { readonly disabled: boo
577577
578const LINK_TARGET_LABELS: Readonly<Record<BrowserLinkTarget, string>> = {578const LINK_TARGET_LABELS: Readonly<Record<BrowserLinkTarget, string>> = {
579 system: "Your default browser",579 system: "Your default browser",
580 app: "T3 Code",580 app: "The Knife",
581};581};
582582
583function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean }) {583function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean }) {
apps/web/src/components/settings/KeybindingsSettings.tsx+1-1
...@@ -257,7 +257,7 @@ function UnknownWhenVariableWarning({...@@ -257,7 +257,7 @@ function UnknownWhenVariableWarning({
257257
258 return (258 return (
259 <WarningTooltipIcon label={label} focusable={focusable} className="size-4.5">259 <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 match260 The Knife does not recognize this condition yet. It can still be saved, but it may not match
261 unless the runtime provides it.261 unless the runtime provides it.
262 </WarningTooltipIcon>262 </WarningTooltipIcon>
263 );263 );
apps/web/src/components/settings/LocalEnvironmentSetting.tsx+3-3
...@@ -43,7 +43,7 @@ export function LocalEnvironmentSetting() {...@@ -43,7 +43,7 @@ export function LocalEnvironmentSetting() {
43 {...searchableSetting("local-environment")}43 {...searchableSetting("local-environment")}
44 description={44 description={
45 enabled45 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."
47 : "Turned off. Agents only run in remote environments."47 : "Turned off. Agents only run in remote environments."
48 }48 }
49 control={49 control={
...@@ -70,8 +70,8 @@ export function LocalEnvironmentSetting() {...@@ -70,8 +70,8 @@ export function LocalEnvironmentSetting() {
70 </AlertDialogTitle>70 </AlertDialogTitle>
71 <AlertDialogDescription>71 <AlertDialogDescription>
72 {enabled72 {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."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 : "T3 Code will restart and start running a server on this computer again."}74 : "The Knife will restart and start running a server on this computer again."}
75 </AlertDialogDescription>75 </AlertDialogDescription>
76 </AlertDialogHeader>76 </AlertDialogHeader>
77 {error ? <p className="px-6 pb-4 text-sm text-destructive">{error}</p> : null}77 {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() {...@@ -22,7 +22,7 @@ export function NotificationSettings() {
22 {...searchableSetting("thread-notifications")}22 {...searchableSetting("thread-notifications")}
23 description={23 description={
24 permissionMessage ??24 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."
26 }26 }
27 control={27 control={
28 <Select28 <Select
apps/web/src/components/settings/SettingsPanels.tsx+1-1
...@@ -3422,7 +3422,7 @@ export function GeneralSettingsPanel() {...@@ -3422,7 +3422,7 @@ export function GeneralSettingsPanel() {
3422 />3422 />
3423 <SettingsRow3423 <SettingsRow
3424 {...searchableSetting("open-source-licenses")}3424 {...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."
3426 control={3426 control={
3427 <Button3427 <Button
3428 render={<Link to="/settings/open-source-licenses" />}3428 render={<Link to="/settings/open-source-licenses" />}
apps/web/src/components/settings/SnapShotSettings.tsx+2-2
...@@ -263,7 +263,7 @@ export function SnapShotSettings() {...@@ -263,7 +263,7 @@ export function SnapShotSettings() {
263 const shortcutStatus = recording263 const shortcutStatus = recording
264 ? "Press your shortcut. Esc cancels."264 ? "Press your shortcut. Esc cancels."
265 : candidateConflict265 : candidateConflict
266 ? `T3 Code already uses this for "${commandLabel(candidateConflict)}".`266 ? `The Knife already uses this for "${commandLabel(candidateConflict)}".`
267 : shortcutCheck.status === "checking"267 : shortcutCheck.status === "checking"
268 ? "Checking shortcut…"268 ? "Checking shortcut…"
269 : shortcutCheck.availability269 : shortcutCheck.availability
...@@ -315,7 +315,7 @@ export function SnapShotSettings() {...@@ -315,7 +315,7 @@ export function SnapShotSettings() {
315 try {315 try {
316 if (state?.macPermissions) {316 if (state?.macPermissions) {
317 saveSnapShotSetupResume(wizard?.wasEnabled ?? settings.snapShotEnabled);317 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.");
319 await bridge.setupSnapShot("test-mac-capture");319 await bridge.setupSnapShot("test-mac-capture");
320 }320 }
321 if (state?.mode === "direct")321 if (state?.mode === "direct")
apps/web/src/components/settings/SnapShotSetupDialog.tsx+2-2
...@@ -174,7 +174,7 @@ export function SnapShotSetupDialog({...@@ -174,7 +174,7 @@ export function SnapShotSetupDialog({
174 ? "Update the capture helper"174 ? "Update the capture helper"
175 : "Allow snapshots",175 : "Allow snapshots",
176 description:176 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.",
178 }178 }
179 : backend === "niri"179 : backend === "niri"
180 ? {180 ? {
...@@ -355,7 +355,7 @@ export function SnapShotSetupDialog({...@@ -355,7 +355,7 @@ export function SnapShotSetupDialog({
355 </p>355 </p>
356 ))}356 ))}
357 {step === "access" && (backend === "gnome" || helperBackend) ? (357 {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>
359 ) : null}359 ) : null}
360 {step === "access" && backend === "gnome" && extension?.status === "enabled" ? (360 {step === "access" && backend === "gnome" && extension?.status === "enabled" ? (
361 <Button361 <Button
apps/web/src/components/settings/providerStatus.ts+1-1
...@@ -44,7 +44,7 @@ export function getProviderSummary(provider: ServerProvider | undefined) {...@@ -44,7 +44,7 @@ export function getProviderSummary(provider: ServerProvider | undefined) {
44 return {44 return {
45 headline: "Disabled",45 headline: "Disabled",
46 detail:46 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.",
48 };48 };
49 }49 }
50 if (!provider.installed) {50 if (!provider.installed) {
apps/web/src/components/settings/scopedSettings.ts+1-1
...@@ -475,7 +475,7 @@ export async function persistScopedSettingsPatch(...@@ -475,7 +475,7 @@ export async function persistScopedSettingsPatch(
475 return [475 return [
476 {476 {
477 ...environment,477 ...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.",
479 },479 },
480 ];480 ];
481 }481 }
apps/web/src/components/settings/settingsLayout.tsx+1
...@@ -573,6 +573,7 @@ export function SettingsPageContainer({...@@ -573,6 +573,7 @@ export function SettingsPageContainer({
573 <div573 <div
574 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto"574 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto"
575 data-settings-page-scroll575 data-settings-page-scroll
576 data-scroll-restoration-id="settings-page"
576 >577 >
577 <WorkspacePageContainer width={width} className={cn("gap-8", className)}>578 <WorkspacePageContainer width={width} className={cn("gap-8", className)}>
578 <SettingsScopeSentence />579 <SettingsScopeSentence />
apps/web/src/components/sidebar/mainAppLocation.ts+5-4
...@@ -14,8 +14,6 @@ export function isSidebarUtilityPage(pathname: string) {...@@ -14,8 +14,6 @@ export function isSidebarUtilityPage(pathname: string) {
14 );14 );
15}15}
1616
17let mainAppHref: string | null = null;
18
19// Mount once in the app shell. Records the latest main app URL so Back can17// Mount once in the app shell. Records the latest main app URL so Back can
20// return there no matter how many utility pages were visited since.18// return there no matter how many utility pages were visited since.
21export function MainAppLocationTracker() {19export function MainAppLocationTracker() {
...@@ -23,7 +21,7 @@ export function MainAppLocationTracker() {...@@ -23,7 +21,7 @@ export function MainAppLocationTracker() {
23 select: (location) => (isSidebarUtilityPage(location.pathname) ? null : location.href),21 select: (location) => (isSidebarUtilityPage(location.pathname) ? null : location.href),
24 });22 });
25 useEffect(() => {23 useEffect(() => {
26 if (href !== null) mainAppHref = href;24 if (href !== null) sessionStorage.setItem("t3code-main-app-href", href);
27 }, [href]);25 }, [href]);
28 return null;26 return null;
29}27}
...@@ -32,5 +30,8 @@ export function MainAppLocationTracker() {...@@ -32,5 +30,8 @@ export function MainAppLocationTracker() {
32// the app was opened directly on a utility page.30// the app was opened directly on a utility page.
33export function useNavigateToMainApp() {31export function useNavigateToMainApp() {
34 const navigate = useNavigate();32 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 );
36}37}
apps/web/src/components/sidebar/updateEverythingRun.tsx+1-1
...@@ -133,7 +133,7 @@ export function DesktopUpdateLaunchNotice() {...@@ -133,7 +133,7 @@ export function DesktopUpdateLaunchNotice() {
133 stackedThreadToast({133 stackedThreadToast({
134 type: "error",134 type: "error",
135 title: "Update did not apply",135 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.`,
137 }),137 }),
138 );138 );
139 }, [notAppliedVersion]);139 }, [notAppliedVersion]);
apps/web/src/connection/clientMetadata.test.ts+1-1
...@@ -80,7 +80,7 @@ describe("client telemetry metadata", () => {...@@ -80,7 +80,7 @@ describe("client telemetry metadata", () => {
80 desktopBridge: { getClientPlatform: () => "darwin" },80 desktopBridge: { getClientPlatform: () => "darwin" },
81 }),81 }),
82 ).toEqual({82 ).toEqual({
83 label: "T3 Code Desktop",83 label: "The Knife Desktop",
84 deviceType: "desktop",84 deviceType: "desktop",
85 os: "macOS",85 os: "macOS",
86 surface: "desktop",86 surface: "desktop",
apps/web/src/connection/clientMetadata.ts+2-2
...@@ -77,7 +77,7 @@ export function clientPresentationMetadata(input: {...@@ -77,7 +77,7 @@ export function clientPresentationMetadata(input: {
77}): AuthClientPresentationMetadata {77}): AuthClientPresentationMetadata {
78 if (input.desktopBridge !== undefined) {78 if (input.desktopBridge !== undefined) {
79 return {79 return {
80 label: "T3 Code Desktop",80 label: "The Knife Desktop",
81 deviceType: "desktop",81 deviceType: "desktop",
82 os: clientOsFromElectronPlatform(input.desktopBridge.getClientPlatform?.()),82 os: clientOsFromElectronPlatform(input.desktopBridge.getClientPlatform?.()),
83 surface: "desktop",83 surface: "desktop",
...@@ -86,7 +86,7 @@ export function clientPresentationMetadata(input: {...@@ -86,7 +86,7 @@ export function clientPresentationMetadata(input: {
86 }86 }
8787
88 return {88 return {
89 label: "T3 Code Web",89 label: "The Knife Web",
90 deviceType: browserDeviceType(input.identity),90 deviceType: browserDeviceType(input.identity),
91 os: browserClientOs(input.identity),91 os: browserClientOs(input.identity),
92 surface: "web",92 surface: "web",
apps/web/src/desktopAppActivation.ts+3-3
...@@ -85,7 +85,7 @@ export async function handleDesktopAppActivationRequest(...@@ -85,7 +85,7 @@ export async function handleDesktopAppActivationRequest(
85 return failure(85 return failure(
86 request.requestId,86 request.requestId,
87 "project-create-failed",87 "project-create-failed",
88 errorMessage(error, "T3 Code could not add the project."),88 errorMessage(error, "The Knife could not add the project."),
89 );89 );
90 }90 }
91 }91 }
...@@ -99,7 +99,7 @@ export async function handleDesktopAppActivationRequest(...@@ -99,7 +99,7 @@ export async function handleDesktopAppActivationRequest(
99 return failure(99 return failure(
100 request.requestId,100 request.requestId,
101 "thread-open-failed",101 "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.",
103 );103 );
104 }104 }
105 return {105 return {
...@@ -113,7 +113,7 @@ export async function handleDesktopAppActivationRequest(...@@ -113,7 +113,7 @@ export async function handleDesktopAppActivationRequest(
113 return failure(113 return failure(
114 request.requestId,114 request.requestId,
115 "thread-open-failed",115 "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."),
117 );117 );
118 }118 }
119}119}
apps/web/src/lib/bootError.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1/** Shows startup failures before React can replace the boot splash. */1/** Shows startup failures before React can replace the boot splash. */
2export function showBootError(error: unknown) {2export function showBootError(error: unknown) {
3 console.error("T3 Code failed to start.", error);3 console.error("The Knife failed to start.", error);
4 const bootShell = document.getElementById("boot-shell");4 const bootShell = document.getElementById("boot-shell");
5 if (!bootShell) return;5 if (!bootShell) return;
66
...@@ -9,7 +9,7 @@ export function showBootError(error: unknown) {...@@ -9,7 +9,7 @@ export function showBootError(error: unknown) {
9 content.setAttribute("role", "alert");9 content.setAttribute("role", "alert");
1010
11 const message = document.createElement("p");11 const message = document.createElement("p");
12 message.textContent = "T3 Code could not load.";12 message.textContent = "The Knife could not load.";
13 content.append(message);13 content.append(message);
1414
15 if (import.meta.env.DEV && error instanceof Error) {15 if (import.meta.env.DEV && error instanceof Error) {
apps/web/src/router.ts+2
...@@ -1,12 +1,14 @@...@@ -1,12 +1,14 @@
1import { createRouter, RouterHistory } from "@tanstack/react-router";1import { createRouter, RouterHistory } from "@tanstack/react-router";
22
3import { routeTree } from "./routeTree.gen";3import { routeTree } from "./routeTree.gen";
4import { isSidebarUtilityPage } from "./components/sidebar/mainAppLocation";
45
5export function getRouter(history: RouterHistory) {6export function getRouter(history: RouterHistory) {
6 return createRouter({7 return createRouter({
7 routeTree,8 routeTree,
8 history,9 history,
9 context: {},10 context: {},
11 scrollRestoration: ({ location }) => isSidebarUtilityPage(location.pathname),
10 // Route components are split chunks (autoCodeSplitting in vite.config);12 // Route components are split chunks (autoCodeSplitting in vite.config);
11 // fetching them on hover/focus intent hides the load from the first13 // fetching them on hover/focus intent hides the load from the first
12 // settings or pull-request navigation.14 // settings or pull-request navigation.
apps/web/src/routes/_chat.index.tsx+2-2
...@@ -158,9 +158,9 @@ function HostedStaticOnboardingState() {...@@ -158,9 +158,9 @@ function HostedStaticOnboardingState() {
158 <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">158 <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">
159 <LinkIcon className="size-5" />159 <LinkIcon className="size-5" />
160 </div>160 </div>
161 <EmptyTitle>Connect to a computer running T3 Code</EmptyTitle>161 <EmptyTitle>Connect to a computer running The Knife</EmptyTitle>
162 <EmptyDescription>162 <EmptyDescription>
163 This app connects to T3 Code running on your computer or a server. Start the T3 Code163 This app connects to The Knife running on your computer or a server. Start The Knife
164 desktop app or command-line server on that machine and keep it running.164 desktop app or command-line server on that machine and keep it running.
165 </EmptyDescription>165 </EmptyDescription>
166 <EmptyDescription>{description}</EmptyDescription>166 <EmptyDescription>{description}</EmptyDescription>
apps/web/src/routes/_chat.pull-requests.tsx+1-1
...@@ -1914,7 +1914,7 @@ function PullRequestsRouteView() {...@@ -1914,7 +1914,7 @@ function PullRequestsRouteView() {
1914 ) : !pullRequestsSupported ? (1914 ) : !pullRequestsSupported ? (
1915 <PullRequestsUnavailableState1915 <PullRequestsUnavailableState
1916 title="Pull requests unavailable"1916 title="Pull requests unavailable"
1917 error="Update your T3 Code servers to browse pull requests."1917 error="Update your servers to browse pull requests."
1918 />1918 />
1919 ) : firstLoad ? (1919 ) : firstLoad ? (
1920 <PullRequestListGhost rows={7} />1920 <PullRequestListGhost rows={7} />
apps/web/src/routes/_chat.tsx+1-1
...@@ -153,7 +153,7 @@ function ChatRouteGlobalShortcuts() {...@@ -153,7 +153,7 @@ function ChatRouteGlobalShortcuts() {
153 stackedThreadToast({153 stackedThreadToast({
154 type: "info",154 type: "info",
155 title: "Preview is desktop-only",155 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.",
157 }),157 }),
158 );158 );
159 return;159 return;
apps/web/src/versionSkew.test.ts+1-1
...@@ -24,7 +24,7 @@ import {...@@ -24,7 +24,7 @@ import {
24} from "./versionSkew";24} from "./versionSkew";
2525
26const MISMATCH_HINT =26const 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
29describe("versionSkew", () => {29describe("versionSkew", () => {
30 it("updates only the proven npm prefix and safely quotes its path", () => {30 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(...@@ -87,7 +87,7 @@ export function resolveVersionMismatch(
87 return {87 return {
88 clientVersion: normalizedClientVersion,88 clientVersion: normalizedClientVersion,
89 serverVersion: normalizedServerVersion,89 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.",
91 };91 };
92}92}
9393
assets/README.md+20-60
...@@ -1,67 +1,27 @@...@@ -1,67 +1,27 @@
1# Brand icons1# 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`8`vp run icons:export` regenerates the macOS, iOS, Linux, Windows, and web renditions,
6- `nightly/app-icon.icon`9including browser public assets. `vp run icons:check` verifies those exports.
7- `prod/app-icon.icon`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 exports22## 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.24`vp run icons:export:android` reads the same knife layer and backplate from the
1825Icon Composer project. It creates `knife/android-foreground.png` and
19After changing an Icon Composer project, open it in Icon Composer and export the macOS PNG with exactly these settings:26`knife/android-splash.png`, keeping the blade inside Android's circular safe
2027area. The adaptive and monochrome icons share the foreground alpha silhouette.
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.
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...@@ -206,3 +206,11 @@ passed as `--wsl-runtime`; see the
206Add `--signed` after configuring the platform credentials in the206Add `--signed` after configuring the platform credentials in the
207[release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the207[release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the
208[Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR.208[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 = {...@@ -16,7 +16,7 @@ static const SecretSchema chromium_schema = {
1616
17int main(int argc, char **argv) {17int main(int argc, char **argv) {
18 if (argc != 2 || argv[1][0] == '\0') return 64;18 if (argc != 2 || argv[1][0] == '\0') return 64;
19 g_set_application_name("T3 Code");19 g_set_application_name("The Knife");
2020
21 GHashTable *attributes = secret_attributes_build(&chromium_schema, "application", argv[1], NULL);21 GHashTable *attributes = secret_attributes_build(&chromium_schema, "application", argv[1], NULL);
22 GError *error = NULL;22 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...@@ -139,7 +139,7 @@ pub fn destination(windows: Vec<Window>, pid: u32, title: &str) -> Result<Option
139 .filter(|w| w.pid == pid && w.title == title && w.mapped && !w.hidden);139 .filter(|w| w.pid == pid && w.title == title && w.mapped && !w.hidden);
140 let found = matches.next();140 let found = matches.next();
141 if matches.next().is_some() {141 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());
143 }143 }
144 Ok(found)144 Ok(found)
145}145}
...@@ -157,14 +157,14 @@ pub fn activate(pid: u32, title: &str) -> Result<()> {...@@ -157,14 +157,14 @@ pub fn activate(pid: u32, title: &str) -> Result<()> {
157 if request(&lua)?.trim() != "ok"157 if request(&lua)?.trim() != "ok"
158 && request(&format!("/dispatch focuswindow address:0x{address:x}"))?.trim() != "ok"158 && request(&format!("/dispatch focuswindow address:0x{address:x}"))?.trim() != "ok"
159 {159 {
160 return Err("Hyprland could not focus T3 Code.".into());160 return Err("Hyprland could not focus The Knife.".into());
161 }161 }
162 return Ok(());162 return Ok(());
163 }163 }
164 events.get_ref().set_read_timeout(Some(164 events.get_ref().set_read_timeout(Some(
165 deadline165 deadline
166 .checked_duration_since(Instant::now())166 .checked_duration_since(Instant::now())
167 .ok_or("T3 Code did not become visible.")?,167 .ok_or("The Knife did not become visible.")?,
168 ))?;168 ))?;
169 let mut event = Vec::new();169 let mut event = Vec::new();
170 if events.by_ref().take(8193).read_until(b'\n', &mut event)? == 0 || event.len() > 8192 {170 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<()> {...@@ -348,7 +348,7 @@ fn run() -> Result<()> {
348 ),348 ),
349 )?;349 )?;
350 if serde_json::from_str::<serde_json::Value>(&value)?["activated"] != true {350 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());
352 }352 }
353 println!("{{\"activated\":true}}");353 println!("{{\"activated\":true}}");
354 }354 }
packages/client-runtime/src/connection/compatibility.ts+1-1
...@@ -21,7 +21,7 @@ export function orchestrationProtocolCompatibilityError(...@@ -21,7 +21,7 @@ export function orchestrationProtocolCompatibilityError(
21 })21 })
22 : new ConnectionBlockedError({22 : new ConnectionBlockedError({
23 reason: "unsupported",23 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.`,
25 ...(canSelfUpdate(descriptor) ? { serverUpdateRequired: true } : {}),25 ...(canSelfUpdate(descriptor) ? { serverUpdateRequired: true } : {}),
26 });26 });
27}27}
packages/client-runtime/src/connection/outdatedHostUpdate.ts+2-2
...@@ -77,7 +77,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd...@@ -77,7 +77,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd
77 ) {77 ) {
78 return yield* new OutdatedHostUpdateError({78 return yield* new OutdatedHostUpdateError({
79 environmentId,79 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.`,
81 });81 });
82 }82 }
8383
...@@ -189,7 +189,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd...@@ -189,7 +189,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd
189 if (Option.isNone(resumed)) {189 if (Option.isNone(resumed)) {
190 return yield* new OutdatedHostUpdateError({190 return yield* new OutdatedHostUpdateError({
191 environmentId,191 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.`,
193 });193 });
194 }194 }
195195
packages/client-runtime/src/connection/resolver.test.ts+1-1
...@@ -200,7 +200,7 @@ describe("ConnectionResolver", () => {...@@ -200,7 +200,7 @@ describe("ConnectionResolver", () => {
200 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));200 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));
201201
202 expect(error).toMatchObject({ reason: "unsupported" });202 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");
204 }),204 }),
205 );205 );
206206
packages/client-runtime/src/relay/errorPresentation.ts+1-1
...@@ -95,7 +95,7 @@ export function relayRequestFailureDetail(error: {...@@ -95,7 +95,7 @@ export function relayRequestFailureDetail(error: {
95// coming back, which clears this reason. While it is still reported, the host95// coming back, which clears this reason. While it is still reported, the host
96// is either still off or running a build too old to do that.96// is either still off or running a build too old to do that.
97export const RELAY_TUNNEL_RELEASED_MESSAGE =97export 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
100/** User-facing text for an offline status, or null when the relay gave no known reason. */100/** User-facing text for an offline status, or null when the relay gave no known reason. */
101export function relayOfflineReasonMessage(101export function relayOfflineReasonMessage(
packages/client-runtime/src/state/pullRequestDiffHttp.test.ts+1-1
...@@ -112,7 +112,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {...@@ -112,7 +112,7 @@ describe("fetchEnvironmentPullRequestDiff", () => {
112 traceId: "trace-auth-test",112 traceId: "trace-auth-test",
113 });113 });
114 expect(error.message).toBe(114 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.",
116 );116 );
117 }),117 }),
118 );118 );
packages/client-runtime/src/state/pullRequestDiffHttp.ts+1-1
...@@ -28,7 +28,7 @@ export class PullRequestDiffCredentialRejectedError extends Schema.TaggedError<P...@@ -28,7 +28,7 @@ export class PullRequestDiffCredentialRejectedError extends Schema.TaggedError<P
28 },28 },
29) {29) {
30 override get message(): string {30 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.";
32 }32 }
33}33}
3434
scripts/build-desktop-artifact.test.ts+7-7
...@@ -273,8 +273,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -273,8 +273,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
273 });273 });
274274
275 it("switches desktop packaging product names to nightly for nightly builds", () => {275 it("switches desktop packaging product names to nightly for nightly builds", () => {
276 assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Alpha)");276 assert.equal(resolveDesktopProductName("0.0.17"), "The Knife");
277 assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)");277 assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "The Knife (Nightly)");
278 });278 });
279279
280 it("switches desktop packaging icons to the nightly artwork for nightly versions", () => {280 it("switches desktop packaging icons to the nightly artwork for nightly versions", () => {
...@@ -702,7 +702,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -702,7 +702,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
702 "**/*.map",702 "**/*.map",
703 ]);703 ]);
704 assert.deepStrictEqual(mac.dmg, {704 assert.deepStrictEqual(mac.dmg, {
705 title: "T3 Code (Alpha) 1.2.3 Installer",705 title: "The Knife 1.2.3 Installer",
706 background: "dmg/dmg-background-latest.png",706 background: "dmg/dmg-background-latest.png",
707 window: { width: 640, height: 432 },707 window: { width: 640, height: 432 },
708 contents: [708 contents: [
...@@ -717,11 +717,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -717,11 +717,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
717 // Linux must register the renderer schemes so the generated .desktop717 // Linux must register the renderer schemes so the generated .desktop
718 // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links.718 // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links.
719 assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [719 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"] },
721 ]);721 ]);
722 // macOS also offers itself as a web browser, so it can be chosen as the default.722 // macOS also offers itself as a web browser, so it can be chosen as the default.
723 assert.deepStrictEqual((mac.mac as Record<string, unknown>).protocols, [723 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"] },
725 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },725 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
726 ]);726 ]);
727 // macOS lists a default browser only when it also opens web pages as documents.727 // 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) => {...@@ -746,7 +746,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
746 assert.propertyVal(746 assert.propertyVal(
747 (mac.mac as Record<string, unknown>).extendInfo as Record<string, unknown>,747 (mac.mac as Record<string, unknown>).extendInfo as Record<string, unknown>,
748 "NSLocalNetworkUsageDescription",748 "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.",
750 );750 );
751 for (const config of [linux, win]) {751 for (const config of [linux, win]) {
752 assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);752 assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);
...@@ -2141,7 +2141,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -2141,7 +2141,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
2141 assert.equal(mac.provisioningProfile, "/tmp/t3code.provisionprofile");2141 assert.equal(mac.provisioningProfile, "/tmp/t3code.provisionprofile");
2142 assert.match(String(mac.sign), /[\\/]scripts[\\/]sign-macos\.ts$/);2142 assert.match(String(mac.sign), /[\\/]scripts[\\/]sign-macos\.ts$/);
2143 assert.deepStrictEqual(mac.protocols, [2143 assert.deepStrictEqual(mac.protocols, [
2144 { name: "T3 Code", schemes: ["t3code", "t3code-dev"] },2144 { name: "The Knife", schemes: ["t3code", "t3code-dev"] },
2145 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },2145 { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" },
2146 ]);2146 ]);
2147 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),2147 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
scripts/build-desktop-artifact.ts+11-11
...@@ -971,7 +971,7 @@ export const DESKTOP_FILE_EXCLUSIONS = [...@@ -971,7 +971,7 @@ export const DESKTOP_FILE_EXCLUSIONS = [
971 "!**/node_modules/@cursor/sdk-*/**/*",971 "!**/node_modules/@cursor/sdk-*/**/*",
972 "!apps/desktop/prod-resources/cursor-sdk",972 "!apps/desktop/prod-resources/cursor-sdk",
973 "!apps/desktop/prod-resources/cursor-sdk/**/*",973 "!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,
975 // so the SDK's optional platform packages (each a ~200MB bundled executable)975 // so the SDK's optional platform packages (each a ~200MB bundled executable)
976 // are dead weight. The trailing dash keeps the SDK's own JS package.976 // are dead weight. The trailing dash keeps the SDK's own JS package.
977 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",977 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
...@@ -2745,8 +2745,8 @@ export function resolvePackageManagerUserAgent(packageManager: string): string {...@@ -2745,8 +2745,8 @@ export function resolvePackageManagerUserAgent(packageManager: string): string {
27452745
2746export function resolveDesktopProductName(version: string): string {2746export function resolveDesktopProductName(version: string): string {
2747 return resolveDesktopUpdateChannel(version) === "nightly"2747 return resolveDesktopUpdateChannel(version) === "nightly"
2748 ? "T3 Code (Nightly)"2748 ? "The Knife (Nightly)"
2749 : (desktopPackageJson.productName ?? "T3 Code");2749 : (desktopPackageJson.productName ?? "The Knife");
2750}2750}
27512751
2752export const createBuildConfig = Effect.fn("createBuildConfig")(function* (2752export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
...@@ -2771,7 +2771,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2771,7 +2771,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2771 const buildConfig: Record<string, unknown> = {2771 const buildConfig: Record<string, unknown> = {
2772 appId: DESKTOP_APP_ID,2772 appId: DESKTOP_APP_ID,
2773 productName: resolveDesktopProductName(version),2773 productName: resolveDesktopProductName(version),
2774 artifactName: "T3-Code-${version}-${arch}.${ext}",2774 artifactName: "The-Knife-${version}-${arch}.${ext}",
2775 electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],2775 electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],
2776 files: [2776 files: [
2777 ...DESKTOP_FILE_EXCLUSIONS,2777 ...DESKTOP_FILE_EXCLUSIONS,
...@@ -2824,9 +2824,9 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2824,9 +2824,9 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2824 // macOS 26 offers Messages/Mail codes in every text field unless opted out.2824 // macOS 26 offers Messages/Mail codes in every text field unless opted out.
2825 NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac: true,2825 NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac: true,
2826 NSLocalNetworkUsageDescription:2826 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.",
2828 NSScreenCaptureUsageDescription:2828 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.",
2830 // macOS lists an app under Default web browser only when it opens web2830 // macOS lists an app under Default web browser only when it opens web
2831 // pages as documents as well as http and https links (see protocols).2831 // pages as documents as well as http and https links (see protocols).
2832 CFBundleDocumentTypes: [2832 CFBundleDocumentTypes: [
...@@ -2840,10 +2840,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2840,10 +2840,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2840 },2840 },
2841 protocols: [2841 protocols: [
2842 {2842 {
2843 name: "T3 Code",2843 name: "The Knife",
2844 schemes: ["t3code", "t3code-dev"],2844 schemes: ["t3code", "t3code-dev"],
2845 },2845 },
2846 // Lets people choose T3 Code as their default web browser, which opens2846 // Lets people choose The Knife as their default web browser, which opens
2847 // each link in a new thread's browser panel.2847 // each link in a new thread's browser panel.
2848 {2848 {
2849 name: "Web site URL",2849 name: "Web site URL",
...@@ -2907,7 +2907,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2907,7 +2907,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2907 // t3code:// OAuth callbacks to the app.2907 // t3code:// OAuth callbacks to the app.
2908 protocols: [2908 protocols: [
2909 {2909 {
2910 name: "T3 Code",2910 name: "The Knife",
2911 schemes: ["t3code", "t3code-dev"],2911 schemes: ["t3code", "t3code-dev"],
2912 },2912 },
2913 ],2913 ],
...@@ -3869,7 +3869,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -3869,7 +3869,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
3869 private: true,3869 private: true,
3870 packageManager: rootPackageJson.packageManager,3870 packageManager: rootPackageJson.packageManager,
3871 description:3871 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.",
3873 license: "MIT",3873 license: "MIT",
3874 // Required by the .deb control file.3874 // Required by the .deb control file.
3875 homepage: "https://t3.codes",3875 homepage: "https://t3.codes",
...@@ -4158,7 +4158,7 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", {...@@ -4158,7 +4158,7 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", {
4158 Flag.optional,4158 Flag.optional,
4159 ),4159 ),
4160}).pipe(4160}).pipe(
4161 Command.withDescription("Build a desktop artifact for T3 Code."),4161 Command.withDescription("Build a desktop artifact for The Knife."),
4162 Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)),4162 Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)),
4163);4163);
41644164
scripts/export-android-icons.ts+45-207
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1#!/usr/bin/env node1#!/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
16import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
17import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
18import * as Console from "effect/Console";5import * as Console from "effect/Console";
...@@ -22,211 +9,62 @@ import * as Path from "effect/Path";...@@ -22,211 +9,62 @@ import * as Path from "effect/Path";
22import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
23import sharp from "sharp";10import sharp from "sharp";
2411
25type IconVariant = "dev" | "nightly" | "prod";12import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
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";
4313
44export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()(14export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()(
45 "AndroidIconRenderError",15 "AndroidIconRenderError",
46 { layer: Schema.String, cause: Schema.Defect() },16 { cause: Schema.Defect() },
47) {}17) {}
4818
49const wordmarkTransform = (size: number) => {19const decodeProject = Schema.decodeEffect(
50 const scale = (size * WORDMARK_FRACTION) / TEXT.width;20 Schema.fromJsonString(Schema.Struct({ fill: Schema.Struct({ solid: Schema.String }) })),
51 const tx = (size - TEXT.width * scale) / 2 - TEXT.x * scale;21);
52 const ty = (size - TEXT.height * scale) / 2 - TEXT.y * scale;22const decodeColor = Schema.decodeUnknownEffect(
53 return `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) scale(${scale.toFixed(4)})`;23 Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]),
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 },
135);24);
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
209const exportAndroidIcons = Effect.gen(function* () {26const exportAndroidIcons = Effect.gen(function* () {
210 const fs = yield* FileSystem.FileSystem;27 const fs = yield* FileSystem.FileSystem;
211 const path = yield* Path.Path;28 const path = yield* Path.Path;
212 const repositoryRoot = path.resolve(import.meta.dirname, "..");29 const repositoryRoot = path.resolve(import.meta.dirname, "..");
213 const outputs = [30 const projectRoot = path.join(repositoryRoot, BRAND_ASSET_PATHS.productionIconComposerProject);
214 ["android-icon-foreground.png", yield* renderForeground(repositoryRoot, ADAPTIVE_CANVAS)],31 const project = yield* decodeProject(
215 [32 yield* fs.readFileString(path.join(projectRoot, "icon.json")),
216 "android-icon-background-dev.png",33 );
217 yield* renderDevelopmentBackground(repositoryRoot, ADAPTIVE_CANVAS),34 const [r, g, b, alpha] = yield* decodeColor(
218 ],35 project.fill.solid.slice("srgb:".length).split(",").map(Number),
219 [36 );
220 "android-icon-background-nightly.png",37 const background = { r: r * 255, g: g * 255, b: b * 255, alpha };
221 yield* renderNightlyBackground(repositoryRoot, ADAPTIVE_CANVAS),38 const source = yield* fs.readFile(path.join(projectRoot, "Assets", "knife.png"));
222 ],39
223 ["android-splash-icon-dev.png", yield* renderSplashIcon(repositoryRoot, "dev")],40 for (const [name, size] of [
224 ["android-splash-icon-nightly.png", yield* renderSplashIcon(repositoryRoot, "nightly")],41 ["android-foreground.png", 432],
225 ["android-splash-icon-prod.png", yield* renderSplashIcon(repositoryRoot, "prod")],42 ["android-splash.png", 1152],
226 ] as const;43 ] as const) {
227 for (const [name, contents] of outputs) {44 const png = yield* Effect.tryPromise({
228 yield* fs.writeFile(path.join(repositoryRoot, OUTPUT_DIRECTORY, name), contents);45 try: async () => {
229 yield* Console.log(`wrote ${OUTPUT_DIRECTORY}/${name}`);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}`);
230 }68 }
231});69});
23270
scripts/export-brand-icons.ts+35-63
...@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";...@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
12import * as Stream from "effect/Stream";12import * as Stream from "effect/Stream";
13import { Command, Flag } from "effect/cli";13import { Command, Flag } from "effect/cli";
14import { ChildProcess, ChildProcessSpawner } from "effect/process";14import { ChildProcess, ChildProcessSpawner } from "effect/process";
15import sharp from "sharp";
1516
16import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts";17import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts";
17import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts";18import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts";
...@@ -41,7 +42,7 @@ const decodeIconComposerVersion = Schema.decodeUnknownEffect(...@@ -41,7 +42,7 @@ const decodeIconComposerVersion = Schema.decodeUnknownEffect(
41 Schema.fromJsonString(IconComposerVersion),42 Schema.fromJsonString(IconComposerVersion),
42);43);
4344
44type IconPlatform = "iOS";45type IconPlatform = "iOS" | "macOS";
4546
46interface VariantOutputs {47interface VariantOutputs {
47 readonly ios: string;48 readonly ios: string;
...@@ -204,7 +205,7 @@ export class IconExportAssetsStaleError extends Schema.TaggedError<IconExportAss...@@ -204,7 +205,7 @@ export class IconExportAssetsStaleError extends Schema.TaggedError<IconExportAss
204205
205const ICON_VARIANTS = [206const ICON_VARIANTS = [
206 {207 {
207 label: "development",208 label: "knife",
208 source: BRAND_ASSET_PATHS.developmentIconComposerProject,209 source: BRAND_ASSET_PATHS.developmentIconComposerProject,
209 outputs: {210 outputs: {
210 ios: BRAND_ASSET_PATHS.developmentIosIconPng,211 ios: BRAND_ASSET_PATHS.developmentIosIconPng,
...@@ -217,44 +218,8 @@ const ICON_VARIANTS = [...@@ -217,44 +218,8 @@ const ICON_VARIANTS = [
217 windowsIco: BRAND_ASSET_PATHS.developmentWindowsIconIco,218 windowsIco: BRAND_ASSET_PATHS.developmentWindowsIconIco,
218 },219 },
219 },220 },
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 },
248] as const satisfies ReadonlyArray<IconVariant>;221] 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
258const RepositoryRoot = Effect.service(Path.Path).pipe(223const RepositoryRoot = Effect.service(Path.Path).pipe(
259 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),224 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
260 Effect.mapError(225 Effect.mapError(
...@@ -586,6 +551,36 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (...@@ -586,6 +551,36 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
586 });551 });
587552
588 const ios = yield* render("iOS", 1024);553 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 });
589 const icoRenditions = yield* Effect.forEach(584 const icoRenditions = yield* Effect.forEach(
590 WINDOWS_ICON_SIZES,585 WINDOWS_ICON_SIZES,
591 (size) => render("iOS", size).pipe(Effect.map((contents) => ({ size, contents }))),586 (size) => render("iOS", size).pipe(Effect.map((contents) => ({ size, contents }))),
...@@ -598,6 +593,7 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (...@@ -598,6 +593,7 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
598593
599 return new Map<string, Buffer>([594 return new Map<string, Buffer>([
600 [variant.outputs.ios, ios],595 [variant.outputs.ios, ios],
596 [variant.outputs.macos, macos],
601 [variant.outputs.universal, ios],597 [variant.outputs.universal, ios],
602 [variant.outputs.appleTouch, yield* render("iOS", 180)],598 [variant.outputs.appleTouch, yield* render("iOS", 180)],
603 [variant.outputs.favicon16, yield* render("iOS", 16)],599 [variant.outputs.favicon16, yield* render("iOS", 16)],
...@@ -607,24 +603,6 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (...@@ -607,24 +603,6 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
607 ]);603 ]);
608});604});
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
628const writeAtomically = Effect.fn("iconExport.writeAtomically")(function* (606const writeAtomically = Effect.fn("iconExport.writeAtomically")(function* (
629 repositoryRoot: string,607 repositoryRoot: string,
630 relativePath: string,608 relativePath: string,
...@@ -774,7 +752,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn...@@ -774,7 +752,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn
774 });752 });
775 }753 }
776 yield* Console.log(`All ${generated.size} generated icon assets are current.`);754 yield* Console.log(`All ${generated.size} generated icon assets are current.`);
777 yield* logManualMacOsExportInstructions();
778 return;755 return;
779 }756 }
780757
...@@ -784,7 +761,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn...@@ -784,7 +761,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn
784 { concurrency: 1, discard: true },761 { concurrency: 1, discard: true },
785 );762 );
786 yield* Console.log(`Updated ${generated.size} generated icon assets.`);763 yield* Console.log(`Updated ${generated.size} generated icon assets.`);
787 yield* logManualMacOsExportInstructions();
788});764});
789765
790export const exportBrandIconsCommand = Command.make(766export const exportBrandIconsCommand = Command.make(
...@@ -796,11 +772,7 @@ export const exportBrandIconsCommand = Command.make(...@@ -796,11 +772,7 @@ export const exportBrandIconsCommand = Command.make(
796 ),772 ),
797 },773 },
798 ({ check }) => exportBrandIcons(check).pipe(Effect.scoped),774 ({ check }) => exportBrandIcons(check).pipe(Effect.scoped),
799).pipe(775).pipe(Command.withDescription("Export The Knife assets from its Icon Composer project."));
800 Command.withDescription(
801 "Export development, preview, and production assets from Icon Composer projects.",
802 ),
803);
804776
805if (import.meta.main) {777if (import.meta.main) {
806 Command.run(exportBrandIconsCommand, { version: "0.0.0" }).pipe(778 Command.run(exportBrandIconsCommand, { version: "0.0.0" }).pipe(
scripts/lib/brand-assets.test.ts-15
...@@ -83,19 +83,4 @@ describe("brand-assets", () => {...@@ -83,19 +83,4 @@ describe("brand-assets", () => {
83 expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly");83 expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly");
84 expect(resolveWebAssetBrandForPackageVersion("0.0.29-preview.20260723.882")).toBe("nightly");84 expect(resolveWebAssetBrandForPackageVersion("0.0.29-preview.20260723.882")).toBe("nightly");
85 });85 });
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 });
101});86});
scripts/lib/brand-assets.ts+27-30
...@@ -1,34 +1,31 @@...@@ -1,34 +1,31 @@
1export const BRAND_ASSET_PATHS = {1export const BRAND_ASSET_PATHS = {
2 developmentIconComposerProject: "assets/dev/app-icon.icon",2 developmentIconComposerProject: "assets/knife/app-icon.icon",
3 developmentIosIconPng: "assets/dev/blueprint-ios-1024.png",3 developmentIosIconPng: "assets/knife/icon-1024.png",
4 developmentUniversalIconPng: "assets/dev/blueprint-universal-1024.png",4 developmentUniversalIconPng: "assets/knife/icon-1024.png",
55 productionIconComposerProject: "assets/knife/app-icon.icon",
6 productionIconComposerProject: "assets/prod/app-icon.icon",6 productionIosIconPng: "assets/knife/icon-1024.png",
7 productionIosIconPng: "assets/prod/black-ios-1024.png",7 productionMacIconPng: "assets/knife/macos-1024.png",
8 productionMacIconPng: "assets/prod/black-macos-1024.png",8 productionLinuxIconPng: "assets/knife/icon-1024.png",
9 productionLinuxIconPng: "assets/prod/black-universal-1024.png",9 productionWindowsIconIco: "assets/knife/icon.ico",
10 productionWindowsIconIco: "assets/prod/t3-black-windows.ico",10 productionWebFaviconIco: "assets/knife/icon.ico",
11 productionWebFaviconIco: "assets/prod/t3-black-web-favicon.ico",11 productionWebFavicon16Png: "assets/knife/favicon-16x16.png",
12 productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png",12 productionWebFavicon32Png: "assets/knife/favicon-32x32.png",
13 productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png",13 productionWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
14 productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png",14 nightlyIconComposerProject: "assets/knife/app-icon.icon",
1515 nightlyIosIconPng: "assets/knife/icon-1024.png",
16 nightlyIconComposerProject: "assets/nightly/app-icon.icon",16 nightlyMacIconPng: "assets/knife/macos-1024.png",
17 nightlyIosIconPng: "assets/nightly/nightly-ios-1024.png",17 nightlyLinuxIconPng: "assets/knife/icon-1024.png",
18 nightlyMacIconPng: "assets/nightly/nightly-macos-1024.png",18 nightlyWindowsIconIco: "assets/knife/icon.ico",
19 nightlyLinuxIconPng: "assets/nightly/nightly-universal-1024.png",19 nightlyWebFaviconIco: "assets/knife/icon.ico",
20 nightlyWindowsIconIco: "assets/nightly/nightly-windows.ico",20 nightlyWebFavicon16Png: "assets/knife/favicon-16x16.png",
21 nightlyWebFaviconIco: "assets/nightly/nightly-web-favicon.ico",21 nightlyWebFavicon32Png: "assets/knife/favicon-32x32.png",
22 nightlyWebFavicon16Png: "assets/nightly/nightly-web-favicon-16x16.png",22 nightlyWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
23 nightlyWebFavicon32Png: "assets/nightly/nightly-web-favicon-32x32.png",23 developmentDesktopIconPng: "assets/knife/macos-1024.png",
24 nightlyWebAppleTouchIconPng: "assets/nightly/nightly-web-apple-touch-180.png",24 developmentWindowsIconIco: "assets/knife/icon.ico",
2525 developmentWebFaviconIco: "assets/knife/icon.ico",
26 developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png",26 developmentWebFavicon16Png: "assets/knife/favicon-16x16.png",
27 developmentWindowsIconIco: "assets/dev/blueprint-windows.ico",27 developmentWebFavicon32Png: "assets/knife/favicon-32x32.png",
28 developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico",28 developmentWebAppleTouchIconPng: "assets/knife/apple-touch-180.png",
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",
32} as const;29} as const;
3330
34export type WebAssetBrand = "development" | "nightly" | "production";31export type WebAssetBrand = "development" | "nightly" | "production";
scripts/mobile-native-client.ts+3-3
...@@ -348,9 +348,9 @@ const main = Command.make(...@@ -348,9 +348,9 @@ const main = Command.make(
348 [348 [
349 "xcodebuild",349 "xcodebuild",
350 "-workspace",350 "-workspace",
351 path.join(mobile, "ios/T3CodeDev.xcworkspace"),351 path.join(mobile, "ios/TheKnifeDev.xcworkspace"),
352 "-scheme",352 "-scheme",
353 "T3CodeDev",353 "TheKnifeDev",
354 "-configuration",354 "-configuration",
355 "Debug",355 "Debug",
356 "-destination",356 "-destination",
...@@ -367,7 +367,7 @@ const main = Command.make(...@@ -367,7 +367,7 @@ const main = Command.make(
367 "simctl",367 "simctl",
368 "install",368 "install",
369 device,369 device,
370 path.join(output, "Build/Products/Debug-iphonesimulator/T3CodeDev.app"),370 path.join(output, "Build/Products/Debug-iphonesimulator/TheKnifeDev.app"),
371 ],371 ],
372 true,372 true,
373 );373 );
scripts/mobile-showcase.ts+3-3
...@@ -42,7 +42,7 @@ const SERVER_HOST = "0.0.0.0";...@@ -42,7 +42,7 @@ const SERVER_HOST = "0.0.0.0";
42const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";42const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";
43const IOS_APP_PATH = NodePath.join(43const IOS_APP_PATH = NodePath.join(
44 MOBILE_ROOT,44 MOBILE_ROOT,
45 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app",45 ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/TheKnife.app",
46);46);
47const ANDROID_APK_PATH = NodePath.join(47const ANDROID_APK_PATH = NodePath.join(
48 MOBILE_ROOT,48 MOBILE_ROOT,
...@@ -707,9 +707,9 @@ async function buildIos(): Promise<string> {...@@ -707,9 +707,9 @@ async function buildIos(): Promise<string> {
707 "xcodebuild",707 "xcodebuild",
708 [708 [
709 "-workspace",709 "-workspace",
710 NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"),710 NodePath.join(MOBILE_ROOT, "ios/TheKnife.xcworkspace"),
711 "-scheme",711 "-scheme",
712 "T3Code",712 "TheKnife",
713 "-configuration",713 "-configuration",
714 "Debug",714 "Debug",
715 "-sdk",715 "-sdk",