| author | |
| committer | |
| log | 1dced964e7161946a640107004968697e5b258b8 |
| tree | cd98d50fd695ba6c072d917a9b55b9d257890c8a |
| parent | 339179f50a84dc599cce373dbb48f172f182f44e |
| signature | Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ (~clover) |
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-astra175 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 | 26 | [SQLite fixture reference](../test-t3-app/references/sqlite-fixtures.md) only |
| 27 | 27 | when inspecting or seeding SQLite. Stop the test server before fixture writes. |
| 28 | 28 | |
| 29 | ## Launch T3 Code Dev | |
| 29 | ## Launch The Knife Dev | |
| 30 | 30 | |
| 31 | 31 | From the checkout being tested on the selected device host, run: |
| 32 | 32 | |
| ... | ... | @@ -57,7 +57,7 @@ arguments stored in `agent_device_command` and the Bash array |
| 57 | 57 | "$agent_device_command" "${agent_device_target_args[@]}" |
| 58 | 58 | ``` |
| 59 | 59 | |
| 60 | It issues a fresh credential and opens T3 Code Dev's existing pairing route | |
| 60 | It issues a fresh credential and opens The Knife Dev's existing pairing route | |
| 61 | 61 | through AgentDevice. For a backend on the device host, use |
| 62 | 62 | `http://127.0.0.1:<server-port>` on iOS or `http://10.0.2.2:<server-port>` |
| 63 | 63 | on Android. For a remote backend, use its reachable origin. |
.gitignore+1| ... | ... | @@ -45,3 +45,4 @@ node_modules/ |
| 45 | 45 | |
| 46 | 46 | # pnpm content-addressable store; never belongs in the repo. |
| 47 | 47 | .pnpm-store/ |
| 48 | /apps/web/public/fonts/private/ |
AGENTS.md+42-11| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | # T3 Code | |
| 1 | # The Knife | |
| 2 | ||
| 3 | The 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. | |
| 2 | 4 | |
| 3 | 5 | T3 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. |
| 4 | 6 | |
| ... | ... | @@ -104,16 +106,6 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real |
| 104 | 106 | |
| 105 | 107 | For 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. |
| 106 | 108 | |
| 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 | 109 | ## Documentation |
| 118 | 110 | |
| 119 | 111 | Most 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 | 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 | 119 | - `docs/operations/` holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides. |
| 128 | 120 | |
| 121 | ## Fork patch workflow | |
| 122 | ||
| 123 | This checkout is a fork of T3 code for Clover. All changes should be | |
| 124 | categorized into a set of well-maintained changes on top of the upstream. When | |
| 125 | a change to something you already patched is requested, the changes are | |
| 126 | squashed in. This allows rebasing without having hundreds of changes. The | |
| 127 | patches can also be cherry-picked for upstreaming if Theo or someone elses fork | |
| 128 | wants 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 | 160 | ## Plans and work artifacts |
| 130 | 161 | |
| 131 | 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 | # T3 Code | |
| 1 | # The Knife | |
| 2 | 2 | |
| 3 | T3 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). | |
| 3 | A fork of [T3 Code](https://t3.codes) with more precise engineering from the | |
| 4 | backend to the UI/UX. | |
| 4 | 5 | |
| 5 | Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, and Google Antigravity. If they're set up on your computer, T3 Code can control them. | |
| 6 | ||
| 7 | ## "Wait, what are you selling me?" | |
| 8 | ||
| 9 | Nothing. 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 | ||
| 11 | We 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 | |
| 28 | curl -fsSL https://t3.codes/install.sh | sh | |
| 29 | ``` | |
| 30 | ||
| 31 | On Windows, in PowerShell: | |
| 32 | ||
| 33 | ```powershell | |
| 34 | irm https://t3.codes/install.ps1 | iex | |
| 35 | ``` | |
| 36 | ||
| 37 | Then 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 | ||
| 39 | To try it once without installing, run `npx t3@latest` instead. | |
| 40 | ||
| 41 | ### Desktop app | |
| 42 | ||
| 43 | Install 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 | |
| 48 | winget install T3Tools.T3Code | |
| 49 | ``` | |
| 50 | ||
| 51 | #### Windows (`scoop`) | |
| 52 | ||
| 53 | ```bash | |
| 54 | scoop install extras/t3code | |
| 55 | ``` | |
| 56 | ||
| 57 | #### macOS (Homebrew) | |
| 58 | ||
| 59 | ```bash | |
| 60 | brew install --cask t3-code | |
| 61 | ``` | |
| 62 | ||
| 63 | #### Debian, Ubuntu (`.deb`) | |
| 64 | ||
| 65 | Download the `.deb` from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), then: | |
| 66 | ||
| 67 | ```bash | |
| 68 | sudo apt install ./T3-Code-*.deb | |
| 69 | ``` | |
| 70 | ||
| 71 | #### Arch Linux (AUR) | |
| 72 | ||
| 73 | Stable: | |
| 74 | ||
| 75 | ```bash | |
| 76 | yay -S t3code-bin | |
| 77 | ``` | |
| 78 | ||
| 79 | Nightly: | |
| 80 | ||
| 81 | ```bash | |
| 82 | yay -S t3code-nightly-bin | |
| 83 | ``` | |
| 84 | ||
| 85 | The AUR packaging is maintained in this repository under [`packaging/aur`](./packaging/aur). | |
| 86 | ||
| 87 | ## Some notes | |
| 88 | ||
| 89 | We are very very early in this project. Expect bugs. | |
| 90 | ||
| 91 | We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be. | |
| 92 | ||
| 93 | ## Documentation | |
| 94 | ||
| 95 | Full 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 | ||
| 109 | Building 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 | ||
| 115 | T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool. | |
| 116 | ||
| 117 | #### macOS / Linux | |
| 118 | ||
| 119 | ```bash | |
| 120 | curl -fsSL https://vite.plus | bash | |
| 121 | ``` | |
| 122 | ||
| 123 | #### Windows | |
| 124 | ||
| 125 | ```bash | |
| 126 | irm https://vite.plus/ps1 | iex | |
| 127 | ``` | |
| 128 | ||
| 129 | Checkout their getting started guide for more information: https://viteplus.dev/guide/ | |
| 130 | ||
| 131 | ### Install dependencies | |
| 132 | ||
| 133 | ```bash | |
| 134 | vp i | |
| 135 | ``` | |
| 136 | ||
| 137 | Read [CONTRIBUTING.md](./CONTRIBUTING.md) before reporting a bug or opening a PR. | |
| 138 | ||
| 139 | Have a feature request? Start an [Ideas discussion](https://github.com/pingdotgg/t3code/discussions/categories/ideas). | |
| 140 | ||
| 141 | Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv). | |
| 6 | - [Read patches](https://shale.paperclover.net/the-knife/log). | |
| 7 | - Download for [Apple Silicon Mac](https://file.paperclover.net/shr/the-knife/The-Knife-darwin-arm64.zip), [Windows](https://file.paperclover.net/shr/the-knife/The-Knife-win32-x64.exe), or [Linux x64](https://file.paperclover.net/shr/the-knife/The-Knife-linux-x64.AppImage). |
apps/desktop/gnome-extension/captureFeedback.js+1-1| ... | ... | @@ -110,7 +110,7 @@ export class CaptureFeedback { |
| 110 | 110 | check(); |
| 111 | 111 | })); |
| 112 | 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 | 114 | session.target = target; |
| 115 | 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 | 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 | 47 | const pid = options ? await this._getProcessId(sender) : undefined; |
| 48 | 48 | this._checkSession(); |
| 49 | 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 | 23 | |
| 24 | 24 | it("rejects other clients before looking at the active window", async () => { |
| 25 | 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 | 27 | expect(snapshots()).toBe(0); |
| 28 | 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 | 38 | const { service } = fixture({ getNameOwner: async () => owner }); |
| 39 | 39 | expect(await service.capture(owner)).toBe("pixels"); |
| 40 | 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 | }); |
| 43 | 43 | |
| 44 | 44 | it("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 | 94 | await expect(service.capture(":1.23")).rejects.toThrow("already in progress"); |
| 95 | 95 | finish(new Error("gone")); |
| 96 | 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 | }); |
| 99 | 99 | |
| 100 | 100 | it("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 | 1 | { |
| 2 | 2 | "uuid": "snap-shot@t3.codes", |
| 3 | "name": "T3 Code SnapShots", | |
| 4 | "description": "Allows T3 Code to capture the focused window, identify it for optional accessibility text, and animate it into T3 Code. Disable this extension to revoke access.", | |
| 3 | "name": "The Knife SnapShots", | |
| 4 | "description": "Allows The Knife to capture the focused window, identify it for optional accessibility text, and animate it into The Knife. Disable this extension to revoke access.", | |
| 5 | 5 | "shell-version": ["45", "46", "47", "48", "49", "50"], |
| 6 | 6 | "version": 2, |
| 7 | 7 | "url": "https://github.com/pingdotgg/t3code" |
apps/desktop/package.json+1-1| ... | ... | @@ -42,5 +42,5 @@ |
| 42 | 42 | "tailwindcss": "^4.0.0", |
| 43 | 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 | 20 | <svg x="32" y="23" width="30" height="18" viewBox="15.5309 37 94.3941 56.96" fill="#303034"> |
| 21 | 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 | 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 | 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 | 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 | 26 | </svg> |
apps/desktop/resources/dmg/dmg-background-nightly.svg+1-1| ... | ... | @@ -27,7 +27,7 @@ |
| 27 | 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 | 28 | </svg> |
| 29 | 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 | 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 | 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 | 33 | </svg> |
apps/desktop/resources/linux/com.t3tools.t3code.metainfo.xml+2-2| ... | ... | @@ -3,14 +3,14 @@ |
| 3 | 3 | <id>com.t3tools.t3code</id> |
| 4 | 4 | <metadata_license>CC0-1.0</metadata_license> |
| 5 | 5 | <project_license>MIT</project_license> |
| 6 | <name>T3 Code</name> | |
| 6 | <name>The Knife</name> | |
| 7 | 7 | <summary>Desktop GUI for coding agents</summary> |
| 8 | 8 | <developer id="com.t3tools"> |
| 9 | 9 | <name>T3 Tools Inc.</name> |
| 10 | 10 | </developer> |
| 11 | 11 | <description> |
| 12 | 12 | <p> |
| 13 | T3 Code is an open-source desktop app for coding agents. Work with your | |
| 13 | The Knife is an open-source desktop app for coding agents. Work with your | |
| 14 | 14 | existing agent subscriptions, review code changes, and run commands in |
| 15 | 15 | your projects. |
| 16 | 16 | </p> |
apps/desktop/scripts/electron-launcher.mjs+9-18| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | // This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron" | |
| 1 | // This file mostly exists because we want dev mode to say "The Knife (Dev)" instead of "electron" | |
| 2 | 2 | |
| 3 | 3 | import * as NodeChildProcess from "node:child_process"; |
| 4 | 4 | import * as NodeFS from "node:fs"; |
| ... | ... | @@ -15,19 +15,13 @@ const repoRoot = NodePath.resolve(desktopDir, "..", ".."); |
| 15 | 15 | const devBundleIdSuffix = NodePath.basename(repoRoot) |
| 16 | 16 | .toLowerCase() |
| 17 | 17 | .replaceAll(/[^a-z0-9]+/g, ""); |
| 18 | const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; | |
| 18 | const APP_DISPLAY_NAME = isDevelopment ? "The Knife (Dev)" : "The Knife"; | |
| 19 | 19 | const APP_BUNDLE_ID = isDevelopment |
| 20 | 20 | ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}` |
| 21 | 21 | : "com.t3tools.t3code"; |
| 22 | 22 | const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"]; |
| 23 | 23 | const LAUNCHER_VERSION = 20; |
| 24 | const developmentMacIconPngPath = NodePath.join( | |
| 25 | repoRoot, | |
| 26 | "assets", | |
| 27 | "dev", | |
| 28 | "blueprint-macos-1024.png", | |
| 29 | ); | |
| 30 | const productionMacIconPngPath = NodePath.join(repoRoot, "assets", "prod", "black-macos-1024.png"); | |
| 24 | const macIconPngPath = NodePath.join(repoRoot, "assets", "knife", "macos-1024.png"); | |
| 31 | 25 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime. |
| 32 | 26 | const hostPlatform = NodeOS.platform(); |
| 33 | 27 | |
| ... | ... | @@ -203,13 +197,10 @@ function registerMacLauncherBundle(appBundlePath) { |
| 203 | 197 | } |
| 204 | 198 | |
| 205 | 199 | // Bundle-internal paths are macOS paths whatever host builds them. |
| 206 | export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) { | |
| 200 | export function resolveMacLauncherIconPaths(runtimeDir) { | |
| 207 | 201 | return { |
| 208 | sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath, | |
| 209 | generatedIconPath: NodePath.posix.join( | |
| 210 | runtimeDir, | |
| 211 | development ? "icon-dev.icns" : "icon-prod.icns", | |
| 212 | ), | |
| 202 | sourceIconPath: macIconPngPath, | |
| 203 | generatedIconPath: NodePath.posix.join(runtimeDir, "icon.icns"), | |
| 213 | 204 | }; |
| 214 | 205 | } |
| 215 | 206 | |
| ... | ... | @@ -270,8 +261,8 @@ export function resolveMacBundleInfoPlistStrings(executableName) { |
| 270 | 261 | CFBundleExecutable: executableName, |
| 271 | 262 | CFBundleIconFile: "icon.icns", |
| 272 | 263 | NSScreenCaptureUsageDescription: |
| 273 | "T3 Code captures the active window when you use the snapshot shortcut.", | |
| 274 | NSDocumentsFolderUsageDescription: "T3 Code reads project files you open in the desktop app.", | |
| 264 | "The Knife captures the active window when you use the snapshot shortcut.", | |
| 265 | NSDocumentsFolderUsageDescription: "The Knife reads project files you open in the desktop app.", | |
| 275 | 266 | }; |
| 276 | 267 | } |
| 277 | 268 | |
| ... | ... | @@ -404,7 +395,7 @@ function buildMacLauncher(electronBinaryPath) { |
| 404 | 395 | if (isDevelopment) { |
| 405 | 396 | // Keep Electron's native executable inside the branded bundle. Launching the |
| 406 | 397 | // 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 | 399 | // Its conventional executable name also keeps Electron's default-app runtime |
| 409 | 400 | // in development mode instead of making app.isPackaged report true. |
| 410 | 401 | writeDevelopmentEnvironmentScript(); |
apps/desktop/scripts/electron-launcher.test.mjs+15-20| ... | ... | @@ -76,18 +76,18 @@ describe("electron development launcher", () => { |
| 76 | 76 | |
| 77 | 77 | it("keeps the native Electron executable name inside the branded macOS bundle", () => { |
| 78 | 78 | const paths = resolveMacLauncherPaths( |
| 79 | "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app", | |
| 80 | "T3 Code (Dev)", | |
| 79 | "/repo/apps/desktop/.electron-runtime/The Knife (Dev).app", | |
| 80 | "The Knife (Dev)", | |
| 81 | 81 | ); |
| 82 | 82 | |
| 83 | assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher"); | |
| 83 | assert.equal(paths.launcherExecutableName, "The Knife (Dev) Launcher"); | |
| 84 | 84 | assert.equal( |
| 85 | 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 | 88 | assert.equal( |
| 89 | 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 | ); |
| 92 | 92 | |
| 93 | 93 | const script = makeDevelopmentLauncherScript({ |
| ... | ... | @@ -98,32 +98,32 @@ describe("electron development launcher", () => { |
| 98 | 98 | }); |
| 99 | 99 | assert.include( |
| 100 | 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 | 103 | assert.notInclude(script, "node_modules/electron"); |
| 104 | 104 | }); |
| 105 | 105 | |
| 106 | 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"); | |
| 108 | 108 | |
| 109 | 109 | assert.equal( |
| 110 | 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 | 113 | assert.equal( |
| 114 | 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 | }); |
| 118 | 118 | |
| 119 | 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 | 121 | "--force", |
| 122 | 122 | "--deep", |
| 123 | 123 | "--sign", |
| 124 | 124 | "-", |
| 125 | 125 | "--timestamp=none", |
| 126 | "/runtime/T3 Code (Dev).app", | |
| 126 | "/runtime/The Knife (Dev).app", | |
| 127 | 127 | ]); |
| 128 | 128 | }); |
| 129 | 129 | |
| ... | ... | @@ -141,14 +141,9 @@ describe("electron development launcher", () => { |
| 141 | 141 | } |
| 142 | 142 | }); |
| 143 | 143 | |
| 144 | it("derives launcher icons from canonical development and production assets", () => { | |
| 145 | const development = resolveMacLauncherIconPaths("/runtime", true); | |
| 146 | const production = resolveMacLauncherIconPaths("/runtime", false); | |
| 147 | ||
| 148 | // The source icons are real repo paths, joined for the host. | |
| 149 | assert.match(development.sourceIconPath, /assets[\\/]dev[\\/]blueprint-macos-1024\.png$/); | |
| 150 | assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns"); | |
| 151 | assert.match(production.sourceIconPath, /assets[\\/]prod[\\/]black-macos-1024\.png$/); | |
| 152 | assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns"); | |
| 144 | it("derives launcher icons from the canonical Knife artwork", () => { | |
| 145 | const icon = resolveMacLauncherIconPaths("/runtime"); | |
| 146 | assert.match(icon.sourceIconPath, /assets[\\/]knife[\\/]macos-1024\.png$/); | |
| 147 | assert.equal(icon.generatedIconPath, "/runtime/icon.icns"); | |
| 153 | 148 | }); |
| 154 | 149 | }); |
apps/desktop/scripts/verify-preload-bundle.mjs+1| ... | ... | @@ -108,6 +108,7 @@ const executeBundle = (source, sandboxModules) => { |
| 108 | 108 | { |
| 109 | 109 | process: sandboxProcess, |
| 110 | 110 | require: requireSandboxModule, |
| 111 | window: { addEventListener: () => undefined }, | |
| 111 | 112 | }, |
| 112 | 113 | { |
| 113 | 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 | 136 | const wasQuitting = yield* Ref.getAndSet(state.quitting, true); |
| 137 | 137 | if (!wasQuitting) { |
| 138 | 138 | yield* electronDialog.showErrorBox( |
| 139 | "T3 Code failed to start", | |
| 139 | "The Knife failed to start", | |
| 140 | 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 | 171 | activeRequestId = parsed.requestId; |
| 172 | 172 | void input.handle(parsed).then(finish, () => { |
| 173 | 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 | 45 | request(request: DesktopAppActivationRequest): Promise<DesktopAppActivationResponse> { |
| 46 | 46 | if (this.#closed) { |
| 47 | 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 | 51 | if (this.#pending.has(request.requestId)) { |
| ... | ... | @@ -90,7 +90,7 @@ export class DesktopAppActivationBroker { |
| 90 | 90 | failure( |
| 91 | 91 | pending.request.requestId, |
| 92 | 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 | 103 | |
| 104 | 104 | cancel(requestId: string): void { |
| 105 | 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 | } |
| 109 | 109 | |
| ... | ... | @@ -112,7 +112,7 @@ export class DesktopAppActivationBroker { |
| 112 | 112 | this.#renderer = null; |
| 113 | 113 | for (const pending of this.#pending.values()) { |
| 114 | 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 | 218 | yield* identity.configure; |
| 219 | 219 | |
| 220 | 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 | 222 | assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); |
| 223 | 223 | assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); |
| 224 | 224 | // Packaged: the bundle's own icon stands, so a custom one the user |
| ... | ... | @@ -247,28 +247,34 @@ describe("DesktopAppIdentity", () => { |
| 247 | 247 | stage: "Dev", |
| 248 | 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 }) => { | |
| 251 | const calls: ElectronAppCalls = { | |
| 252 | setAboutPanelOptions: [], | |
| 253 | setDockIcon: [], | |
| 254 | setName: [], | |
| 255 | }; | |
| 250 | ])( | |
| 251 | "preserves the credential identity and valid native User-Agent for $stage", | |
| 252 | ({ stage, environment }) => { | |
| 253 | const calls: ElectronAppCalls = { | |
| 254 | setAboutPanelOptions: [], | |
| 255 | setDockIcon: [], | |
| 256 | setName: [], | |
| 257 | }; | |
| 256 | 258 | |
| 257 | return withIdentity( | |
| 258 | Effect.gen(function* () { | |
| 259 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; | |
| 260 | yield* identity.configure; | |
| 259 | return withIdentity( | |
| 260 | Effect.gen(function* () { | |
| 261 | const identity = yield* DesktopAppIdentity.DesktopAppIdentity; | |
| 262 | yield* identity.configure; | |
| 261 | 263 | |
| 262 | const runtimeName = calls.setName[0]; | |
| 263 | assert.isDefined(runtimeName); | |
| 264 | assert.equal(runtimeName, `T3 Code ${stage}`); | |
| 265 | // RFC 9110's token grammar, after Electron removes ASCII spaces. | |
| 266 | assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/); | |
| 267 | assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`); | |
| 268 | }), | |
| 269 | { calls, environment }, | |
| 270 | ); | |
| 271 | }); | |
| 264 | const runtimeName = calls.setName[0]; | |
| 265 | assert.isDefined(runtimeName); | |
| 266 | assert.equal(runtimeName, `T3 Code ${stage}`); | |
| 267 | // RFC 9110's token grammar, after Electron removes ASCII spaces. | |
| 268 | assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/); | |
| 269 | assert.equal( | |
| 270 | calls.setAboutPanelOptions[0]?.applicationName, | |
| 271 | stage === "Alpha" ? "The Knife" : `The Knife (${stage})`, | |
| 272 | ); | |
| 273 | }), | |
| 274 | { calls, environment }, | |
| 275 | ); | |
| 276 | }, | |
| 277 | ); | |
| 272 | 278 | |
| 273 | 279 | it.effect("sets the dock icon only when running unpackaged", () => { |
| 274 | 280 | const calls: ElectronAppCalls = { |
apps/desktop/src/app/DesktopAppIdentity.ts+2-6| ... | ... | @@ -91,12 +91,8 @@ export const make = Effect.gen(function* () { |
| 91 | 91 | |
| 92 | 92 | const configure = Effect.gen(function* () { |
| 93 | 93 | const commitHash = yield* resolveAboutCommitHash; |
| 94 | // Electron removes spaces from this name to build the native User-Agent | |
| 95 | // product token, but leaves parentheses intact. Keep the runtime name valid | |
| 96 | // without rewriting preview sessions (which breaks Turnstile, #7110). | |
| 97 | yield* electronApp.setName( | |
| 98 | `${environment.branding.baseName} ${environment.branding.stageLabel}`, | |
| 99 | ); | |
| 94 | // Electron's runtime name also identifies the shipped Safe Storage/keyring entry. | |
| 95 | yield* electronApp.setName(`T3 Code ${environment.branding.stageLabel}`); | |
| 100 | 96 | yield* electronApp.setAboutPanelOptions({ |
| 101 | 97 | applicationName: environment.displayName, |
| 102 | 98 | applicationVersion: environment.appVersion, |
apps/desktop/src/app/DesktopAssets.test.ts+3-3| ... | ... | @@ -50,7 +50,7 @@ describe("DesktopAssets", () => { |
| 50 | 50 | ), |
| 51 | 51 | ); |
| 52 | 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 | 55 | const assets = yield* DesktopAssets.DesktopAssets.pipe( |
| 56 | 56 | Effect.provide( |
| ... | ... | @@ -62,8 +62,8 @@ describe("DesktopAssets", () => { |
| 62 | 62 | |
| 63 | 63 | const icons = yield* assets.iconPaths; |
| 64 | 64 | |
| 65 | assert.match(Option.getOrThrow(icons.ico), /assets\/dev\/blueprint-windows\.ico$/); | |
| 66 | assert.match(Option.getOrThrow(icons.png), /assets\/dev\/blueprint-universal-1024\.png$/); | |
| 65 | assert.match(Option.getOrThrow(icons.ico), /assets\/knife\/icon\.ico$/); | |
| 66 | assert.match(Option.getOrThrow(icons.png), /assets\/knife\/icon-1024\.png$/); | |
| 67 | 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 | 61 | return Option.none<string>(); |
| 62 | 62 | }); |
| 63 | 63 | |
| 64 | const 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 | ||
| 77 | 64 | function resolveSourceTreeIconPath( |
| 78 | 65 | environment: DesktopEnvironment.DesktopEnvironment["Service"], |
| 79 | 66 | ext: keyof DesktopIconPaths, |
| 80 | 67 | ): string | undefined { |
| 81 | 68 | if (environment.isPackaged || ext === "icns") return undefined; |
| 82 | const brand = environment.isDevelopment ? "dev" : "prod"; | |
| 83 | const fileNames = sourceTreeIconFileNames[brand]; | |
| 84 | 69 | const fileName = |
| 85 | 70 | ext === "ico" |
| 86 | ? fileNames.ico | |
| 71 | ? "icon.ico" | |
| 87 | 72 | : environment.platform === "darwin" |
| 88 | ? fileNames.macPng | |
| 89 | : fileNames.universalPng; | |
| 90 | return environment.path.join(environment.rootDir, "assets", brand, fileName); | |
| 73 | ? "macos-1024.png" | |
| 74 | : "icon-1024.png"; | |
| 75 | return environment.path.join(environment.rootDir, "assets", "knife", fileName); | |
| 91 | 76 | } |
| 92 | 77 | |
| 93 | 78 | const 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 | 24 | const cmdText = (value: string) => value.replaceAll("%", "%%"); |
| 25 | 25 | const cmdWord = (value: string) => `"${cmdText(value)}"`; |
| 26 | 26 | |
| 27 | const MOVED = "T3 Code has moved or been removed. Open the app once to update this command."; | |
| 27 | const MOVED = "The Knife has moved or been removed. Open the app once to update this command."; | |
| 28 | 28 | |
| 29 | 29 | export type CliShimTarget = |
| 30 | 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 | 93 | } |
| 94 | 94 | >()("@t3tools/desktop/app/DesktopEnvironment") {} |
| 95 | 95 | |
| 96 | const APP_BASE_NAME = "T3 Code"; | |
| 96 | const APP_BASE_NAME = "The Knife"; | |
| 97 | 97 | |
| 98 | 98 | function resolveDesktopAppStageLabel(input: { |
| 99 | 99 | readonly isDevelopment: boolean; |
| ... | ... | @@ -114,7 +114,7 @@ export function resolveDesktopAppBranding(input: { |
| 114 | 114 | return { |
| 115 | 115 | baseName: APP_BASE_NAME, |
| 116 | 116 | stageLabel, |
| 117 | displayName: `${APP_BASE_NAME} (${stageLabel})`, | |
| 117 | displayName: stageLabel === "Alpha" ? APP_BASE_NAME : `${APP_BASE_NAME} (${stageLabel})`, | |
| 118 | 118 | }; |
| 119 | 119 | } |
| 120 | 120 |
apps/desktop/src/app/DesktopWebLinks.ts+2-2| ... | ... | @@ -6,14 +6,14 @@ import * as Option from "effect/Option"; |
| 6 | 6 | import * as ElectronWindow from "../electron/ElectronWindow.ts"; |
| 7 | 7 | import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts"; |
| 8 | 8 | |
| 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. */ | |
| 10 | 10 | export const isWebLink = (value: string) => { |
| 11 | 11 | if (!URL.canParse(value)) return false; |
| 12 | 12 | const { protocol } = new URL(value); |
| 13 | 13 | return protocol === "http:" || protocol === "https:"; |
| 14 | 14 | }; |
| 15 | 15 | |
| 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. */ | |
| 17 | 17 | export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path); |
| 18 | 18 | |
| 19 | 19 | /** |
apps/desktop/src/backend/DesktopBackendPool.ts+3-3| ... | ... | @@ -247,7 +247,7 @@ export const layer = Layer.effect( |
| 247 | 247 | ); |
| 248 | 248 | yield* electronDialog.showErrorBox( |
| 249 | 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 | 252 | yield* appSettings.applyWslWindowsFallbackInMemory; |
| 253 | 253 | return true; |
| ... | ... | @@ -258,7 +258,7 @@ export const layer = Layer.effect( |
| 258 | 258 | }); |
| 259 | 259 | yield* electronDialog.showErrorBox( |
| 260 | 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 | 263 | // Fully disable the WSL backend — both flags, matching the "Switch to |
| 264 | 264 | // Windows" recovery path — so the manager's next restart re-resolves the |
| ... | ... | @@ -309,7 +309,7 @@ export const layer = Layer.effect( |
| 309 | 309 | electronDialog |
| 310 | 310 | .showErrorBox( |
| 311 | 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 | 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 | 104 | httpBaseUrl: config.httpBaseUrl.href, |
| 105 | 105 | credential, |
| 106 | 106 | clientMetadata: { |
| 107 | label: "T3 Code Desktop", | |
| 107 | label: "The Knife Desktop", | |
| 108 | 108 | deviceType: "desktop", |
| 109 | 109 | }, |
| 110 | 110 | }).pipe( |
apps/desktop/src/electron/WindowsForeground.ts+1-1| ... | ... | @@ -216,7 +216,7 @@ export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> { |
| 216 | 216 | export async function activateWindowsForeground(handleBuffer: Buffer): Promise<void> { |
| 217 | 217 | const api = await loadWindowsForegroundApi(); |
| 218 | 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 | } |
| 221 | 221 | |
| 222 | 222 | export async function isWindowsShellHostedForeground(): Promise<boolean> { |
apps/desktop/src/permissions/MacPermissionHelper.test.ts+8-8| ... | ... | @@ -61,7 +61,7 @@ vi.mock("electron", async () => { |
| 61 | 61 | } |
| 62 | 62 | return { |
| 63 | 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 | 66 | nativeImage: { createFromPath: mocks.createFromPath }, |
| 67 | 67 | BrowserWindow: class extends MockWindow {}, |
| ... | ... | @@ -124,11 +124,11 @@ function send(action: string, trusted = true) { |
| 124 | 124 | |
| 125 | 125 | describe("macAppBundlePath", () => { |
| 126 | 126 | it("resolves bundles with spaces and refuses non-bundle executables", () => { |
| 127 | expect(macAppBundlePath("/Applications/T3 Code.app/Contents/MacOS/T3 Code")).toBe( | |
| 128 | "/Applications/T3 Code.app", | |
| 127 | expect(macAppBundlePath("/Applications/The Knife.app/Contents/MacOS/The Knife")).toBe( | |
| 128 | "/Applications/The Knife.app", | |
| 129 | 129 | ); |
| 130 | 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 | }); |
| 134 | 134 | 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 | 138 | send("drag"); |
| 139 | 139 | expect(mocks.createFromPath).toHaveBeenCalledWith("/bundle/prod-resources/icon.png"); |
| 140 | 140 | expect(mocks.startDrag).toHaveBeenCalledWith({ |
| 141 | file: "/Applications/T3 Code (Nightly).app", | |
| 141 | file: "/Applications/The Knife (Nightly).app", | |
| 142 | 142 | icon: mocks.createFromPath.mock.results[0]!.value.resize(), |
| 143 | 143 | }); |
| 144 | 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 | }); |
| 147 | 147 | it("rechecks permissions and releases resources when granted", async () => { |
| 148 | 148 | await open(); |
| ... | ... | @@ -168,7 +168,7 @@ it("does not open for a permission already granted", async () => { |
| 168 | 168 | }); |
| 169 | 169 | it("does not show a helper with a missing packaged icon", async () => { |
| 170 | 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 | 172 | expect(windows).toHaveLength(0); |
| 173 | 173 | }); |
| 174 | 174 | it("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 | 184 | throw new Error("drag failed"); |
| 185 | 185 | }); |
| 186 | 186 | send("drag"); |
| 187 | expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app"); | |
| 187 | expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/The Knife (Nightly).app"); | |
| 188 | 188 | expect(windows[0]!.destroyed).toBe(false); |
| 189 | 189 | }); |
| 190 | 190 |
apps/desktop/src/permissions/MacPermissionHelper.ts+3-3| ... | ... | @@ -60,8 +60,8 @@ button:focus-visible { outline: 2px solid #007aff; outline-offset: -2px; } |
| 60 | 60 | img { width: 32px; height: 32px; pointer-events: none; } |
| 61 | 61 | </style></head><body><main id="panel"> |
| 62 | 62 | <button id="close" aria-label="Close permission helper">×</button> |
| 63 | <header>↑ Drag T3 Code into the list above</header> | |
| 64 | <button id="app" draggable="true" aria-label="Drag T3 Code to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">T3 Code</button> | |
| 63 | <header>↑ Drag The Knife into the list above</header> | |
| 64 | <button id="app" draggable="true" aria-label="Drag The Knife to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">The Knife</button> | |
| 65 | 65 | </main></body></html>`; |
| 66 | 66 | } |
| 67 | 67 | |
| ... | ... | @@ -95,7 +95,7 @@ export class MacPermissionHelper { |
| 95 | 95 | const appIcon = iconPaths |
| 96 | 96 | .map((iconPath) => Electron.nativeImage.createFromPath(iconPath)) |
| 97 | 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 | 99 | const icon = appIcon.resize({ width: 64, height: 64 }); |
| 100 | 100 | const window = new Electron.BrowserWindow({ |
| 101 | 101 | width: 560, |
apps/desktop/src/snapShot/DesktopSnapShot.test.ts+3-3| ... | ... | @@ -1630,7 +1630,7 @@ it.effect( |
| 1630 | 1630 | const warning = logs.find( |
| 1631 | 1631 | (message) => |
| 1632 | 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 | 1635 | assert.strictEqual(Array.isArray(warning) ? warning[1] : undefined, activationFailure); |
| 1636 | 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 | 3161 | const revoked = yield* service.state; |
| 3162 | 3162 | assert.equal( |
| 3163 | 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 | 3166 | assert.deepEqual(revoked.macPermissions, { screenRecording: false, accessibility: true }); |
| 3167 | 3167 | |
| ... | ... | @@ -3174,7 +3174,7 @@ it.effect("flags revoked macOS permissions on read and re-registers once they re |
| 3174 | 3174 | const blocked = yield* service.state; |
| 3175 | 3175 | assert.equal( |
| 3176 | 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 | 3179 | assert.isFalse(blocked.shortcutRegistered); |
| 3180 | 3180 |
apps/desktop/src/snapShot/DesktopSnapShot.ts+8-8| ... | ... | @@ -101,11 +101,11 @@ const FLASH_FRAME_INTERVAL_MS = 16; |
| 101 | 101 | const FLASH_PEAK_OPACITY = 0.08; |
| 102 | 102 | const MAC_SCREEN_CAPTURE_SETTINGS_URL = MAC_PERMISSION_SETTINGS_URLS["screen-recording"]; |
| 103 | 103 | const 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."; | |
| 105 | 105 | const MAC_ACCESSIBILITY_PERMISSION_MESSAGE = |
| 106 | "Allow Accessibility in System Settings, then restart T3 Code."; | |
| 106 | "Allow Accessibility in System Settings, then restart The Knife."; | |
| 107 | 107 | const 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."; | |
| 109 | 109 | const MAC_PERMISSION_MESSAGES = new Set([ |
| 110 | 110 | MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE, |
| 111 | 111 | MAC_ACCESSIBILITY_PERMISSION_MESSAGE, |
| ... | ... | @@ -187,7 +187,7 @@ export class DesktopSnapShot extends Context.Service< |
| 187 | 187 | shortcut: SnapShotShortcut, |
| 188 | 188 | ) => Effect.Effect<DesktopSnapShotShortcutAvailability>; |
| 189 | 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 | 191 | readonly capture: Effect.Effect<void, DesktopSnapShotError>; |
| 192 | 192 | readonly listPending: Effect.Effect< |
| 193 | 193 | ReadonlyArray<DesktopPendingSnapShot>, |
| ... | ... | @@ -898,7 +898,7 @@ export const make = Effect.gen(function* () { |
| 898 | 898 | const capturedAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)); |
| 899 | 899 | if (snapshot.linuxActivationFailure) { |
| 900 | 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 | 902 | snapshot.linuxActivationFailure.cause, |
| 903 | 903 | ); |
| 904 | 904 | } |
| ... | ... | @@ -1018,7 +1018,7 @@ export const make = Effect.gen(function* () { |
| 1018 | 1018 | if (mode === "portal" && niriSocketPath()) { |
| 1019 | 1019 | return { |
| 1020 | 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 | 1024 | if (mode === "portal" && isHyprlandCaptureSession()) { |
| ... | ... | @@ -1168,7 +1168,7 @@ export const make = Effect.gen(function* () { |
| 1168 | 1168 | const { startNiriCaptureShortcut } = await import("./NiriCaptureShortcut.ts"); |
| 1169 | 1169 | return startNiriCaptureShortcut(linuxAppId, onCurrentShortcut, () => { |
| 1170 | 1170 | void runPromise( |
| 1171 | setShortcutFailure("The Niri capture endpoint disconnected. Restart T3 Code."), | |
| 1171 | setShortcutFailure("The Niri capture endpoint disconnected. Restart The Knife."), | |
| 1172 | 1172 | ).catch(() => undefined); |
| 1173 | 1173 | }); |
| 1174 | 1174 | }).pipe( |
| ... | ... | @@ -1190,7 +1190,7 @@ export const make = Effect.gen(function* () { |
| 1190 | 1190 | shortcutActionRegistered: registered, |
| 1191 | 1191 | shortcutMessage: registered |
| 1192 | 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 | 1194 | message: null, |
| 1195 | 1195 | }); |
| 1196 | 1196 | return; |
apps/desktop/src/snapShot/GnomeCaptureSetup.ts+5-5| ... | ... | @@ -56,7 +56,7 @@ export async function installGnomeCaptureBundle({ bundle, dataHome }: SetupPaths |
| 56 | 56 | await NodeFSP.readFile(NodePath.join(target, "metadata.json"), "utf8"), |
| 57 | 57 | ); |
| 58 | 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 | 61 | const staged = await NodeFSP.mkdtemp(NodePath.join(parent, ".t3-capture-install-")); |
| 62 | 62 | let backup: string | undefined; |
| ... | ... | @@ -184,7 +184,7 @@ export class GnomeCaptureSetup { |
| 184 | 184 | return { |
| 185 | 185 | status: "restart-required", |
| 186 | 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 | 189 | if ((installed?.version ?? info.version?.value ?? 0) < bundled.version) |
| 190 | 190 | return { |
| ... | ... | @@ -196,12 +196,12 @@ export class GnomeCaptureSetup { |
| 196 | 196 | return { |
| 197 | 197 | status: "extensions-disabled", |
| 198 | 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 | 201 | if (info.state?.value === 1) |
| 202 | 202 | return { |
| 203 | 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 | 206 | if (info.state?.value === 3 || info.state?.value === 4) |
| 207 | 207 | return { |
| ... | ... | @@ -213,7 +213,7 @@ export class GnomeCaptureSetup { |
| 213 | 213 | return { |
| 214 | 214 | status: "disabled", |
| 215 | 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 | 218 | } catch (error) { |
| 219 | 219 | return { |
apps/desktop/src/snapShot/HyprlandSnapShot.ts+1-1| ... | ... | @@ -96,7 +96,7 @@ export class HyprlandCaptureSetup { |
| 96 | 96 | const bundle = await regularFile(this.paths.bundle); |
| 97 | 97 | if (!bundle) |
| 98 | 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 | 101 | if (!installed.equals(bundle)) |
| 102 | 102 | return { |
apps/desktop/src/snapShot/KdeSnapShot.ts+3-3| ... | ... | @@ -33,7 +33,7 @@ export function kdeCaptureDesktopEntry(executable: string): string { |
| 33 | 33 | return [ |
| 34 | 34 | "[Desktop Entry]", |
| 35 | 35 | "Type=Application", |
| 36 | "Name=T3 Code SnapShots", | |
| 36 | "Name=The Knife SnapShots", | |
| 37 | 37 | "NoDisplay=true", |
| 38 | 38 | `Exec=${escapeDesktopEntryExecArgument(executable)} check`, |
| 39 | 39 | // KService reads this custom property as a KConfig list, not an XDG ';' list. |
| ... | ... | @@ -116,7 +116,7 @@ export class KdeCaptureSetup { |
| 116 | 116 | if (!bundle) |
| 117 | 117 | return { |
| 118 | 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 | 121 | if (!installed.equals(bundle) || entry.toString() !== kdeCaptureDesktopEntry(executable)) |
| 122 | 122 | return { |
| ... | ... | @@ -162,7 +162,7 @@ export class KdeCaptureSetup { |
| 162 | 162 | const bundle = await regularFile(this.paths.bundle); |
| 163 | 163 | if (!bundle) |
| 164 | 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 | 167 | await NodeFSP.mkdir(directory, { recursive: true }); |
| 168 | 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 | 306 | this.bus.requestName(`${appId}.SnapShot`, NameFlag.DO_NOT_QUEUE), |
| 307 | 307 | ); |
| 308 | 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 | 311 | const withFeedback = this.feedbackAvailable && options !== undefined; |
| 312 | 312 | const reply = await this.call({ |
apps/desktop/src/snapShot/NiriCaptureShortcut.ts+1-1| ... | ... | @@ -55,7 +55,7 @@ export async function startNiriCaptureShortcut( |
| 55 | 55 | }), |
| 56 | 56 | ]); |
| 57 | 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 | 59 | return close; |
| 60 | 60 | } catch (error) { |
| 61 | 61 | close(); |
apps/desktop/src/snapShot/NiriSnapShot.ts+1-1| ... | ... | @@ -175,7 +175,7 @@ async function activateNiriWindow(path: string, title: string, signal: AbortSign |
| 175 | 175 | (window) => window.pid === process.pid && window.title === title, |
| 176 | 176 | ); |
| 177 | 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 | 179 | return matches[0]; |
| 180 | 180 | }); |
| 181 | 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 | 318 | expect(client.hasSession).toBe(true); |
| 319 | 319 | expect(client.state.shortcutCanRetry).toBe(false); |
| 320 | 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 | 323 | await expect(client.configure()).rejects.toThrow("Open your desktop's shortcut settings"); |
| 324 | 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 | 135 | throw new Error("Change the capture binding in your Hyprland config, then save it."); |
| 136 | 136 | if (!this.hasSession || this.version < 2) |
| 137 | 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 | 140 | await this.call({ |
| 141 | 141 | destination: this.owner, |
| ... | ... | @@ -164,7 +164,7 @@ export class PortalCaptureShortcut { |
| 164 | 164 | // This failed session is closing, so retry can register a fresh one. |
| 165 | 165 | shortcutCanRetry: !this.managedByHyprland, |
| 166 | 166 | 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 | 168 | : error instanceof Error |
| 169 | 169 | ? error.message |
| 170 | 170 | : "Could not register the capture shortcut.", |
| ... | ... | @@ -304,7 +304,7 @@ export class PortalCaptureShortcut { |
| 304 | 304 | shortcutMessage: |
| 305 | 305 | this.version >= 2 |
| 306 | 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 | 309 | return undefined; |
| 310 | 310 | } |
| ... | ... | @@ -327,7 +327,7 @@ export class PortalCaptureShortcut { |
| 327 | 327 | shortcutPending: false, |
| 328 | 328 | shortcutMessage: shortcut |
| 329 | 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 | 332 | return; |
| 333 | 333 | } |
apps/desktop/src/snapShot/SnapShotTransition.ts+1-1| ... | ... | @@ -114,7 +114,7 @@ function createWindow( |
| 114 | 114 | resizable: false, |
| 115 | 115 | show: false, |
| 116 | 116 | skipTaskbar: true, |
| 117 | title: "T3 Code Snapshot Animation", | |
| 117 | title: "The Knife Snapshot Animation", | |
| 118 | 118 | transparent: true, |
| 119 | 119 | webPreferences: { |
| 120 | 120 | backgroundThrottling: false, |
apps/desktop/src/snapShot/snapShot.ts+1-1| ... | ... | @@ -447,7 +447,7 @@ export function hideAndWaitForBlur(window: { |
| 447 | 447 | return new Promise((resolve, reject) => { |
| 448 | 448 | const timeout = setTimeout(() => { |
| 449 | 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 | 451 | }, WINDOW_BLUR_TIMEOUT_MS); |
| 452 | 452 | const onBlur = () => { |
| 453 | 453 | clearTimeout(timeout); |
apps/desktop/src/ssh/DesktopSshEnvironment.test.ts+1-1| ... | ... | @@ -30,7 +30,7 @@ describe("sshEnvironment", () => { |
| 30 | 30 | assert.equal(cause.message, "Failed to present SSH password prompt for devbox."); |
| 31 | 31 | assert.equal( |
| 32 | 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 | }); |
| 36 | 36 |
apps/desktop/src/ssh/DesktopSshEnvironment.ts+1-1| ... | ... | @@ -97,7 +97,7 @@ export function toSshPasswordPromptError( |
| 97 | 97 | break; |
| 98 | 98 | case "DesktopSshPromptWindowUnavailableError": |
| 99 | 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 | 101 | break; |
| 102 | 102 | case "DesktopSshPromptTimedOutError": |
| 103 | 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 | 63 | ) { |
| 64 | 64 | override get message(): string { |
| 65 | 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 | } |
| 69 | 69 |
apps/desktop/src/window/DesktopApplicationMenu.test.ts+5-5| ... | ... | @@ -29,7 +29,7 @@ const environmentInput = { |
| 29 | 29 | |
| 30 | 30 | const layerElectronApp = Layer.succeed(ElectronApp.ElectronApp, { |
| 31 | 31 | metadata: Effect.die("unexpected metadata read"), |
| 32 | name: Effect.succeed("T3 Code"), | |
| 32 | name: Effect.succeed("The Knife"), | |
| 33 | 33 | systemLocale: Effect.succeed("en-US"), |
| 34 | 34 | whenReady: Effect.void, |
| 35 | 35 | quit: Effect.void, |
| ... | ... | @@ -142,21 +142,21 @@ describe("DesktopApplicationMenu", () => { |
| 142 | 142 | const template = yield* Deferred.await(applicationMenuTemplate); |
| 143 | 143 | const applicationMenu = template[0]; |
| 144 | 144 | assert.isDefined(applicationMenu); |
| 145 | assert.equal(applicationMenu.label, "T3 Code (Nightly)"); | |
| 145 | assert.equal(applicationMenu.label, "The Knife (Nightly)"); | |
| 146 | 146 | if (!Array.isArray(applicationMenu.submenu)) { |
| 147 | 147 | throw new Error("Expected application menu submenu to be an array."); |
| 148 | 148 | } |
| 149 | 149 | assert.equal( |
| 150 | 150 | applicationMenu.submenu.find((item) => item.role === "about")?.label, |
| 151 | "About T3 Code (Nightly)", | |
| 151 | "About The Knife (Nightly)", | |
| 152 | 152 | ); |
| 153 | 153 | assert.equal( |
| 154 | 154 | applicationMenu.submenu.find((item) => item.role === "hide")?.label, |
| 155 | "Hide T3 Code (Nightly)", | |
| 155 | "Hide The Knife (Nightly)", | |
| 156 | 156 | ); |
| 157 | 157 | assert.equal( |
| 158 | 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 | 74 | yield* electronDialog.showMessageBox({ |
| 75 | 75 | type: "info", |
| 76 | 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 | 78 | buttons: ["OK"], |
| 79 | 79 | }); |
| 80 | 80 | } else if (updateState.status === "error") { |
apps/desktop/src/wsl/DesktopWslBackend.test.ts+1-1| ... | ... | @@ -172,7 +172,7 @@ describe("DesktopWslBackend", () => { |
| 172 | 172 | |
| 173 | 173 | yield* recordOwnershipFailure(); |
| 174 | 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 | 176 | yield* backend.reconcile; |
| 177 | 177 | assert.equal(startCount, 1); |
| 178 | 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 | 177 | Ref.set( |
| 178 | 178 | preflightErrorRef, |
| 179 | 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 | 505 | |
| 506 | 506 | const formatNodePtyProbeFailureReason = (exitCode: number): string | null => |
| 507 | 507 | 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 | 509 | : null; |
| 510 | 510 | |
| 511 | 511 | // Captures the login-shell PATH as `resolvedPath:` so the launch can forward the |
apps/mobile/README.md+5-5| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | # T3 Code Mobile | |
| 1 | # The Knife Mobile | |
| 2 | 2 | |
| 3 | 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. | |
| 5 | 5 | |
| 6 | 6 | ## Quickstart |
| 7 | 7 | |
| ... | ... | @@ -10,9 +10,9 @@ |
| 10 | 10 | |
| 11 | 11 | This app has three variants: |
| 12 | 12 | |
| 13 | - `development`: Expo dev client, installable side-by-side as `T3 Code Dev` | |
| 14 | - `preview`: persistent internal preview build, installable side-by-side as `T3 Code Preview` | |
| 15 | - `production`: store/release build as `T3 Code` | |
| 13 | - `development`: Expo dev client, installable side-by-side as `The Knife Dev` | |
| 14 | - `preview`: persistent internal preview build, installable side-by-side as `The Knife Preview` | |
| 15 | - `production`: store/release build as `The Knife` | |
| 16 | 16 | |
| 17 | 17 | Run commands from `apps/mobile`. |
| 18 | 18 |
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 | 19 | |
| 20 | 20 | const fromRepoRoot = (relativePath: string) => `../../${relativePath}`; |
| 21 | 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. | |
| 23 | const androidAdaptiveForeground = "./assets/android-icon-foreground.png"; | |
| 22 | // The knife sits inside the adaptive icon safe zone. | |
| 23 | const androidAdaptiveForeground = "../../assets/knife/android-foreground.png"; | |
| 24 | 24 | |
| 25 | 25 | if ( |
| 26 | 26 | isIosPersonalTeamBuild && |
| ... | ... | @@ -37,11 +37,11 @@ const DEVELOPMENT_ASSETS = { |
| 37 | 37 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject), |
| 38 | 38 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng), |
| 39 | 39 | androidAdaptiveForeground, |
| 40 | androidAdaptiveBackgroundColor: "#347FF8", | |
| 41 | androidAdaptiveBackgroundImage: "./assets/android-icon-background-dev.png", | |
| 42 | androidSplashIcon: "./assets/android-splash-icon-dev.png", | |
| 43 | androidMonochromeIcon: "./assets/android-icon-mark.png", | |
| 44 | androidNotificationIcon: "./assets/android-notification-icon.png", | |
| 40 | androidAdaptiveBackgroundColor: "#171422", | |
| 41 | androidAdaptiveBackgroundImage: undefined, | |
| 42 | androidSplashIcon: "../../assets/knife/android-splash.png", | |
| 43 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", | |
| 44 | androidNotificationIcon: "../../assets/knife/android-foreground.png", | |
| 45 | 45 | androidNotificationColor: "#00639B", |
| 46 | 46 | } as const; |
| 47 | 47 | |
| ... | ... | @@ -50,11 +50,11 @@ const PREVIEW_ASSETS = { |
| 50 | 50 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject), |
| 51 | 51 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng), |
| 52 | 52 | androidAdaptiveForeground, |
| 53 | androidAdaptiveBackgroundColor: "#111533", | |
| 54 | androidAdaptiveBackgroundImage: "./assets/android-icon-background-nightly.png", | |
| 55 | androidSplashIcon: "./assets/android-splash-icon-nightly.png", | |
| 56 | androidMonochromeIcon: "./assets/android-icon-mark.png", | |
| 57 | androidNotificationIcon: "./assets/android-notification-icon.png", | |
| 53 | androidAdaptiveBackgroundColor: "#171422", | |
| 54 | androidAdaptiveBackgroundImage: undefined, | |
| 55 | androidSplashIcon: "../../assets/knife/android-splash.png", | |
| 56 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", | |
| 57 | androidNotificationIcon: "../../assets/knife/android-foreground.png", | |
| 58 | 58 | androidNotificationColor: "#7565C7", |
| 59 | 59 | } as const; |
| 60 | 60 | |
| ... | ... | @@ -63,17 +63,17 @@ const RELEASE_ASSETS = { |
| 63 | 63 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject), |
| 64 | 64 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng), |
| 65 | 65 | androidAdaptiveForeground, |
| 66 | androidAdaptiveBackgroundColor: "#000000", | |
| 66 | androidAdaptiveBackgroundColor: "#171422", | |
| 67 | 67 | androidAdaptiveBackgroundImage: undefined, |
| 68 | androidSplashIcon: "./assets/android-splash-icon-prod.png", | |
| 69 | androidMonochromeIcon: "./assets/android-icon-mark.png", | |
| 70 | androidNotificationIcon: "./assets/android-notification-icon.png", | |
| 68 | androidSplashIcon: "../../assets/knife/android-splash.png", | |
| 69 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", | |
| 70 | androidNotificationIcon: "../../assets/knife/android-foreground.png", | |
| 71 | 71 | androidNotificationColor: "#FFFFFF", |
| 72 | 72 | } as const; |
| 73 | 73 | |
| 74 | 74 | const VARIANT_CONFIG = { |
| 75 | 75 | development: { |
| 76 | appName: "T3 Code Dev", | |
| 76 | appName: "The Knife Dev", | |
| 77 | 77 | scheme: "t3code-dev", |
| 78 | 78 | iosBundleIdentifier: "com.t3tools.t3code.dev", |
| 79 | 79 | androidPackage: "com.t3tools.t3code.dev", |
| ... | ... | @@ -81,7 +81,7 @@ const VARIANT_CONFIG = { |
| 81 | 81 | assets: DEVELOPMENT_ASSETS, |
| 82 | 82 | }, |
| 83 | 83 | preview: { |
| 84 | appName: "T3 Code Preview", | |
| 84 | appName: "The Knife Preview", | |
| 85 | 85 | scheme: "t3code-preview", |
| 86 | 86 | iosBundleIdentifier: "com.t3tools.t3code.preview", |
| 87 | 87 | androidPackage: "com.t3tools.t3code.preview", |
| ... | ... | @@ -89,7 +89,7 @@ const VARIANT_CONFIG = { |
| 89 | 89 | assets: PREVIEW_ASSETS, |
| 90 | 90 | }, |
| 91 | 91 | production: { |
| 92 | appName: "T3 Code", | |
| 92 | appName: "The Knife", | |
| 93 | 93 | scheme: "t3code", |
| 94 | 94 | iosBundleIdentifier: "com.t3tools.t3code", |
| 95 | 95 | androidPackage: "com.t3tools.t3code", |
| ... | ... | @@ -134,7 +134,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [ |
| 134 | 134 | { |
| 135 | 135 | name: "SubscriptionUsage", |
| 136 | 136 | displayName: "Subscription usage", |
| 137 | description: "Subscription quotas from your connected T3 Code environments.", | |
| 137 | description: "Subscription quotas from your connected environments.", | |
| 138 | 138 | ios: { |
| 139 | 139 | configuration: { |
| 140 | 140 | title: "Subscription usage", |
| ... | ... | @@ -185,7 +185,7 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [ |
| 185 | 185 | { |
| 186 | 186 | name: "AgentActivity", |
| 187 | 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 | 189 | // Live Activity companion; there is no Android presentation for it. |
| 190 | 190 | android: null, |
| 191 | 191 | ios: { supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"] }, |
| ... | ... | @@ -261,7 +261,7 @@ const config: ExpoConfig = { |
| 261 | 261 | `webcredentials:${variant.relyingParty}`, |
| 262 | 262 | ], |
| 263 | 263 | entitlements: { |
| 264 | "keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`], | |
| 264 | "keychain-access-groups": [`$(AppIdentifierPrefix)${iosBundleIdentifier}`], | |
| 265 | 265 | }, |
| 266 | 266 | infoPlist: { |
| 267 | 267 | UISupportedInterfaceOrientations: [ |
| ... | ... | @@ -273,8 +273,8 @@ const config: ExpoConfig = { |
| 273 | 273 | NSAllowsArbitraryLoads: true, |
| 274 | 274 | }, |
| 275 | 275 | NSLocalNetworkUsageDescription: |
| 276 | "Allow T3 Code to connect to T3 Code servers on your local network or tailnet.", | |
| 277 | NSPhotoLibraryAddUsageDescription: "Allow T3 Code to save images to your photo library.", | |
| 276 | "Allow The Knife to connect to The Knife servers on your local network or tailnet.", | |
| 277 | NSPhotoLibraryAddUsageDescription: "Allow The Knife to save images to your photo library.", | |
| 278 | 278 | // "Audio, AirPlay, and Picture in Picture": the browser screen's system |
| 279 | 279 | // picture in picture needs it to start and to stay up outside the app. |
| 280 | 280 | UIBackgroundModes: ["audio"], |
| ... | ... | @@ -321,6 +321,7 @@ const config: ExpoConfig = { |
| 321 | 321 | favicon: variant.assets.appIcon, |
| 322 | 322 | }, |
| 323 | 323 | plugins: [ |
| 324 | ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []), | |
| 324 | 325 | "expo-asset", |
| 325 | 326 | [ |
| 326 | 327 | "expo-font", |
| ... | ... | @@ -359,8 +360,6 @@ const config: ExpoConfig = { |
| 359 | 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 | 363 | ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }], |
| 365 | 364 | "expo-web-browser", |
| 366 | 365 | [ |
| ... | ... | @@ -382,7 +381,7 @@ const config: ExpoConfig = { |
| 382 | 381 | [ |
| 383 | 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 | 385 | recordAudioAndroid: false, |
| 387 | 386 | enableBackgroundPlayback: false, |
| 388 | 387 | enableBackgroundRecording: false, |
| ... | ... | @@ -391,7 +390,7 @@ const config: ExpoConfig = { |
| 391 | 390 | [ |
| 392 | 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 | 394 | microphonePermission: false, |
| 396 | 395 | barcodeScannerEnabled: true, |
| 397 | 396 | recordAudioAndroid: false, |
| ... | ... | @@ -466,7 +465,6 @@ const config: ExpoConfig = { |
| 466 | 465 | "./plugins/withAndroidModernAlertDialog.cjs", |
| 467 | 466 | "./plugins/withAndroidPredictiveBackCompat.cjs", |
| 468 | 467 | "./plugins/withAndroidTabletOrientation.cjs", |
| 469 | ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []), | |
| 470 | 468 | ], |
| 471 | 469 | extra: { |
| 472 | 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 | 18 | } |
| 19 | 19 | val records = payload?.optJSONArray("records") |
| 20 | 20 | if (payload == null || records == null) { |
| 21 | clipboard.setPrimaryClip(ClipData.newPlainText("T3 Code", text)) | |
| 21 | clipboard.setPrimaryClip(ClipData.newPlainText("The Knife", text)) | |
| 22 | 22 | return |
| 23 | 23 | } |
| 24 | 24 | val all = (0 until records.length()).map { records.getJSONObject(it) } |
| ... | ... | @@ -36,12 +36,12 @@ internal object T3ComposerClipboard { |
| 36 | 36 | clipboard.setPrimaryClip( |
| 37 | 37 | if (selected.isEmpty()) { |
| 38 | 38 | ClipData.newPlainText( |
| 39 | "T3 Code", | |
| 39 | "The Knife", | |
| 40 | 40 | text |
| 41 | 41 | ) |
| 42 | 42 | } else { |
| 43 | 43 | ClipData.newHtmlText( |
| 44 | "T3 Code", | |
| 44 | "The Knife", | |
| 45 | 45 | text, |
| 46 | 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 | 14 | registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { (view: T3LayoutMetricsView, _: UITraitCollection) in |
| 15 | 15 | view.publishMetrics() |
| 16 | 16 | } |
| 17 | #if compiler(>=6.4) | |
| 17 | #if T3_IOS_27_1_SDK | |
| 18 | 18 | if #available(iOS 27.1, *) { |
| 19 | 19 | hasHinge = nil |
| 20 | 20 | addInteraction(UIHingeInteraction { [weak self] _, update in |
| ... | ... | @@ -67,7 +67,7 @@ final class T3LayoutMetricsView: ExpoView { |
| 67 | 67 | } |
| 68 | 68 | var verticalBarEdge = "none" |
| 69 | 69 | var regions: [[String: Any]] = [] |
| 70 | #if compiler(>=6.4) | |
| 70 | #if T3_IOS_27_1_SDK | |
| 71 | 71 | if #available(iOS 27.1, *) { |
| 72 | 72 | let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft |
| 73 | 73 | switch traitCollection.verticalBarEdge { |
apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec+6| ... | ... | @@ -15,5 +15,11 @@ Pod::Spec.new do |s| |
| 15 | 15 | s.pod_target_xcconfig = { |
| 16 | 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 | 24 | s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}' |
| 19 | 25 | end |
apps/mobile/plugins/withWidgetLogoAsset.cjs+10-8| ... | ... | @@ -1,9 +1,9 @@ |
| 1 | 1 | "use strict"; |
| 2 | 2 | |
| 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 | 5 | // expo-widgets generates ExpoWidgetsTarget without a Resources build phase and |
| 6 | // has no asset support, so this plugin (a) writes an SVG template image set into | |
| 6 | // has no asset support, so this plugin (a) writes a template image set into | |
| 7 | 7 | // the generated widget asset catalog and (b) wires that catalog into the widget |
| 8 | 8 | // target with an actool build phase. |
| 9 | 9 | // |
| ... | ... | @@ -22,17 +22,16 @@ const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs"); |
| 22 | 22 | |
| 23 | 23 | const TARGET_NAME = "ExpoWidgetsTarget"; |
| 24 | 24 | const CATALOG_NAME = "Assets.xcassets"; |
| 25 | const IMAGE_SET = "T3Mark.imageset"; | |
| 26 | const SVG_NAME = "T3Mark.svg"; | |
| 25 | const IMAGE_SET = "KnifeMark.imageset"; | |
| 26 | const IMAGE_NAME = "knife.png"; | |
| 27 | 27 | |
| 28 | 28 | const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n"; |
| 29 | 29 | const IMAGE_SET_CONTENTS = |
| 30 | 30 | JSON.stringify( |
| 31 | 31 | { |
| 32 | images: [{ idiom: "universal", filename: SVG_NAME }], | |
| 32 | images: [{ idiom: "universal", filename: IMAGE_NAME }], | |
| 33 | 33 | info: { author: "expo", version: 1 }, |
| 34 | 34 | properties: { |
| 35 | "preserves-vector-representation": true, | |
| 36 | 35 | "template-rendering-intent": "template", |
| 37 | 36 | }, |
| 38 | 37 | }, |
| ... | ... | @@ -44,13 +43,16 @@ function withAssetFiles(config) { |
| 44 | 43 | return withDangerousMod(config, [ |
| 45 | 44 | "ios", |
| 46 | 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 | 50 | const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME); |
| 49 | 51 | const imageSetDir = path.join(catalogDir, IMAGE_SET); |
| 50 | 52 | fs.mkdirSync(imageSetDir, { recursive: true }); |
| 51 | 53 | fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS); |
| 52 | 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 | 56 | return cfg; |
| 55 | 57 | }, |
| 56 | 58 | ]); |
apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs+1| ... | ... | @@ -4,6 +4,7 @@ module.exports = function withoutIosPersonalTeamCapabilities(config) { |
| 4 | 4 | return withEntitlementsPlist(config, (modConfig) => { |
| 5 | 5 | delete modConfig.modResults["aps-environment"]; |
| 6 | 6 | delete modConfig.modResults["com.apple.developer.applesignin"]; |
| 7 | delete modConfig.modResults["com.apple.developer.associated-domains"]; | |
| 7 | 8 | delete modConfig.modResults["com.apple.security.application-groups"]; |
| 8 | 9 | return modConfig; |
| 9 | 10 | }); |
apps/mobile/src/Stack.tsx+1-1| ... | ... | @@ -280,7 +280,7 @@ const SettingsContentStack = createV5SheetStackNavigator({ |
| 280 | 280 | SettingsAbout: createNativeStackScreen({ |
| 281 | 281 | screen: SettingsAboutRouteScreen, |
| 282 | 282 | linking: "about", |
| 283 | options: { title: "About T3 Code" }, | |
| 283 | options: { title: "About The Knife" }, | |
| 284 | 284 | }), |
| 285 | 285 | SettingsEnvironmentNew: createNativeStackScreen({ |
| 286 | 286 | screen: ConnectionsNewRouteScreen, |
apps/mobile/src/components/BrandMark.tsx+3-4| ... | ... | @@ -3,7 +3,7 @@ import { Image } from "expo-image"; |
| 3 | 3 | import { View } from "react-native"; |
| 4 | 4 | |
| 5 | 5 | import { AppText as Text } from "./AppText"; |
| 6 | import { T3_CODE_BRAND_MARK_SOURCE } from "./brandAssets"; | |
| 6 | import { KNIFE_BRAND_MARK_SOURCE } from "./brandAssets"; | |
| 7 | 7 | |
| 8 | 8 | const appVariant = Constants.expoConfig?.extra?.appVariant; |
| 9 | 9 | const DEFAULT_STAGE_LABEL = |
| ... | ... | @@ -17,17 +17,16 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab |
| 17 | 17 | return ( |
| 18 | 18 | <View className="flex-row items-center gap-3"> |
| 19 | 19 | <Image |
| 20 | source={T3_CODE_BRAND_MARK_SOURCE} | |
| 20 | source={KNIFE_BRAND_MARK_SOURCE} | |
| 21 | 21 | accessibilityIgnoresInvertColors |
| 22 | 22 | style={{ |
| 23 | 23 | width: iconSize, |
| 24 | 24 | height: iconSize, |
| 25 | borderRadius: compact ? 10 : 14, | |
| 26 | 25 | }} |
| 27 | 26 | /> |
| 28 | 27 | <View className="gap-1"> |
| 29 | 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 | 30 | <View className="rounded-full bg-subtle px-2 py-1"> |
| 32 | 31 | <Text className="text-3xs font-t3-bold tracking-[1.1px] uppercase text-foreground-muted"> |
| 33 | 32 | {stageLabel} |
apps/mobile/src/components/CompactBrandTitle.tsx+4-4| ... | ... | @@ -4,7 +4,7 @@ import type { NativeStackNavigationOptions } from "@react-navigation/native-stac |
| 4 | 4 | import { Platform, View } from "react-native"; |
| 5 | 5 | |
| 6 | 6 | import { AppText as Text } from "./AppText"; |
| 7 | import { T3Wordmark } from "./T3Wordmark"; | |
| 7 | import { KnifeMark } from "./KnifeMark"; | |
| 8 | 8 | import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; |
| 9 | 9 | import { resolveMobileStageLabel } from "../lib/mobileBranding"; |
| 10 | 10 | import { useAndroidControlSizing } from "./useAndroidControlSizing"; |
| ... | ... | @@ -34,19 +34,19 @@ export function CompactBrandTitle( |
| 34 | 34 | return ( |
| 35 | 35 | <View |
| 36 | 36 | aria-level={1} |
| 37 | accessibilityLabel={props.accessibilityLabel ?? "T3 Code, Threads"} | |
| 37 | accessibilityLabel={props.accessibilityLabel ?? "The Knife, Threads"} | |
| 38 | 38 | accessible |
| 39 | 39 | role="heading" |
| 40 | 40 | className="flex-row items-center gap-1.5" |
| 41 | 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 | 44 | <Text |
| 45 | 45 | allowFontScaling={props.allowFontScaling} |
| 46 | 46 | className="font-t3-medium text-foreground-muted" |
| 47 | 47 | style={{ fontSize: 21 * scale, letterSpacing: -0.5 * scale }} |
| 48 | 48 | > |
| 49 | Code | |
| 49 | The Knife | |
| 50 | 50 | </Text> |
| 51 | 51 | <View |
| 52 | 52 | className="rounded-full bg-subtle px-1.5 py-0.5" |
apps/mobile/src/components/KnifeMark.tsx created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | import { Image } from "expo-image"; | |
| 2 | import { KNIFE_BRAND_MARK_SOURCE } from "./brandAssets"; | |
| 3 | ||
| 4 | export 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 @@ |
| 1 | import type { ColorValue } from "react-native"; | |
| 2 | import Svg, { Path } from "react-native-svg"; | |
| 3 | import { withUniwind } from "uniwind"; | |
| 4 | ||
| 5 | const 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 | */ | |
| 11 | export 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 | import Constants from "expo-constants"; | |
| 2 | ||
| 3 | const appVariant = Constants.expoConfig?.extra?.appVariant; | |
| 4 | ||
| 5 | export 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"); | |
| 1 | export const KNIFE_BRAND_MARK_SOURCE = require("../../../../assets/knife/apple-touch-180.png"); |
apps/mobile/src/features/agent-awareness/remoteRegistration.ts+1-1| ... | ... | @@ -534,7 +534,7 @@ function armAgentAwarenessLiveActivityForLocalWorkNow(input: { |
| 534 | 534 | const nowIso = new Date(Date.now()).toISOString(); |
| 535 | 535 | const activity = startAgentLiveActivity( |
| 536 | 536 | { |
| 537 | title: "T3 Code", | |
| 537 | title: "The Knife", | |
| 538 | 538 | subtitle: "Agent work in progress", |
| 539 | 539 | activeCount: 1, |
| 540 | 540 | updatedAt: nowIso, |
apps/mobile/src/features/diagnostics/crash-log-model.test.ts+2-2| ... | ... | @@ -61,14 +61,14 @@ describe("formatStartupCrashReport", () => { |
| 61 | 61 | version: "1.1.1", |
| 62 | 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 | 65 | expect(report).toContain("2026-09-13T05:35:52.000Z"); |
| 66 | 66 | expect(report).toContain("at NewTaskFlowProvider"); |
| 67 | 67 | }); |
| 68 | 68 | |
| 69 | 69 | it("says so when nothing was recorded", () => { |
| 70 | 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 | 72 | records: ReadonlyArray<StartupCrashRecord>, |
| 73 | 73 | app: { readonly version: string; readonly build: string }, |
| 74 | 74 | ): string { |
| 75 | const header = `T3 Code ${app.version} (${app.build})`; | |
| 75 | const header = `The Knife ${app.version} (${app.build})`; | |
| 76 | 76 | if (records.length === 0) return `${header}\nNo startup crashes recorded.`; |
| 77 | 77 | return [ |
| 78 | 78 | header, |
apps/mobile/src/features/pull-requests/MaterialPullRequestToolbar.android.tsx+1-1| ... | ... | @@ -69,7 +69,7 @@ export function MaterialPullRequestToolbar(props: { readonly onRequestVisibility |
| 69 | 69 | brand={ |
| 70 | 70 | <CompactBrandTitle |
| 71 | 71 | allowFontScaling={false} |
| 72 | accessibilityLabel="T3 Code, Pull requests" | |
| 72 | accessibilityLabel="The Knife, Pull requests" | |
| 73 | 73 | /> |
| 74 | 74 | } |
| 75 | 75 | onPress={() => |
apps/mobile/src/features/pull-requests/pullRequestAgentTasks.ts+1-1| ... | ... | @@ -23,7 +23,7 @@ export function pullRequestAgentPrompt(task: PullRequestAgentTask): string { |
| 23 | 23 | case "conflicts": |
| 24 | 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 | 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 | } |
| 29 | 29 |
apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx+1-1| ... | ... | @@ -20,7 +20,7 @@ export function SettingsAboutRouteScreen() { |
| 20 | 20 | const insets = useSafeAreaInsets(); |
| 21 | 21 | |
| 22 | 22 | return ( |
| 23 | <SettingsScreen title="About T3 Code"> | |
| 23 | <SettingsScreen title="About The Knife"> | |
| 24 | 24 | <ScrollView |
| 25 | 25 | contentInsetAdjustmentBehavior="automatic" |
| 26 | 26 | showsVerticalScrollIndicator={false} |
apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx+3-3| ... | ... | @@ -113,7 +113,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm |
| 113 | 113 | return; |
| 114 | 114 | Alert.alert( |
| 115 | 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 | 118 | { text: "Cancel", style: "cancel" }, |
| 119 | 119 | { |
| ... | ... | @@ -227,7 +227,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm |
| 227 | 227 | {notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null} |
| 228 | 228 | {config ? ( |
| 229 | 229 | <> |
| 230 | <SettingsSection title="T3 Code"> | |
| 230 | <SettingsSection title="The Knife"> | |
| 231 | 231 | <View className="gap-1 p-4"> |
| 232 | 232 | <Text className="text-base text-foreground">Version {version}</Text> |
| 233 | 233 | {running ? ( |
| ... | ... | @@ -252,7 +252,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm |
| 252 | 252 | <Text className="text-sm text-foreground-muted"> |
| 253 | 253 | {capabilities?.serverSelfUpdate === "desktop-managed" |
| 254 | 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 | 256 | </Text> |
| 257 | 257 | ) : null} |
| 258 | 258 | </View> |
apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx+1-1| ... | ... | @@ -166,7 +166,7 @@ export function SettingsMicrophoneRouteScreen() { |
| 166 | 166 | <Text className="px-2 text-sm text-foreground-muted"> |
| 167 | 167 | {permission === "ask" |
| 168 | 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 | 170 | </Text> |
| 171 | 171 | </> |
| 172 | 172 | ) : null} |
apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx+2-2| ... | ... | @@ -338,7 +338,7 @@ function ConfiguredSettingsNotificationsRouteScreen() { |
| 338 | 338 | |
| 339 | 339 | Alert.alert( |
| 340 | 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 | 343 | { text: "Cancel", style: "cancel" }, |
| 344 | 344 | { text: "Open Settings", onPress: () => void Linking.openSettings() }, |
| ... | ... | @@ -492,7 +492,7 @@ function ConfiguredSettingsNotificationsRouteScreen() { |
| 492 | 492 | void openAndroidLiveUpdateSettings().catch(() => { |
| 493 | 493 | Alert.alert( |
| 494 | 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 | 215 | |
| 216 | 216 | <SettingsSection title="App"> |
| 217 | 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 | 219 | </SettingsSection> |
| 220 | 220 | </> |
| 221 | 221 | ); |
apps/mobile/src/features/showcase/showcaseAgentActivity.ts+1-1| ... | ... | @@ -74,7 +74,7 @@ export function buildShowcaseAgentActivity( |
| 74 | 74 | }); |
| 75 | 75 | } |
| 76 | 76 | return { |
| 77 | title: "T3 Code", | |
| 77 | title: "The Knife", | |
| 78 | 78 | subtitle: "Agent work in progress", |
| 79 | 79 | activeCount: rows.filter((row) => ACTIVE_PHASES.has(row.phase)).length, |
| 80 | 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 | 636 | armAgentAwarenessLiveActivityForLocalWork({ |
| 637 | 637 | environmentId: props.environmentId, |
| 638 | 638 | threadTitle: props.selectedThread.title, |
| 639 | projectTitle: props.environmentLabel ?? "T3 Code", | |
| 639 | projectTitle: props.environmentLabel ?? "The Knife", | |
| 640 | 640 | }); |
| 641 | 641 | } finally { |
| 642 | 642 | inFlightThreadIdsRef.current.delete(threadKey); |
apps/mobile/src/features/threads/ThreadFeed.tsx+1-1| ... | ... | @@ -1200,7 +1200,7 @@ function renderFeedEntry( |
| 1200 | 1200 | </Text> |
| 1201 | 1201 | ) : presentation.attribution === "t3code" ? ( |
| 1202 | 1202 | <Text className="mb-1 pr-1 font-t3-medium text-2xs text-foreground-muted opacity-60"> |
| 1203 | Sent by T3 Code | |
| 1203 | Sent by The Knife | |
| 1204 | 1204 | </Text> |
| 1205 | 1205 | ) : presentation.attribution === "agent" ? ( |
| 1206 | 1206 | <AgentMessageAttribution |
apps/mobile/src/features/threads/thread-work-log.tsx+2-2| ... | ... | @@ -57,7 +57,7 @@ import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity"; |
| 57 | 57 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; |
| 58 | 58 | |
| 59 | 59 | import { AppText as Text } from "../../components/AppText"; |
| 60 | import { T3Wordmark } from "../../components/T3Wordmark"; | |
| 60 | import { KnifeMark } from "../../components/KnifeMark"; | |
| 61 | 61 | import { BrowserAppIcon } from "../../components/BrowserAppIcon"; |
| 62 | 62 | import { cn } from "../../lib/cn"; |
| 63 | 63 | import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout"; |
| ... | ... | @@ -131,7 +131,7 @@ function WorkLogIcon(props: { |
| 131 | 131 | const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName; |
| 132 | 132 | if (props.icon === "t3-code") { |
| 133 | 133 | return ( |
| 134 | <T3Wordmark height={10} {...(colorClassName ? { colorClassName } : { color: props.color })} /> | |
| 134 | <KnifeMark height={14} /> | |
| 135 | 135 | ); |
| 136 | 136 | } |
| 137 | 137 | if (props.icon === "computer") { |
apps/mobile/src/features/workspace/WorkspaceRootScreen.tsx+2-2| ... | ... | @@ -42,7 +42,7 @@ function WorkspaceRootTitle(props: { |
| 42 | 42 | <View style={{ width: props.maxWidth, minHeight: 32, justifyContent: "center" }}> |
| 43 | 43 | <WorkspaceConnectionTitle |
| 44 | 44 | brand={ |
| 45 | <CompactBrandTitle accessibilityLabel={`T3 Code, ${TAB_TITLES[state.selectedTab]}`} /> | |
| 45 | <CompactBrandTitle accessibilityLabel={`The Knife, ${TAB_TITLES[state.selectedTab]}`} /> | |
| 46 | 46 | } |
| 47 | 47 | maxWidth={props.maxWidth} |
| 48 | 48 | onPress={props.onOpenEnvironments} |
| ... | ... | @@ -114,7 +114,7 @@ function WorkspaceRootContent() { |
| 114 | 114 | /> |
| 115 | 115 | ) : ( |
| 116 | 116 | <AndroidScreenHeader |
| 117 | title="T3 Code" | |
| 117 | title="The Knife" | |
| 118 | 118 | titleContent={renderHeaderTitle()} |
| 119 | 119 | actions={[ |
| 120 | 120 | { |
apps/mobile/src/lib/authClientMetadata.ts+1-1| ... | ... | @@ -7,7 +7,7 @@ export function authClientMetadata(appVersion?: string): AuthClientPresentationM |
| 7 | 7 | const deviceModel = Device.modelName?.trim(); |
| 8 | 8 | |
| 9 | 9 | return { |
| 10 | label: "T3 Code Mobile", | |
| 10 | label: "The Knife Mobile", | |
| 11 | 11 | deviceType: |
| 12 | 12 | Device.deviceType === Device.DeviceType.TABLET |
| 13 | 13 | ? "tablet" |
apps/mobile/src/lib/connection.test.ts+1-1| ... | ... | @@ -40,7 +40,7 @@ describe("mobile remote connection records", () => { |
| 40 | 40 | |
| 41 | 41 | it("identifies mobile token exchanges for authorized-client presentation", () => { |
| 42 | 42 | expect(authClientMetadata()).toEqual({ |
| 43 | label: "T3 Code Mobile", | |
| 43 | label: "The Knife Mobile", | |
| 44 | 44 | deviceType: "mobile", |
| 45 | 45 | os: "iOS", |
| 46 | 46 | osMajorVersion: 18, |
apps/mobile/src/lib/mobileTheme.ts+1-1| ... | ... | @@ -26,7 +26,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ |
| 26 | 26 | readonly id: MobileThemeId; |
| 27 | 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 | 30 | { id: "material-you", label: "Material You" }, |
| 31 | 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 | 244 | </HStack> |
| 245 | 245 | ); |
| 246 | 246 | |
| 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 | 247 | const renderLogo = (height: number, color: Foreground) => ( |
| 254 | <HStack modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(color)]}> | |
| 255 | <Image assetName="T3Mark" modifiers={[resizable()]} /> | |
| 248 | <HStack modifiers={[frame({ width: height, height }), foregroundStyle(color)]}> | |
| 249 | <Image assetName="KnifeMark" modifiers={[resizable()]} /> | |
| 256 | 250 | </HStack> |
| 257 | 251 | ); |
| 258 | 252 |
apps/mobile/src/widgets/SubscriptionUsage.android.tsx+17-5| ... | ... | @@ -29,8 +29,20 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi |
| 29 | 29 | }); |
| 30 | 30 | const muted = colors.onSurfaceVariant; |
| 31 | 31 | const providers = props.providers ?? [ |
| 32 | { name: "Codex", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 }, | |
| 33 | { name: "Claude", detail: "Open T3 to connect", windows: [], expiresAt: 0, totalWindows: 0 }, | |
| 32 | { | |
| 33 | name: "Codex", | |
| 34 | detail: "Open The Knife to connect", | |
| 35 | windows: [], | |
| 36 | expiresAt: 0, | |
| 37 | totalWindows: 0, | |
| 38 | }, | |
| 39 | { | |
| 40 | name: "Claude", | |
| 41 | detail: "Open The Knife to connect", | |
| 42 | windows: [], | |
| 43 | expiresAt: 0, | |
| 44 | totalWindows: 0, | |
| 45 | }, | |
| 34 | 46 | ]; |
| 35 | 47 | return ( |
| 36 | 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 | 72 | </Text> |
| 61 | 73 | {shown.length === 0 ? ( |
| 62 | 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 | 76 | </Text> |
| 65 | 77 | ) : null} |
| 66 | 78 | {shown.map((window) => { |
| ... | ... | @@ -90,7 +102,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi |
| 90 | 102 | })} |
| 91 | 103 | {hidden > 0 ? ( |
| 92 | 104 | <Text color={muted} maxLines={1} style={{ fontSize: 10 }}> |
| 93 | {`${hidden} more in T3`} | |
| 105 | {`${hidden} more in The Knife`} | |
| 94 | 106 | </Text> |
| 95 | 107 | ) : null} |
| 96 | 108 | </Column> |
| ... | ... | @@ -104,7 +116,7 @@ export function SubscriptionUsage(props: SubscriptionUsageProps, environment: Wi |
| 104 | 116 | > |
| 105 | 117 | {props.checkedAt |
| 106 | 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 | 120 | </Text> |
| 109 | 121 | </Column> |
| 110 | 122 | </Button> |
apps/mobile/src/widgets/SubscriptionUsage.tsx+6-6| ... | ... | @@ -39,8 +39,8 @@ function SubscriptionUsage( |
| 39 | 39 | const monochrome = |
| 40 | 40 | environment.widgetRenderingMode !== "fullColor" || environment.isLuminanceReduced; |
| 41 | 41 | const providers = props.providers ?? [ |
| 42 | { name: "Codex", detail: "Open T3 to connect", windows: [], expiresAt: 0 }, | |
| 43 | { name: "Claude", detail: "Open T3 to connect", windows: [], expiresAt: 0 }, | |
| 42 | { name: "Codex", detail: "Open The Knife to connect", windows: [], expiresAt: 0 }, | |
| 43 | { name: "Claude", detail: "Open The Knife to connect", windows: [], expiresAt: 0 }, | |
| 44 | 44 | ]; |
| 45 | 45 | const columns = providers.map((provider) => { |
| 46 | 46 | const stale = provider.windows.length > 0 && now >= provider.expiresAt; |
| ... | ... | @@ -73,7 +73,7 @@ function SubscriptionUsage( |
| 73 | 73 | ].slice(0, limit) |
| 74 | 74 | : windows.slice(0, limit); |
| 75 | 75 | const detail = stale |
| 76 | ? "Open T3 to refresh" | |
| 76 | ? "Open The Knife to refresh" | |
| 77 | 77 | : period !== "auto" && windows.length === 0 && provider.windows.length > 0 |
| 78 | 78 | ? `No ${period} limit reported` |
| 79 | 79 | : provider.detail; |
| ... | ... | @@ -121,7 +121,7 @@ function SubscriptionUsage( |
| 121 | 121 | ? `${tightest.remaining}% left` |
| 122 | 122 | : period !== "auto" && !stale && provider.windows.length > 0 |
| 123 | 123 | ? "N/A" |
| 124 | : "Open T3"} | |
| 124 | : "Open The Knife"} | |
| 125 | 125 | </Text> |
| 126 | 126 | </HStack> |
| 127 | 127 | {tightest ? ( |
| ... | ... | @@ -223,7 +223,7 @@ function SubscriptionUsage( |
| 223 | 223 | > |
| 224 | 224 | {(period === "auto" ? (provider.totalWindows ?? windows.length) : windows.length) - |
| 225 | 225 | limit}{" "} |
| 226 | more in T3 | |
| 226 | more in The Knife | |
| 227 | 227 | </Text> |
| 228 | 228 | ) : null} |
| 229 | 229 | </VStack> |
| ... | ... | @@ -255,7 +255,7 @@ function SubscriptionUsage( |
| 255 | 255 | > |
| 256 | 256 | {props.checkedAt |
| 257 | 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 | 259 | </Text> |
| 260 | 260 | ) : null} |
| 261 | 261 | </VStack> |
apps/mobile/src/widgets/publishSubscriptionUsage.android.ts+1-2| ... | ... | @@ -22,8 +22,7 @@ export async function publishSubscriptionUsage(snapshot: SubscriptionUsageSnapsh |
| 22 | 22 | }); |
| 23 | 23 | } |
| 24 | 24 | widget.updateSnapshot(snapshot); |
| 25 | // Android has no timeline; an alarm re-renders the stored snapshot at each | |
| 26 | // deadline so stale readings flip to "Open T3 to refresh" unattended. | |
| 25 | // Android has no timeline; an alarm re-renders the stored snapshot at each deadline. | |
| 27 | 26 | requireOptionalNativeModule<{ schedule: (name: string, deadlines: number[]) => void }>( |
| 28 | 27 | "T3WidgetExpiry", |
| 29 | 28 | )?.schedule( |
apps/mobile/src/widgets/subscriptionUsageSnapshot.ts+2-2| ... | ... | @@ -94,7 +94,7 @@ function subscriptionUsageProps( |
| 94 | 94 | return { |
| 95 | 95 | name, |
| 96 | 96 | detail: !fresh |
| 97 | ? "Open T3 to refresh" | |
| 97 | ? "Open The Knife to refresh" | |
| 98 | 98 | : pool.accounts.length > 1 |
| 99 | 99 | ? `${pool.accounts.length} accounts · pooled` |
| 100 | 100 | : "Subscription remaining", |
| ... | ... | @@ -161,7 +161,7 @@ export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, n |
| 161 | 161 | ...snapshot, |
| 162 | 162 | providers: snapshot.providers.map((provider) => |
| 163 | 163 | 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 | 165 | : provider, |
| 166 | 166 | ), |
| 167 | 167 | }, |
apps/web/index.html+3-3| ... | ... | @@ -477,13 +477,13 @@ |
| 477 | 477 | cursor: pointer; |
| 478 | 478 | } |
| 479 | 479 | </style> |
| 480 | <title>T3 Code (Alpha)</title> | |
| 480 | <title>The Knife</title> | |
| 481 | 481 | </head> |
| 482 | 482 | <body> |
| 483 | 483 | <div id="root"> |
| 484 | 484 | <div id="boot-shell"> |
| 485 | <div id="boot-shell-card" aria-label="T3 Code splash screen"> | |
| 486 | <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" /> | |
| 485 | <div id="boot-shell-card" aria-label="The Knife splash screen"> | |
| 486 | <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="The Knife" /> | |
| 487 | 487 | </div> |
| 488 | 488 | </div> |
| 489 | 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 | 1 | { |
| 2 | "name": "The Knife", | |
| 3 | "short_name": "The Knife", | |
| 2 | 4 | "id": "/", |
| 3 | 5 | "start_url": "/", |
| 4 | 6 | "scope": "/", |
apps/web/src/bootstrap.test.ts+2-2| ... | ... | @@ -64,7 +64,7 @@ describe("app startup failures", () => { |
| 64 | 64 | await import("./bootstrap"); |
| 65 | 65 | await vi.dynamicImportSettled(); |
| 66 | 66 | |
| 67 | expect(bootShell?.text).toContain("T3 Code could not load."); | |
| 67 | expect(bootShell?.text).toContain("The Knife could not load."); | |
| 68 | 68 | const reloadButton = bootShell?.children[0]?.children.find( |
| 69 | 69 | (element) => element.tagName === "button", |
| 70 | 70 | ); |
| ... | ... | @@ -78,7 +78,7 @@ describe("app startup failures", () => { |
| 78 | 78 | |
| 79 | 79 | showBootError(new Error("internal module path")); |
| 80 | 80 | |
| 81 | expect(bootShell?.text).toContain("T3 Code could not load."); | |
| 81 | expect(bootShell?.text).toContain("The Knife could not load."); | |
| 82 | 82 | expect(bootShell?.text.includes("internal module path")).toBe(dev); |
| 83 | 83 | }); |
| 84 | 84 |
apps/web/src/branding.test.ts+15-15| ... | ... | @@ -24,9 +24,9 @@ describe("branding", () => { |
| 24 | 24 | value: { |
| 25 | 25 | desktopBridge: { |
| 26 | 26 | getAppBranding: () => ({ |
| 27 | baseName: "T3 Code", | |
| 27 | baseName: "The Knife", | |
| 28 | 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 | 34 | |
| 35 | 35 | const branding = await import("./branding"); |
| 36 | 36 | |
| 37 | expect(branding.APP_BASE_NAME).toBe("T3 Code"); | |
| 37 | expect(branding.APP_BASE_NAME).toBe("The Knife"); | |
| 38 | 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 | }); |
| 41 | 41 | |
| 42 | 42 | it("normalizes hosted app channel metadata", async () => { |
| ... | ... | @@ -47,7 +47,7 @@ describe("branding", () => { |
| 47 | 47 | expect(branding.HOSTED_APP_CHANNEL).toBe("nightly"); |
| 48 | 48 | expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly"); |
| 49 | 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 | }); |
| 52 | 52 | |
| 53 | 53 | it("does not label the latest hosted app channel", async () => { |
| ... | ... | @@ -58,7 +58,7 @@ describe("branding", () => { |
| 58 | 58 | expect(branding.HOSTED_APP_CHANNEL).toBe("latest"); |
| 59 | 59 | expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest"); |
| 60 | 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 | }); |
| 63 | 63 | |
| 64 | 64 | it("ignores unknown hosted app channels", async () => { |
| ... | ... | @@ -84,33 +84,33 @@ describe("branding logic", () => { |
| 84 | 84 | it("updates the display name for nightly primary server versions", () => { |
| 85 | 85 | expect( |
| 86 | 86 | resolveServerBackedAppDisplayName({ |
| 87 | baseName: "T3 Code", | |
| 88 | fallbackDisplayName: "T3 Code (Alpha)", | |
| 87 | baseName: "The Knife", | |
| 88 | fallbackDisplayName: "The Knife (Alpha)", | |
| 89 | 89 | fallbackStageLabel: "Alpha", |
| 90 | 90 | primaryServerVersion: "0.0.28-nightly.20260616.12", |
| 91 | 91 | }), |
| 92 | ).toBe("T3 Code (Nightly)"); | |
| 92 | ).toBe("The Knife (Nightly)"); | |
| 93 | 93 | }); |
| 94 | 94 | |
| 95 | 95 | it("keeps the fallback display name for stable primary server versions", () => { |
| 96 | 96 | expect( |
| 97 | 97 | resolveServerBackedAppDisplayName({ |
| 98 | baseName: "T3 Code", | |
| 99 | fallbackDisplayName: "T3 Code (Alpha)", | |
| 98 | baseName: "The Knife", | |
| 99 | fallbackDisplayName: "The Knife (Alpha)", | |
| 100 | 100 | fallbackStageLabel: "Alpha", |
| 101 | 101 | primaryServerVersion: "0.0.27", |
| 102 | 102 | }), |
| 103 | ).toBe("T3 Code (Alpha)"); | |
| 103 | ).toBe("The Knife (Alpha)"); | |
| 104 | 104 | }); |
| 105 | 105 | |
| 106 | 106 | it("keeps the fallback display name for malformed nightly primary server versions", () => { |
| 107 | 107 | expect( |
| 108 | 108 | resolveServerBackedAppDisplayName({ |
| 109 | baseName: "T3 Code", | |
| 110 | fallbackDisplayName: "T3 Code (Alpha)", | |
| 109 | baseName: "The Knife", | |
| 110 | fallbackDisplayName: "The Knife (Alpha)", | |
| 111 | 111 | fallbackStageLabel: "Alpha", |
| 112 | 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 | 16 | hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null; |
| 17 | 17 | export const HOSTED_APP_CHANNEL_LABEL = |
| 18 | 18 | HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null; |
| 19 | export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; | |
| 19 | export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "The Knife"; | |
| 20 | 20 | export const APP_STAGE_LABEL = |
| 21 | 21 | injectedDesktopAppBranding?.stageLabel ?? |
| 22 | 22 | HOSTED_APP_CHANNEL_LABEL ?? |
| 23 | (import.meta.env.DEV ? "Dev" : "Alpha"); | |
| 23 | (import.meta.env.DEV ? "Dev" : "Latest"); | |
| 24 | 24 | export const APP_DISPLAY_NAME = |
| 25 | 25 | injectedDesktopAppBranding?.displayName ?? |
| 26 | 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 | 59 | if (status.status === "available") return; |
| 60 | 60 | if (status.status === "unsupported") { |
| 61 | 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 | } |
| 65 | 65 | |
| ... | ... | @@ -95,7 +95,7 @@ function ensureRelayClientAvailable( |
| 95 | 95 | return yield* new CloudEnvironmentLinkError({ |
| 96 | 96 | message: |
| 97 | 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 | 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 | 11178 | ) : renderedRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? ( |
| 11179 | 11179 | <PullRequestsUnavailableState |
| 11180 | 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 | 11183 | ) : renderedRightPanelSurface?.kind === "pull-request" ? ( |
| 11184 | 11184 | // 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 @@ |
| 1 | import type { ImgHTMLAttributes } from "react"; | |
| 2 | import knife from "../../../../assets/knife/app-icon.icon/Assets/knife.png"; | |
| 3 | ||
| 4 | export 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 | 171 | } |
| 172 | 172 | |
| 173 | 173 | const 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 | 175 | terminal: "Terminal surfaces are only available from a project thread.", |
| 176 | 176 | files: "Files are only available when a project is open.", |
| 177 | 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 | 157 | if (desktopTargets.length > 0) { |
| 158 | 158 | const confirmed = |
| 159 | 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 | 161 | )) ?? true; |
| 162 | 162 | if (!confirmed) return; |
| 163 | 163 | } |
| ... | ... | @@ -279,7 +279,7 @@ export function ServerUpdateAction({ |
| 279 | 279 | // remote machine installs without asking anyone there. |
| 280 | 280 | const confirmed = |
| 281 | 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 | 283 | )) ?? true; |
| 284 | 284 | if (!confirmed) { |
| 285 | 285 | return; |
apps/web/src/components/T3Wordmark.tsx deleted-12| ... | ... | @@ -1,12 +0,0 @@ |
| 1 | import type { SVGProps } from "react"; | |
| 2 | ||
| 3 | export 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 | 119 | import ReactMarkdown from "react-markdown"; |
| 120 | 120 | import remarkGfm from "remark-gfm"; |
| 121 | 121 | import type { Root, RootContent } from "mdast"; |
| 122 | import { T3Wordmark } from "../T3Wordmark"; | |
| 122 | import { KnifeMark } from "../KnifeMark"; | |
| 123 | 123 | import { ThreadContextChip } from "../ThreadContextChip"; |
| 124 | 124 | import { |
| 125 | 125 | BotIcon, |
| ... | ... | @@ -2462,7 +2462,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" |
| 2462 | 2462 | className="me-1 text-2xs text-muted-foreground/70" |
| 2463 | 2463 | data-user-message-attribution="t3code" |
| 2464 | 2464 | > |
| 2465 | Sent by T3 Code | |
| 2465 | Sent by The Knife | |
| 2466 | 2466 | </p> |
| 2467 | 2467 | ) : userMessage.attribution === "agent" ? ( |
| 2468 | 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 | 2837 | return ( |
| 2838 | 2838 | <> |
| 2839 | 2839 | <div className="relative min-w-0 px-1 py-0.5"> |
| 2840 | <MessageAuthorHeading>T3 Code</MessageAuthorHeading> | |
| 2840 | <MessageAuthorHeading>The Knife</MessageAuthorHeading> | |
| 2841 | 2841 | <div data-thread-find-text="true"> |
| 2842 | 2842 | <AssistantCitationSource |
| 2843 | 2843 | messageId={row.message.id} |
| ... | ... | @@ -5236,7 +5236,7 @@ function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className |
| 5236 | 5236 | case "device": |
| 5237 | 5237 | return <SmartphoneIcon className={className} aria-hidden />; |
| 5238 | 5238 | case "t3-code": |
| 5239 | return <T3Wordmark className={className} aria-hidden />; | |
| 5239 | return <KnifeMark className={className} aria-hidden />; | |
| 5240 | 5240 | case "check": |
| 5241 | 5241 | return <CheckIcon className={className} aria-hidden />; |
| 5242 | 5242 | case "circle-alert": |
apps/web/src/components/chat/V2LifecycleRow.tsx+2-2| ... | ... | @@ -48,7 +48,7 @@ import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon" |
| 48 | 48 | import { cn } from "~/lib/utils"; |
| 49 | 49 | import { TimelineSystemDivider } from "./TimelineSystemDivider"; |
| 50 | 50 | import { Button, InlineButton } from "../ui/button"; |
| 51 | import { T3Wordmark } from "../T3Wordmark"; | |
| 51 | import { KnifeMark } from "../KnifeMark"; | |
| 52 | 52 | |
| 53 | 53 | const LIFECYCLE_TYPES = new Set<OrchestrationV2TurnItem["type"]>([ |
| 54 | 54 | "run_interrupt_request", |
| ... | ... | @@ -170,7 +170,7 @@ export function V2LifecycleRow(props: { |
| 170 | 170 | return ( |
| 171 | 171 | <WorkLogRow |
| 172 | 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 | 174 | label={<>Created thread{item.title ? ` · ${item.title}` : ""}</>} |
| 175 | 175 | trailing={ |
| 176 | 176 | <InlineButton |
apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.test.ts+3-3| ... | ... | @@ -31,7 +31,7 @@ describe("mobile client presentation", () => { |
| 31 | 31 | it("describes the client platform and enabled notification events", () => { |
| 32 | 32 | const client = device(); |
| 33 | 33 | |
| 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 | 35 | expect(mobileClientNotificationDetail(client)).toBe( |
| 36 | 36 | "Alerts enabled for approvals, completions.", |
| 37 | 37 | ); |
| ... | ... | @@ -42,7 +42,7 @@ describe("mobile client presentation", () => { |
| 42 | 42 | mobileClientPlatformLabel( |
| 43 | 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 | }); |
| 47 | 47 | |
| 48 | 48 | it("distinguishes disabled notifications from an empty event selection", () => { |
| ... | ... | @@ -68,7 +68,7 @@ describe("mobile client presentation", () => { |
| 68 | 68 | |
| 69 | 69 | it("handles missing app versions and invalid update timestamps", () => { |
| 70 | 70 | expect(mobileClientPlatformLabel(device({ iosMajorVersion: null }))).toBe( |
| 71 | "iOS · T3 Code 1.2.3", | |
| 71 | "iOS · The Knife 1.2.3", | |
| 72 | 72 | ); |
| 73 | 73 | expect(mobileClientPlatformLabel(device({ appVersion: null }))).toBe("iOS 18"); |
| 74 | 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 | 21 | : device.iosMajorVersion === null |
| 22 | 22 | ? "iOS" |
| 23 | 23 | : `iOS ${device.iosMajorVersion}`; |
| 24 | return `${platform}${device.appVersion ? ` · T3 Code ${device.appVersion}` : ""}`; | |
| 24 | return `${platform}${device.appVersion ? ` · The Knife ${device.appVersion}` : ""}`; | |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | 27 | export function mobileClientNotificationDetail(device: RelayClientDeviceRecord): string { |
apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx+1-1| ... | ... | @@ -94,7 +94,7 @@ function EmptyMobileClients() { |
| 94 | 94 | <EmptyHeader> |
| 95 | 95 | <EmptyTitle>No mobile clients</EmptyTitle> |
| 96 | 96 | <EmptyDescription> |
| 97 | Install T3 Code on your phone and sign in to T3 Connect to get push notifications and Live | |
| 97 | Install The Knife on your phone and sign in to T3 Connect to get push notifications and Live | |
| 98 | 98 | Activities. |
| 99 | 99 | </EmptyDescription> |
| 100 | 100 | </EmptyHeader> |
apps/web/src/components/cloud/RelayClientInstallDialog.tsx+4-4| ... | ... | @@ -69,8 +69,8 @@ export function RelayClientInstallDialog() { |
| 69 | 69 | </DialogTitle> |
| 70 | 70 | <DialogDescription> |
| 71 | 71 | {isInstalling |
| 72 | ? "T3 Code is preparing this environment for secure access through T3 Connect." | |
| 73 | : "T3 Code needs the relay client to make this environment available through T3 Connect."} | |
| 72 | ? "The Knife is preparing this environment for secure access through T3 Connect." | |
| 73 | : "The Knife needs the relay client to make this environment available through T3 Connect."} | |
| 74 | 74 | </DialogDescription> |
| 75 | 75 | </DialogHeader> |
| 76 | 76 | <DialogPanel scrollFade={false}> |
| ... | ... | @@ -91,14 +91,14 @@ export function RelayClientInstallDialog() { |
| 91 | 91 | value={activeStepIndex + 1} |
| 92 | 92 | /> |
| 93 | 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 | 95 | </p> |
| 96 | 96 | </div> |
| 97 | 97 | ) : ( |
| 98 | 98 | <div className="rounded-xl border border-border/70 bg-muted/35 p-3"> |
| 99 | 99 | <p className="text-sm font-medium text-foreground">Managed relay client</p> |
| 100 | 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 | 102 | {view.status === "confirming" ? view.version : ""} locally. |
| 103 | 103 | </p> |
| 104 | 104 | </div> |
apps/web/src/components/desktop/DesktopAppActivationCoordinator.tsx+1-1| ... | ... | @@ -63,7 +63,7 @@ export function DesktopAppActivationCoordinator() { |
| 63 | 63 | }); |
| 64 | 64 | if (result._tag === "Failure") { |
| 65 | 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 | 68 | return projectId; |
| 69 | 69 | }, |
apps/web/src/components/desktop/SshPasswordPromptDialog.tsx+1-1| ... | ... | @@ -161,7 +161,7 @@ function ActiveSshPasswordPrompt({ |
| 161 | 161 | <DialogTitle>SSH Password Required</DialogTitle> |
| 162 | 162 | <DialogDescription> |
| 163 | 163 | 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 | 165 | </DialogDescription> |
| 166 | 166 | </DialogHeader> |
| 167 | 167 | <DialogPanel scrollFade={false}> |
apps/web/src/components/desktopUpdate.logic.test.ts+3-3| ... | ... | @@ -279,7 +279,7 @@ describe("desktop update UI helpers", () => { |
| 279 | 279 | availableVersion: "1.1.0", |
| 280 | 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 | }); |
| 284 | 284 | |
| 285 | 285 | it("falls back to generic install confirmation copy when no version is available", () => { |
| ... | ... | @@ -288,7 +288,7 @@ describe("desktop update UI helpers", () => { |
| 288 | 288 | availableVersion: null, |
| 289 | 289 | downloadedVersion: null, |
| 290 | 290 | }), |
| 291 | ).toContain("Install update and restart T3 Code?"); | |
| 291 | ).toContain("Install update and restart The Knife?"); | |
| 292 | 292 | }); |
| 293 | 293 | |
| 294 | 294 | it("keeps the same install confirmation copy across desktop platforms", () => { |
| ... | ... | @@ -298,7 +298,7 @@ describe("desktop update UI helpers", () => { |
| 298 | 298 | downloadedVersion: "1.1.0", |
| 299 | 299 | }), |
| 300 | 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 | 62 | |
| 63 | 63 | const action = resolveDesktopUpdateButtonAction(state); |
| 64 | 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 | 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 | } |
| 72 | 72 | |
| 73 | 73 | export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string { |
| ... | ... | @@ -108,7 +108,7 @@ export function getDesktopUpdateInstallConfirmationMessage( |
| 108 | 108 | state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">, |
| 109 | 109 | ): string { |
| 110 | 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 | } |
| 113 | 113 | |
| 114 | 114 | export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null { |
apps/web/src/components/onboarding/FirstRunGate.tsx+1-1| ... | ... | @@ -214,7 +214,7 @@ function FirstRunRecovery({ |
| 214 | 214 | <p className="mt-2 text-sm text-muted-foreground"> |
| 215 | 215 | {settingsReadFailed |
| 216 | 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 | 218 | </p> |
| 219 | 219 | <Button |
| 220 | 220 | className="mt-5" |
apps/web/src/components/onboarding/WelcomeWizard.tsx+9-9| ... | ... | @@ -84,7 +84,7 @@ import { TerminalViewport } from "../ThreadTerminalDrawer"; |
| 84 | 84 | import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList"; |
| 85 | 85 | import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation"; |
| 86 | 86 | import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; |
| 87 | import { T3Wordmark } from "../T3Wordmark"; | |
| 87 | import { KnifeMark } from "../KnifeMark"; | |
| 88 | 88 | import { Alert, AlertDescription } from "../ui/alert"; |
| 89 | 89 | import { Button } from "../ui/button"; |
| 90 | 90 | import { CommandBlock } from "../CommandBlock"; |
| ... | ... | @@ -233,12 +233,12 @@ export function WelcomeWizard({ |
| 233 | 233 | initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true} |
| 234 | 234 | > |
| 235 | 235 | <WizardHeader |
| 236 | title="Set up T3 Code" | |
| 236 | title="Set up The Knife" | |
| 237 | 237 | identity={ |
| 238 | <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code"> | |
| 239 | <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden /> | |
| 238 | <div className="flex items-center gap-2" role="img" aria-label="The Knife"> | |
| 239 | <KnifeMark className="size-8 shrink-0" aria-hidden /> | |
| 240 | 240 | <span className="text-2xl font-medium tracking-tight text-muted-foreground"> |
| 241 | Code | |
| 241 | The Knife | |
| 242 | 242 | </span> |
| 243 | 243 | </div> |
| 244 | 244 | } |
| ... | ... | @@ -432,8 +432,8 @@ function ConnectionStep({ |
| 432 | 432 | ) : null} |
| 433 | 433 | <div className="mt-6 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6"> |
| 434 | 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 your | |
| 436 | data is used and how to opt out, see our{" "} | |
| 435 | Anonymous usage data helps improve T3 Code. To read more about how your data is used and | |
| 436 | how to opt out, see our{" "} | |
| 437 | 437 | <a |
| 438 | 438 | className="underline underline-offset-2 hover:text-foreground" |
| 439 | 439 | href={PRIVACY_POLICY_URL} |
| ... | ... | @@ -530,7 +530,7 @@ function ConnectAccountOption({ |
| 530 | 530 | </p> |
| 531 | 531 | <CommandBlock command="npx t3 connect" className="mt-3" /> |
| 532 | 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 | 534 | </p> |
| 535 | 535 | </div> |
| 536 | 536 | </CollapsiblePanel> |
| ... | ... | @@ -644,7 +644,7 @@ function PairingForm({ |
| 644 | 644 | </p> |
| 645 | 645 | <CommandBlock command="npx t3 pair" className="mt-2" /> |
| 646 | 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 | 648 | <code className="font-mono">--tailscale</code> to use your tailnet. |
| 649 | 649 | </p> |
| 650 | 650 | </CollapsiblePanel> |
apps/web/src/components/preview/PreviewPanel.tsx+1-1| ... | ... | @@ -41,7 +41,7 @@ export function PreviewPanel({ |
| 41 | 41 | <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center"> |
| 42 | 42 | <p className="max-w-sm text-sm text-muted-foreground"> |
| 43 | 43 | {canOperatePreview |
| 44 | ? "Preview is only available in the T3 Code desktop app." | |
| 44 | ? "Preview is only available in The Knife desktop app." | |
| 45 | 45 | : "Pair this client again with preview access to control browser previews."} |
| 46 | 46 | </p> |
| 47 | 47 | </div> |
apps/web/src/components/preview/PreviewView.tsx+1-1| ... | ... | @@ -1097,7 +1097,7 @@ export function PreviewView({ |
| 1097 | 1097 | ) : ( |
| 1098 | 1098 | <div className="flex h-full items-center justify-center p-8 text-center"> |
| 1099 | 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 | 1101 | </p> |
| 1102 | 1102 | </div> |
| 1103 | 1103 | ) |
apps/web/src/components/promptLinks/HostedPromptLinkLanding.tsx+1-1| ... | ... | @@ -146,7 +146,7 @@ function HostedPromptLinkLanding({ |
| 146 | 146 | <Alert className="mt-5" variant="error"> |
| 147 | 147 | <AlertTitle>This prompt had hidden characters, it may be malicious</AlertTitle> |
| 148 | 148 | <AlertDescription> |
| 149 | T3 Code removed {prompt.hiddenCharacters} hidden character | |
| 149 | The Knife removed {prompt.hiddenCharacters} hidden character | |
| 150 | 150 | {prompt.hiddenCharacters === 1 ? "" : "s"}. Read the prompt before you send it. |
| 151 | 151 | </AlertDescription> |
| 152 | 152 | </Alert> |
apps/web/src/components/promptLinks/NewThreadLinkView.tsx+2-2| ... | ... | @@ -99,7 +99,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) { |
| 99 | 99 | replace: true, |
| 100 | 100 | }).catch(() => null); |
| 101 | 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 | 103 | return; |
| 104 | 104 | } |
| 105 | 105 | setState({ status: "opened" }); |
| ... | ... | @@ -169,7 +169,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) { |
| 169 | 169 | setState({ |
| 170 | 170 | status: "failed", |
| 171 | 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 | 174 | return; |
| 175 | 175 | } |
apps/web/src/components/promptLinks/linkedPrompt.ts+1-1| ... | ... | @@ -10,7 +10,7 @@ export function warnAboutHiddenCharacters(prompt: LinkedPrompt): void { |
| 10 | 10 | toastManager.add({ |
| 11 | 11 | type: "warning", |
| 12 | 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 | 14 | timeout: 0, |
| 15 | 15 | }); |
| 16 | 16 | } |
apps/web/src/components/settings/BrowserImportWizard.tsx+4-4| ... | ... | @@ -285,9 +285,9 @@ function FullDiskAccessStep({ |
| 285 | 285 | return ( |
| 286 | 286 | <> |
| 287 | 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 | 289 | <DialogDescription> |
| 290 | To import cookies from {source.name}, T3 Code needs Full Disk Access. Turn it on in System | |
| 290 | To import cookies from {source.name}, The Knife needs Full Disk Access. Turn it on in System | |
| 291 | 291 | Settings, then come back to finish the import — you can revoke it again once the import is |
| 292 | 292 | done. |
| 293 | 293 | </DialogDescription> |
| ... | ... | @@ -319,8 +319,8 @@ function FullDiskAccessStep({ |
| 319 | 319 | {!permission.isReady(["fullDiskAccess"]) ? ( |
| 320 | 320 | <p className="mt-3 text-xs text-muted-foreground"> |
| 321 | 321 | {stillRequired |
| 322 | ? "Access is still required. Quit and reopen T3 Code if you just allowed it, then retry the import." | |
| 323 | : "If access doesn't update after you allow it, quit and reopen T3 Code, then retry the import."} | |
| 322 | ? "Access is still required. Quit and reopen The Knife if you just allowed it, then retry the import." | |
| 323 | : "If access doesn't update after you allow it, quit and reopen The Knife, then retry the import."} | |
| 324 | 324 | </p> |
| 325 | 325 | ) : null} |
| 326 | 326 | </DialogPanel> |
apps/web/src/components/settings/CaptureShortcutConfig.tsx+5-5| ... | ... | @@ -217,7 +217,7 @@ export function CaptureShortcutConfig({ |
| 217 | 217 | ) : ( |
| 218 | 218 | <> |
| 219 | 219 | <p className="text-muted-foreground"> |
| 220 | Allow T3 Code to read your desktop settings. You'll review any changes here before | |
| 220 | Allow The Knife to read your desktop settings. You'll review any changes here before | |
| 221 | 221 | saving. |
| 222 | 222 | </p> |
| 223 | 223 | <Button |
| ... | ... | @@ -229,7 +229,7 @@ export function CaptureShortcutConfig({ |
| 229 | 229 | </Button> |
| 230 | 230 | {!supported ? ( |
| 231 | 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 | 233 | </p> |
| 234 | 234 | ) : null} |
| 235 | 235 | </> |
| ... | ... | @@ -243,7 +243,7 @@ export function CaptureShortcutConfig({ |
| 243 | 243 | <p role="status" className="text-muted-foreground"> |
| 244 | 244 | {state.shortcutPending |
| 245 | 245 | ? "Connecting to your desktop…" |
| 246 | : "Restart T3 Code to finish connecting your shortcut."} | |
| 246 | : "Restart The Knife to finish connecting your shortcut."} | |
| 247 | 247 | </p> |
| 248 | 248 | ) : null} |
| 249 | 249 | <details className="text-xs text-muted-foreground"> |
| ... | ... | @@ -262,7 +262,7 @@ export function CaptureShortcutConfig({ |
| 262 | 262 | state.shortcutConfigPath ?? |
| 263 | 263 | (niri ? "~/.config/niri/config.kdl" : "~/.config/hypr/hyprland.conf")} |
| 264 | 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 | 266 | {preview && preview.resolvedPath !== preview.path ? ( |
| 267 | 267 | <p className="break-all">Linked to {preview.resolvedPath}. The link will be kept.</p> |
| 268 | 268 | ) : null} |
| ... | ... | @@ -323,7 +323,7 @@ export function CaptureShortcutConfig({ |
| 323 | 323 | {isCopied ? "Copied" : "Copy shortcut"} |
| 324 | 324 | </Button> |
| 325 | 325 | <p> |
| 326 | Turn capture off in T3 Code to stop it. Remove the shortcut from {desktop} to free up | |
| 326 | Turn capture off in The Knife to stop it. Remove the shortcut from {desktop} to free up | |
| 327 | 327 | the keys. |
| 328 | 328 | </p> |
| 329 | 329 | {state.shortcutActionRegistered === false ? ( |
apps/web/src/components/settings/ChatGptWelcomeCoordinator.tsx+1-1| ... | ... | @@ -66,7 +66,7 @@ export function ChatGptWelcomeCoordinator() { |
| 66 | 66 | <OpenAI className="mb-2 size-8" aria-hidden="true" /> |
| 67 | 67 | <DialogTitle>Your ChatGPT plan is connected</DialogTitle> |
| 68 | 68 | <DialogDescription> |
| 69 | Eligible usage in T3 Code uses your ChatGPT plan. Manage your shared usage and any | |
| 69 | Eligible usage in The Knife uses your ChatGPT plan. Manage your shared usage and any | |
| 70 | 70 | credit settings in ChatGPT. |
| 71 | 71 | </DialogDescription> |
| 72 | 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 | 52 | if (!bridge || !state?.supported) return null; |
| 53 | 53 | const installed = state.installedPath !== null; |
| 54 | 54 | 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 | 56 | : !installed |
| 57 | ? "Run T3 Code's CLI as `t3` from any terminal." | |
| 57 | ? "Run The Knife's CLI as `t3` from any terminal." | |
| 58 | 58 | : state.onPath |
| 59 | 59 | ? `Installed at ${state.installedPath}. Open a new terminal to use it.` |
| 60 | 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 | 577 | : installation?.phase === "verifying" |
| 578 | 578 | ? "Checking Codex." |
| 579 | 579 | : installed |
| 580 | ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by T3 Code"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.` | |
| 581 | : (installation?.message ?? "T3 Code downloads and manages Codex for you."); | |
| 580 | ? `${installation?.source === "local" ? "Using your installed Codex" : "Managed by The Knife"}${installation?.installedVersion ? ` · v${installation.installedVersion}` : ""}.` | |
| 581 | : (installation?.message ?? "The Knife downloads and manages Codex for you."); | |
| 582 | 582 | const accountDescription = finishingSignIn ? ( |
| 583 | 583 | "Finishing sign-in..." |
| 584 | 584 | ) : installActive ? ( |
| ... | ... | @@ -655,7 +655,7 @@ function ManagedCodexSetup({ |
| 655 | 655 | const callbackCompletion = |
| 656 | 656 | !handoff && auth?.phase === "waiting" && url ? ( |
| 657 | 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 | 659 | <form |
| 660 | 660 | className="flex flex-col gap-2 sm:flex-row sm:items-center" |
| 661 | 661 | onSubmit={(event) => { |
| ... | ... | @@ -1032,7 +1032,7 @@ export function CodexManagedRuntimeFields({ |
| 1032 | 1032 | <> |
| 1033 | 1033 | <SettingsRow |
| 1034 | 1034 | title="Binary path" |
| 1035 | description="Selected by T3 Code." | |
| 1035 | description="Selected by The Knife." | |
| 1036 | 1036 | control={ |
| 1037 | 1037 | <div className="w-full sm:w-80"> |
| 1038 | 1038 | <Input |
apps/web/src/components/settings/ConnectionsSettings.tsx+10-10| ... | ... | @@ -3480,7 +3480,7 @@ export function ConnectionsSettings() { |
| 3480 | 3480 | {desktopWslState.enabled ? ( |
| 3481 | 3481 | <SettingsRow |
| 3482 | 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 | 3484 | className="bg-muted/20 pl-7 sm:pl-8" |
| 3485 | 3485 | control={ |
| 3486 | 3486 | <Switch |
| ... | ... | @@ -3805,8 +3805,8 @@ export function ConnectionsSettings() { |
| 3805 | 3805 | </AlertDialogTitle> |
| 3806 | 3806 | <AlertDialogDescription> |
| 3807 | 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." | |
| 3809 | : "Devices connected over your local network will disconnect. Existing tunnels, such as T3 Connect or Tailscale HTTPS, keep working. T3 Code will restart."} | |
| 3808 | ? "Let your other devices connect to The Knife over the network. Pair devices to give them access. The Knife will restart." | |
| 3809 | : "Devices connected over your local network will disconnect. Existing tunnels, such as T3 Connect or Tailscale HTTPS, keep working. The Knife will restart."} | |
| 3810 | 3810 | </AlertDialogDescription> |
| 3811 | 3811 | </AlertDialogHeader> |
| 3812 | 3812 | <AlertDialogFooter> |
| ... | ... | @@ -3862,15 +3862,15 @@ export function ConnectionsSettings() { |
| 3862 | 3862 | <AlertDialogDescription> |
| 3863 | 3863 | {pendingWslChange?.kind === "disable" |
| 3864 | 3864 | ? pendingWslChange.wasWslOnly |
| 3865 | ? "T3 Code will restart on the Windows backend. Threads and projects opened against WSL stay safe inside the distro and become available again when you re-enable WSL." | |
| 3866 | : "The WSL backend will stop. Threads and projects opened against WSL stay safe inside the distro, but they'll be unavailable in T3 Code until you re-enable WSL." | |
| 3865 | ? "The Knife will restart on the Windows backend. Threads and projects opened against WSL stay safe inside the distro and become available again when you re-enable WSL." | |
| 3866 | : "The WSL backend will stop. Threads and projects opened against WSL stay safe inside the distro, but they'll be unavailable in The Knife until you re-enable WSL." | |
| 3867 | 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 | 3869 | : pendingWslChange?.kind === "enable" |
| 3870 | 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 | 3871 | : pendingWslChange?.nextValue |
| 3872 | ? "T3 Code will restart and start only the WSL backend. Your Windows-side projects won't be accessible until you turn this off again." | |
| 3873 | : "T3 Code will restart and bring the Windows backend back up alongside WSL."} | |
| 3872 | ? "The Knife will restart and start only the WSL backend. Your Windows-side projects won't be accessible until you turn this off again." | |
| 3873 | : "The Knife will restart and bring the Windows backend back up alongside WSL."} | |
| 3874 | 3874 | </AlertDialogDescription> |
| 3875 | 3875 | </AlertDialogHeader> |
| 3876 | 3876 | <AlertDialogFooter> |
| ... | ... | @@ -3956,7 +3956,7 @@ export function ConnectionsSettings() { |
| 3956 | 3956 | <AlertDialogHeader> |
| 3957 | 3957 | <AlertDialogTitle>Disable Tailscale HTTPS?</AlertDialogTitle> |
| 3958 | 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 | 3960 | </AlertDialogDescription> |
| 3961 | 3961 | </AlertDialogHeader> |
| 3962 | 3962 | <AlertDialogFooter> |
| ... | ... | @@ -3994,7 +3994,7 @@ export function ConnectionsSettings() { |
| 3994 | 3994 | <DialogHeader> |
| 3995 | 3995 | <DialogTitle>Set up Tailscale HTTPS?</DialogTitle> |
| 3996 | 3996 | <DialogDescription> |
| 3997 | T3 Code will restart the local backend with Tailscale Serve enabled and ask | |
| 3997 | The Knife will restart the local backend with Tailscale Serve enabled and ask | |
| 3998 | 3998 | Tailscale to proxy HTTPS traffic to this backend. |
| 3999 | 3999 | </DialogDescription> |
| 4000 | 4000 | </DialogHeader> |
apps/web/src/components/settings/IntegrationsSettings.tsx+1-1| ... | ... | @@ -577,7 +577,7 @@ function BrowserRecordingFrameRateSetting({ disabled }: { readonly disabled: boo |
| 577 | 577 | |
| 578 | 578 | const LINK_TARGET_LABELS: Readonly<Record<BrowserLinkTarget, string>> = { |
| 579 | 579 | system: "Your default browser", |
| 580 | app: "T3 Code", | |
| 580 | app: "The Knife", | |
| 581 | 581 | }; |
| 582 | 582 | |
| 583 | 583 | function BrowserLinkTargetSetting({ disabled }: { readonly disabled: boolean }) { |
apps/web/src/components/settings/KeybindingsSettings.tsx+1-1| ... | ... | @@ -257,7 +257,7 @@ function UnknownWhenVariableWarning({ |
| 257 | 257 | |
| 258 | 258 | return ( |
| 259 | 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 match | |
| 260 | The Knife does not recognize this condition yet. It can still be saved, but it may not match | |
| 261 | 261 | unless the runtime provides it. |
| 262 | 262 | </WarningTooltipIcon> |
| 263 | 263 | ); |
apps/web/src/components/settings/LocalEnvironmentSetting.tsx+3-3| ... | ... | @@ -43,7 +43,7 @@ export function LocalEnvironmentSetting() { |
| 43 | 43 | {...searchableSetting("local-environment")} |
| 44 | 44 | description={ |
| 45 | 45 | 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 | 47 | : "Turned off. Agents only run in remote environments." |
| 48 | 48 | } |
| 49 | 49 | control={ |
| ... | ... | @@ -70,8 +70,8 @@ export function LocalEnvironmentSetting() { |
| 70 | 70 | </AlertDialogTitle> |
| 71 | 71 | <AlertDialogDescription> |
| 72 | 72 | {enabled |
| 73 | ? "T3 Code will restart without running a server on this computer. Any agents and terminals running here will stop, and other devices will no longer be able to connect to this computer. Your projects, history, and remote environments are unaffected." | |
| 74 | : "T3 Code will restart and start running a server on this computer again."} | |
| 73 | ? "The Knife will restart without running a server on this computer. Any agents and terminals running here will stop, and other devices will no longer be able to connect to this computer. Your projects, history, and remote environments are unaffected." | |
| 74 | : "The Knife will restart and start running a server on this computer again."} | |
| 75 | 75 | </AlertDialogDescription> |
| 76 | 76 | </AlertDialogHeader> |
| 77 | 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 | 22 | {...searchableSetting("thread-notifications")} |
| 23 | 23 | description={ |
| 24 | 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 | 27 | control={ |
| 28 | 28 | <Select |
apps/web/src/components/settings/SettingsPanels.tsx+1-1| ... | ... | @@ -3422,7 +3422,7 @@ export function GeneralSettingsPanel() { |
| 3422 | 3422 | /> |
| 3423 | 3423 | <SettingsRow |
| 3424 | 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 | 3426 | control={ |
| 3427 | 3427 | <Button |
| 3428 | 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 | 263 | const shortcutStatus = recording |
| 264 | 264 | ? "Press your shortcut. Esc cancels." |
| 265 | 265 | : candidateConflict |
| 266 | ? `T3 Code already uses this for "${commandLabel(candidateConflict)}".` | |
| 266 | ? `The Knife already uses this for "${commandLabel(candidateConflict)}".` | |
| 267 | 267 | : shortcutCheck.status === "checking" |
| 268 | 268 | ? "Checking shortcut…" |
| 269 | 269 | : shortcutCheck.availability |
| ... | ... | @@ -315,7 +315,7 @@ export function SnapShotSettings() { |
| 315 | 315 | try { |
| 316 | 316 | if (state?.macPermissions) { |
| 317 | 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 | 319 | await bridge.setupSnapShot("test-mac-capture"); |
| 320 | 320 | } |
| 321 | 321 | if (state?.mode === "direct") |
apps/web/src/components/settings/SnapShotSetupDialog.tsx+2-2| ... | ... | @@ -174,7 +174,7 @@ export function SnapShotSetupDialog({ |
| 174 | 174 | ? "Update the capture helper" |
| 175 | 175 | : "Allow snapshots", |
| 176 | 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 | 179 | : backend === "niri" |
| 180 | 180 | ? { |
| ... | ... | @@ -355,7 +355,7 @@ export function SnapShotSetupDialog({ |
| 355 | 355 | </p> |
| 356 | 356 | ))} |
| 357 | 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 | 359 | ) : null} |
| 360 | 360 | {step === "access" && backend === "gnome" && extension?.status === "enabled" ? ( |
| 361 | 361 | <Button |
apps/web/src/components/settings/providerStatus.ts+1-1| ... | ... | @@ -44,7 +44,7 @@ export function getProviderSummary(provider: ServerProvider | undefined) { |
| 44 | 44 | return { |
| 45 | 45 | headline: "Disabled", |
| 46 | 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 | 50 | if (!provider.installed) { |
apps/web/src/components/settings/scopedSettings.ts+1-1| ... | ... | @@ -475,7 +475,7 @@ export async function persistScopedSettingsPatch( |
| 475 | 475 | return [ |
| 476 | 476 | { |
| 477 | 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 | 573 | <div |
| 574 | 574 | className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto" |
| 575 | 575 | data-settings-page-scroll |
| 576 | data-scroll-restoration-id="settings-page" | |
| 576 | 577 | > |
| 577 | 578 | <WorkspacePageContainer width={width} className={cn("gap-8", className)}> |
| 578 | 579 | <SettingsScopeSentence /> |
apps/web/src/components/sidebar/mainAppLocation.ts+5-4| ... | ... | @@ -14,8 +14,6 @@ export function isSidebarUtilityPage(pathname: string) { |
| 14 | 14 | ); |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | let mainAppHref: string | null = null; | |
| 18 | ||
| 19 | 17 | // Mount once in the app shell. Records the latest main app URL so Back can |
| 20 | 18 | // return there no matter how many utility pages were visited since. |
| 21 | 19 | export function MainAppLocationTracker() { |
| ... | ... | @@ -23,7 +21,7 @@ export function MainAppLocationTracker() { |
| 23 | 21 | select: (location) => (isSidebarUtilityPage(location.pathname) ? null : location.href), |
| 24 | 22 | }); |
| 25 | 23 | useEffect(() => { |
| 26 | if (href !== null) mainAppHref = href; | |
| 24 | if (href !== null) sessionStorage.setItem("t3code-main-app-href", href); | |
| 27 | 25 | }, [href]); |
| 28 | 26 | return null; |
| 29 | 27 | } |
| ... | ... | @@ -32,5 +30,8 @@ export function MainAppLocationTracker() { |
| 32 | 30 | // the app was opened directly on a utility page. |
| 33 | 31 | export function useNavigateToMainApp() { |
| 34 | 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 | 133 | stackedThreadToast({ |
| 134 | 134 | type: "error", |
| 135 | 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 | 139 | }, [notAppliedVersion]); |
apps/web/src/connection/clientMetadata.test.ts+1-1| ... | ... | @@ -80,7 +80,7 @@ describe("client telemetry metadata", () => { |
| 80 | 80 | desktopBridge: { getClientPlatform: () => "darwin" }, |
| 81 | 81 | }), |
| 82 | 82 | ).toEqual({ |
| 83 | label: "T3 Code Desktop", | |
| 83 | label: "The Knife Desktop", | |
| 84 | 84 | deviceType: "desktop", |
| 85 | 85 | os: "macOS", |
| 86 | 86 | surface: "desktop", |
apps/web/src/connection/clientMetadata.ts+2-2| ... | ... | @@ -77,7 +77,7 @@ export function clientPresentationMetadata(input: { |
| 77 | 77 | }): AuthClientPresentationMetadata { |
| 78 | 78 | if (input.desktopBridge !== undefined) { |
| 79 | 79 | return { |
| 80 | label: "T3 Code Desktop", | |
| 80 | label: "The Knife Desktop", | |
| 81 | 81 | deviceType: "desktop", |
| 82 | 82 | os: clientOsFromElectronPlatform(input.desktopBridge.getClientPlatform?.()), |
| 83 | 83 | surface: "desktop", |
| ... | ... | @@ -86,7 +86,7 @@ export function clientPresentationMetadata(input: { |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | 88 | return { |
| 89 | label: "T3 Code Web", | |
| 89 | label: "The Knife Web", | |
| 90 | 90 | deviceType: browserDeviceType(input.identity), |
| 91 | 91 | os: browserClientOs(input.identity), |
| 92 | 92 | surface: "web", |
apps/web/src/desktopAppActivation.ts+3-3| ... | ... | @@ -85,7 +85,7 @@ export async function handleDesktopAppActivationRequest( |
| 85 | 85 | return failure( |
| 86 | 86 | request.requestId, |
| 87 | 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 | 99 | return failure( |
| 100 | 100 | request.requestId, |
| 101 | 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 | 105 | return { |
| ... | ... | @@ -113,7 +113,7 @@ export async function handleDesktopAppActivationRequest( |
| 113 | 113 | return failure( |
| 114 | 114 | request.requestId, |
| 115 | 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 | 1 | /** Shows startup failures before React can replace the boot splash. */ |
| 2 | 2 | export function showBootError(error: unknown) { |
| 3 | console.error("T3 Code failed to start.", error); | |
| 3 | console.error("The Knife failed to start.", error); | |
| 4 | 4 | const bootShell = document.getElementById("boot-shell"); |
| 5 | 5 | if (!bootShell) return; |
| 6 | 6 | |
| ... | ... | @@ -9,7 +9,7 @@ export function showBootError(error: unknown) { |
| 9 | 9 | content.setAttribute("role", "alert"); |
| 10 | 10 | |
| 11 | 11 | const message = document.createElement("p"); |
| 12 | message.textContent = "T3 Code could not load."; | |
| 12 | message.textContent = "The Knife could not load."; | |
| 13 | 13 | content.append(message); |
| 14 | 14 | |
| 15 | 15 | if (import.meta.env.DEV && error instanceof Error) { |
apps/web/src/router.ts+2| ... | ... | @@ -1,12 +1,14 @@ |
| 1 | 1 | import { createRouter, RouterHistory } from "@tanstack/react-router"; |
| 2 | 2 | |
| 3 | 3 | import { routeTree } from "./routeTree.gen"; |
| 4 | import { isSidebarUtilityPage } from "./components/sidebar/mainAppLocation"; | |
| 4 | 5 | |
| 5 | 6 | export function getRouter(history: RouterHistory) { |
| 6 | 7 | return createRouter({ |
| 7 | 8 | routeTree, |
| 8 | 9 | history, |
| 9 | 10 | context: {}, |
| 11 | scrollRestoration: ({ location }) => isSidebarUtilityPage(location.pathname), | |
| 10 | 12 | // Route components are split chunks (autoCodeSplitting in vite.config); |
| 11 | 13 | // fetching them on hover/focus intent hides the load from the first |
| 12 | 14 | // settings or pull-request navigation. |
apps/web/src/routes/_chat.index.tsx+2-2| ... | ... | @@ -158,9 +158,9 @@ function HostedStaticOnboardingState() { |
| 158 | 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 | 159 | <LinkIcon className="size-5" /> |
| 160 | 160 | </div> |
| 161 | <EmptyTitle>Connect to a computer running T3 Code</EmptyTitle> | |
| 161 | <EmptyTitle>Connect to a computer running The Knife</EmptyTitle> | |
| 162 | 162 | <EmptyDescription> |
| 163 | This app connects to T3 Code running on your computer or a server. Start the T3 Code | |
| 163 | This app connects to The Knife running on your computer or a server. Start The Knife | |
| 164 | 164 | desktop app or command-line server on that machine and keep it running. |
| 165 | 165 | </EmptyDescription> |
| 166 | 166 | <EmptyDescription>{description}</EmptyDescription> |
apps/web/src/routes/_chat.pull-requests.tsx+1-1| ... | ... | @@ -1914,7 +1914,7 @@ function PullRequestsRouteView() { |
| 1914 | 1914 | ) : !pullRequestsSupported ? ( |
| 1915 | 1915 | <PullRequestsUnavailableState |
| 1916 | 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 | 1919 | ) : firstLoad ? ( |
| 1920 | 1920 | <PullRequestListGhost rows={7} /> |
apps/web/src/routes/_chat.tsx+1-1| ... | ... | @@ -153,7 +153,7 @@ function ChatRouteGlobalShortcuts() { |
| 153 | 153 | stackedThreadToast({ |
| 154 | 154 | type: "info", |
| 155 | 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 | 159 | return; |
apps/web/src/versionSkew.test.ts+1-1| ... | ... | @@ -24,7 +24,7 @@ import { |
| 24 | 24 | } from "./versionSkew"; |
| 25 | 25 | |
| 26 | 26 | const 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."; | |
| 28 | 28 | |
| 29 | 29 | describe("versionSkew", () => { |
| 30 | 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 | 87 | return { |
| 88 | 88 | clientVersion: normalizedClientVersion, |
| 89 | 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 | } |
| 93 | 93 |
assets/README.md+20-60| ... | ... | @@ -1,67 +1,27 @@ |
| 1 | # Brand icons | |
| 1 | # The Knife artwork | |
| 2 | 2 | |
| 3 | The 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 | |
| 4 | layer lives in `Assets/knife.png`; the project owns the obsidian backplate. All | |
| 5 | release channels use this one identity. The older T3 projects remain as upstream | |
| 6 | artwork references. | |
| 4 | 7 | |
| 5 | - `dev/app-icon.icon` | |
| 6 | - `nightly/app-icon.icon` | |
| 7 | - `prod/app-icon.icon` | |
| 8 | `vp run icons:export` regenerates the macOS, iOS, Linux, Windows, and web renditions, | |
| 9 | including browser public assets. `vp run icons:check` verifies those exports. | |
| 10 | Icon Composer 2 or newer is required; `ICON_COMPOSER_TOOL` can select its `ictool`. | |
| 8 | 11 | |
| 9 | Each 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 | |
| 10 | 13 | |
| 11 | Run `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. | |
| 14 | Icon Composer renders the macOS artwork with design generation 26. The exporter | |
| 15 | fits its full-bleed output to an 824×824 body on a transparent 1024×1024 canvas, | |
| 16 | inset 100px on every side. A black shadow uses 20% opacity, a 12px blur, and an | |
| 17 | 8px downward offset. Only the shadow extends outside the body's safe area. | |
| 12 | 18 | |
| 13 | Exporting 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 | |
| 20 | needed. Desktop packaging derives its ICNS and smaller resource PNG from it. | |
| 14 | 21 | |
| 15 | ## macOS exports | |
| 22 | ## Android | |
| 16 | 23 | |
| 17 | Icon Composer's command-line exporter does not expose the `macOS pre-Tahoe` preset. A plain command-line `macOS` export is full bleed and is not suitable for the desktop app, so the export script intentionally leaves the tracked macOS PNGs unchanged and prints a reminder after every run. | |
| 18 | ||
| 19 | After changing an Icon Composer project, open it in Icon Composer and export the macOS PNG with exactly these settings: | |
| 20 | ||
| 21 | - Platform: `macOS pre-Tahoe` | |
| 22 | - Appearance: `Default` | |
| 23 | - Size: `1024pt` | |
| 24 | - Scale: `1×` | |
| 25 | ||
| 26 | Save 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 | ||
| 32 | The 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 | ||
| 34 | To have Codex perform the native exports, paste this prompt into a task opened at the repository root: | |
| 35 | ||
| 36 | ```text | |
| 37 | Use [@Computer](plugin://computer-use@openai-bundled) and the Icon Composer app to export the three macOS app icons in this repository. | |
| 38 | ||
| 39 | For 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 | ||
| 45 | Do not resize, composite, or otherwise post-process the exported PNGs. | |
| 46 | ||
| 47 | 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. | |
| 48 | ``` | |
| 49 | ||
| 50 | Do not edit the generated PNG or ICO files directly. | |
| 51 | ||
| 52 | ## Android launcher and splash artwork | |
| 53 | ||
| 54 | Android masks the central 72dp of a 108dp adaptive canvas, and the Android 12+ splash screen masks | |
| 55 | the central two thirds of a 288dp canvas, so the Icon Composer exports cannot be used directly: | |
| 56 | their rounded-square silhouette gets framed again and the wordmark is cropped. The Android artwork | |
| 57 | is 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 | ||
| 66 | Rerun the export after changing a layer SVG. `android-icon-mark.png` remains a flat silhouette for | |
| 67 | Android's monochrome themed icon. | |
| 24 | `vp run icons:export:android` reads the same knife layer and backplate from the | |
| 25 | Icon Composer project. It creates `knife/android-foreground.png` and | |
| 26 | `knife/android-splash.png`, keeping the blade inside Android's circular safe | |
| 27 | area. The adaptive and monochrome icons share the foreground alpha silhouette. |
assets/knife/android-foreground.png created| Binary files /dev/null and b/assets/knife/android-foreground.png differ |
assets/knife/android-splash.png created| Binary files /dev/null and b/assets/knife/android-splash.png differ |
assets/knife/app-icon.icon/Assets/knife.png created| Binary files /dev/null and b/assets/knife/app-icon.icon/Assets/knife.png differ |
assets/knife/app-icon.icon/icon.json created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | { | |
| 2 | "fill": { | |
| 3 | "solid": "srgb:0.09020,0.07843,0.13333,1.00000" | |
| 4 | }, | |
| 5 | "groups": [ | |
| 6 | { | |
| 7 | "layers": [ | |
| 8 | { | |
| 9 | "image-name": "knife.png", | |
| 10 | "name": "Knife", | |
| 11 | "position": { | |
| 12 | "scale": 0.65, | |
| 13 | "translation-in-points": [ | |
| 14 | 0, | |
| 15 | 0 | |
| 16 | ] | |
| 17 | } | |
| 18 | } | |
| 19 | ], | |
| 20 | "shadow": { | |
| 21 | "kind": "neutral", | |
| 22 | "opacity": 0.3 | |
| 23 | }, | |
| 24 | "translucency": { | |
| 25 | "enabled": false, | |
| 26 | "value": 0.0 | |
| 27 | } | |
| 28 | } | |
| 29 | ], | |
| 30 | "supported-platforms": { | |
| 31 | "circles": [ | |
| 32 | "watchOS" | |
| 33 | ], | |
| 34 | "squares": "shared" | |
| 35 | } | |
| 36 | } |
assets/knife/apple-touch-180.png created| Binary files /dev/null and b/assets/knife/apple-touch-180.png differ |
assets/knife/favicon-16x16.png created| Binary files /dev/null and b/assets/knife/favicon-16x16.png differ |
assets/knife/favicon-32x32.png created| Binary files /dev/null and b/assets/knife/favicon-32x32.png differ |
assets/knife/icon-1024.png created| Binary files /dev/null and b/assets/knife/icon-1024.png differ |
assets/knife/icon.ico created| Binary files /dev/null and b/assets/knife/icon.ico differ |
assets/knife/macos-1024.png created| Binary files /dev/null and b/assets/knife/macos-1024.png differ |
docs/operations/development.md+8| ... | ... | @@ -206,3 +206,11 @@ passed as `--wsl-runtime`; see the |
| 206 | 206 | Add `--signed` after configuring the platform credentials in the |
| 207 | 207 | [release runbook](./release.md). macOS passkeys need a signed, provisioned app; follow the |
| 208 | 208 | [Connect setup](./connect-setup.md#desktop-passkeys) for local signing and renderer HMR. |
| 209 | ||
| 210 | ### Private fork releases for Apple Silicon | |
| 211 | ||
| 212 | Licensed Name Sans and Name Mono files live in the ignored | |
| 213 | `apps/web/public/fonts/private/` directory. Copy that directory into any other | |
| 214 | build workspace before building with these fonts. The web and desktop builds | |
| 215 | bundle the local files automatically; source checkouts without them use system | |
| 216 | fallback 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 | 16 | |
| 17 | 17 | int main(int argc, char **argv) { |
| 18 | 18 | if (argc != 2 || argv[1][0] == '\0') return 64; |
| 19 | g_set_application_name("T3 Code"); | |
| 19 | g_set_application_name("The Knife"); | |
| 20 | 20 | |
| 21 | 21 | GHashTable *attributes = secret_attributes_build(&chromium_schema, "application", argv[1], NULL); |
| 22 | 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 | 139 | .filter(|w| w.pid == pid && w.title == title && w.mapped && !w.hidden); |
| 140 | 140 | let found = matches.next(); |
| 141 | 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 | 144 | Ok(found) |
| 145 | 145 | } |
| ... | ... | @@ -157,14 +157,14 @@ pub fn activate(pid: u32, title: &str) -> Result<()> { |
| 157 | 157 | if request(&lua)?.trim() != "ok" |
| 158 | 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 | 162 | return Ok(()); |
| 163 | 163 | } |
| 164 | 164 | events.get_ref().set_read_timeout(Some( |
| 165 | 165 | deadline |
| 166 | 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 | 169 | let mut event = Vec::new(); |
| 170 | 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 | 348 | ), |
| 349 | 349 | )?; |
| 350 | 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 | 353 | println!("{{\"activated\":true}}"); |
| 354 | 354 | } |
packages/client-runtime/src/connection/compatibility.ts+1-1| ... | ... | @@ -21,7 +21,7 @@ export function orchestrationProtocolCompatibilityError( |
| 21 | 21 | }) |
| 22 | 22 | : new ConnectionBlockedError({ |
| 23 | 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 | 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 | 77 | ) { |
| 78 | 78 | return yield* new OutdatedHostUpdateError({ |
| 79 | 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 | } |
| 83 | 83 | |
| ... | ... | @@ -189,7 +189,7 @@ export const updateOutdatedHost = Effect.fn("clientRuntime.connection.updateOutd |
| 189 | 189 | if (Option.isNone(resumed)) { |
| 190 | 190 | return yield* new OutdatedHostUpdateError({ |
| 191 | 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 | } |
| 195 | 195 |
packages/client-runtime/src/connection/resolver.test.ts+1-1| ... | ... | @@ -200,7 +200,7 @@ describe("ConnectionResolver", () => { |
| 200 | 200 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); |
| 201 | 201 | |
| 202 | 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 | ); |
| 206 | 206 |
packages/client-runtime/src/relay/errorPresentation.ts+1-1| ... | ... | @@ -95,7 +95,7 @@ export function relayRequestFailureDetail(error: { |
| 95 | 95 | // coming back, which clears this reason. While it is still reported, the host |
| 96 | 96 | // is either still off or running a build too old to do that. |
| 97 | 97 | export 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."; | |
| 99 | 99 | |
| 100 | 100 | /** User-facing text for an offline status, or null when the relay gave no known reason. */ |
| 101 | 101 | export function relayOfflineReasonMessage( |
packages/client-runtime/src/state/pullRequestDiffHttp.test.ts+1-1| ... | ... | @@ -112,7 +112,7 @@ describe("fetchEnvironmentPullRequestDiff", () => { |
| 112 | 112 | traceId: "trace-auth-test", |
| 113 | 113 | }); |
| 114 | 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 | 28 | }, |
| 29 | 29 | ) { |
| 30 | 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 | } |
| 34 | 34 |
scripts/build-desktop-artifact.test.ts+7-7| ... | ... | @@ -273,8 +273,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 273 | 273 | }); |
| 274 | 274 | |
| 275 | 275 | it("switches desktop packaging product names to nightly for nightly builds", () => { |
| 276 | assert.equal(resolveDesktopProductName("0.0.17"), "T3 Code (Alpha)"); | |
| 277 | assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)"); | |
| 276 | assert.equal(resolveDesktopProductName("0.0.17"), "The Knife"); | |
| 277 | assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "The Knife (Nightly)"); | |
| 278 | 278 | }); |
| 279 | 279 | |
| 280 | 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 | 702 | "**/*.map", |
| 703 | 703 | ]); |
| 704 | 704 | assert.deepStrictEqual(mac.dmg, { |
| 705 | title: "T3 Code (Alpha) 1.2.3 Installer", | |
| 705 | title: "The Knife 1.2.3 Installer", | |
| 706 | 706 | background: "dmg/dmg-background-latest.png", |
| 707 | 707 | window: { width: 640, height: 432 }, |
| 708 | 708 | contents: [ |
| ... | ... | @@ -717,11 +717,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 717 | 717 | // Linux must register the renderer schemes so the generated .desktop |
| 718 | 718 | // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links. |
| 719 | 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 | 722 | // macOS also offers itself as a web browser, so it can be chosen as the default. |
| 723 | 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 | 725 | { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" }, |
| 726 | 726 | ]); |
| 727 | 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 | 746 | assert.propertyVal( |
| 747 | 747 | (mac.mac as Record<string, unknown>).extendInfo as Record<string, unknown>, |
| 748 | 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 | 751 | for (const config of [linux, win]) { |
| 752 | 752 | assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES); |
| ... | ... | @@ -2141,7 +2141,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 2141 | 2141 | assert.equal(mac.provisioningProfile, "/tmp/t3code.provisionprofile"); |
| 2142 | 2142 | assert.match(String(mac.sign), /[\\/]scripts[\\/]sign-macos\.ts$/); |
| 2143 | 2143 | assert.deepStrictEqual(mac.protocols, [ |
| 2144 | { name: "T3 Code", schemes: ["t3code", "t3code-dev"] }, | |
| 2144 | { name: "The Knife", schemes: ["t3code", "t3code-dev"] }, | |
| 2145 | 2145 | { name: "Web site URL", schemes: ["http", "https"], role: "Viewer" }, |
| 2146 | 2146 | ]); |
| 2147 | 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 | 971 | "!**/node_modules/@cursor/sdk-*/**/*", |
| 972 | 972 | "!apps/desktop/prod-resources/cursor-sdk", |
| 973 | 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 | 975 | // so the SDK's optional platform packages (each a ~200MB bundled executable) |
| 976 | 976 | // are dead weight. The trailing dash keeps the SDK's own JS package. |
| 977 | 977 | "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*", |
| ... | ... | @@ -2745,8 +2745,8 @@ export function resolvePackageManagerUserAgent(packageManager: string): string { |
| 2745 | 2745 | |
| 2746 | 2746 | export function resolveDesktopProductName(version: string): string { |
| 2747 | 2747 | return resolveDesktopUpdateChannel(version) === "nightly" |
| 2748 | ? "T3 Code (Nightly)" | |
| 2749 | : (desktopPackageJson.productName ?? "T3 Code"); | |
| 2748 | ? "The Knife (Nightly)" | |
| 2749 | : (desktopPackageJson.productName ?? "The Knife"); | |
| 2750 | 2750 | } |
| 2751 | 2751 | |
| 2752 | 2752 | export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| ... | ... | @@ -2771,7 +2771,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2771 | 2771 | const buildConfig: Record<string, unknown> = { |
| 2772 | 2772 | appId: DESKTOP_APP_ID, |
| 2773 | 2773 | productName: resolveDesktopProductName(version), |
| 2774 | artifactName: "T3-Code-${version}-${arch}.${ext}", | |
| 2774 | artifactName: "The-Knife-${version}-${arch}.${ext}", | |
| 2775 | 2775 | electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES], |
| 2776 | 2776 | files: [ |
| 2777 | 2777 | ...DESKTOP_FILE_EXCLUSIONS, |
| ... | ... | @@ -2824,9 +2824,9 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2824 | 2824 | // macOS 26 offers Messages/Mail codes in every text field unless opted out. |
| 2825 | 2825 | NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac: true, |
| 2826 | 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 | 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 | 2830 | // macOS lists an app under Default web browser only when it opens web |
| 2831 | 2831 | // pages as documents as well as http and https links (see protocols). |
| 2832 | 2832 | CFBundleDocumentTypes: [ |
| ... | ... | @@ -2840,10 +2840,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2840 | 2840 | }, |
| 2841 | 2841 | protocols: [ |
| 2842 | 2842 | { |
| 2843 | name: "T3 Code", | |
| 2843 | name: "The Knife", | |
| 2844 | 2844 | schemes: ["t3code", "t3code-dev"], |
| 2845 | 2845 | }, |
| 2846 | // Lets people choose T3 Code as their default web browser, which opens | |
| 2846 | // Lets people choose The Knife as their default web browser, which opens | |
| 2847 | 2847 | // each link in a new thread's browser panel. |
| 2848 | 2848 | { |
| 2849 | 2849 | name: "Web site URL", |
| ... | ... | @@ -2907,7 +2907,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2907 | 2907 | // t3code:// OAuth callbacks to the app. |
| 2908 | 2908 | protocols: [ |
| 2909 | 2909 | { |
| 2910 | name: "T3 Code", | |
| 2910 | name: "The Knife", | |
| 2911 | 2911 | schemes: ["t3code", "t3code-dev"], |
| 2912 | 2912 | }, |
| 2913 | 2913 | ], |
| ... | ... | @@ -3869,7 +3869,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 3869 | 3869 | private: true, |
| 3870 | 3870 | packageManager: rootPackageJson.packageManager, |
| 3871 | 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 | 3873 | license: "MIT", |
| 3874 | 3874 | // Required by the .deb control file. |
| 3875 | 3875 | homepage: "https://t3.codes", |
| ... | ... | @@ -4158,7 +4158,7 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { |
| 4158 | 4158 | Flag.optional, |
| 4159 | 4159 | ), |
| 4160 | 4160 | }).pipe( |
| 4161 | Command.withDescription("Build a desktop artifact for T3 Code."), | |
| 4161 | Command.withDescription("Build a desktop artifact for The Knife."), | |
| 4162 | 4162 | Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)), |
| 4163 | 4163 | ); |
| 4164 | 4164 |
scripts/export-android-icons.ts+45-207| ... | ... | @@ -1,18 +1,5 @@ |
| 1 | 1 | #!/usr/bin/env node |
| 2 | 2 | |
| 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 | ||
| 16 | 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 17 | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 18 | 5 | import * as Console from "effect/Console"; |
| ... | ... | @@ -22,211 +9,62 @@ import * as Path from "effect/Path"; |
| 22 | 9 | import * as Schema from "effect/Schema"; |
| 23 | 10 | import sharp from "sharp"; |
| 24 | 11 | |
| 25 | type IconVariant = "dev" | "nightly" | "prod"; | |
| 26 | ||
| 27 | // 108dp at xxxhdpi. Expo's prebuild derives every launcher density bucket from this. | |
| 28 | const ADAPTIVE_CANVAS = 432; | |
| 29 | // 288dp at xxxhdpi: the full Android 12+ splash canvas, so the icon needs no upscaling. | |
| 30 | const SPLASH_CANVAS = 1152; | |
| 31 | // Icon Composer's layer sources use a 128pt viewBox; the wordmark path spans this box. | |
| 32 | const 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. | |
| 36 | const WORDMARK_FRACTION = 0.48; | |
| 37 | // Icon Composer positions layers on a 1024pt canvas, with translation relative to center. | |
| 38 | const COMPOSER_CANVAS_PT = 1024; | |
| 39 | const SVG_DENSITY = 300; | |
| 40 | const OUTPUT_DIRECTORY = "apps/mobile/assets"; | |
| 41 | // Production has no background artwork, so its splash composes onto the adaptive color. | |
| 42 | const PRODUCTION_BACKGROUND_COLOR = "#000000"; | |
| 12 | import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; | |
| 43 | 13 | |
| 44 | 14 | export class AndroidIconRenderError extends Schema.TaggedError<AndroidIconRenderError>()( |
| 45 | 15 | "AndroidIconRenderError", |
| 46 | { layer: Schema.String, cause: Schema.Defect() }, | |
| 16 | { cause: Schema.Defect() }, | |
| 47 | 17 | ) {} |
| 48 | 18 | |
| 49 | const wordmarkTransform = (size: number) => { | |
| 50 | const scale = (size * WORDMARK_FRACTION) / TEXT.width; | |
| 51 | const tx = (size - TEXT.width * scale) / 2 - TEXT.x * scale; | |
| 52 | const ty = (size - TEXT.height * scale) / 2 - TEXT.y * scale; | |
| 53 | return `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) scale(${scale.toFixed(4)})`; | |
| 54 | }; | |
| 55 | ||
| 56 | const 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. | |
| 61 | const fullBleed = (svg: string) => | |
| 62 | svg.replace(/<rect width="128" height="128" rx="10"\/>/, '<rect width="128" height="128"/>'); | |
| 63 | ||
| 64 | const 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 | ||
| 71 | const 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 | ||
| 85 | const 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 | ||
| 94 | const 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 | ||
| 106 | const 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 | ||
| 119 | const 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 | }, | |
| 19 | const decodeProject = Schema.decodeEffect( | |
| 20 | Schema.fromJsonString(Schema.Struct({ fill: Schema.Struct({ solid: Schema.String }) })), | |
| 21 | ); | |
| 22 | const decodeColor = Schema.decodeUnknownEffect( | |
| 23 | Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]), | |
| 135 | 24 | ); |
| 136 | ||
| 137 | const 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 | ||
| 185 | const 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 | ||
| 200 | const 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 | }); | |
| 208 | 25 | |
| 209 | 26 | const exportAndroidIcons = Effect.gen(function* () { |
| 210 | 27 | const fs = yield* FileSystem.FileSystem; |
| 211 | 28 | const path = yield* Path.Path; |
| 212 | 29 | const repositoryRoot = path.resolve(import.meta.dirname, ".."); |
| 213 | const outputs = [ | |
| 214 | ["android-icon-foreground.png", yield* renderForeground(repositoryRoot, ADAPTIVE_CANVAS)], | |
| 215 | [ | |
| 216 | "android-icon-background-dev.png", | |
| 217 | yield* renderDevelopmentBackground(repositoryRoot, ADAPTIVE_CANVAS), | |
| 218 | ], | |
| 219 | [ | |
| 220 | "android-icon-background-nightly.png", | |
| 221 | yield* renderNightlyBackground(repositoryRoot, ADAPTIVE_CANVAS), | |
| 222 | ], | |
| 223 | ["android-splash-icon-dev.png", yield* renderSplashIcon(repositoryRoot, "dev")], | |
| 224 | ["android-splash-icon-nightly.png", yield* renderSplashIcon(repositoryRoot, "nightly")], | |
| 225 | ["android-splash-icon-prod.png", yield* renderSplashIcon(repositoryRoot, "prod")], | |
| 226 | ] as const; | |
| 227 | for (const [name, contents] of outputs) { | |
| 228 | yield* fs.writeFile(path.join(repositoryRoot, OUTPUT_DIRECTORY, name), contents); | |
| 229 | yield* Console.log(`wrote ${OUTPUT_DIRECTORY}/${name}`); | |
| 30 | const projectRoot = path.join(repositoryRoot, BRAND_ASSET_PATHS.productionIconComposerProject); | |
| 31 | const project = yield* decodeProject( | |
| 32 | yield* fs.readFileString(path.join(projectRoot, "icon.json")), | |
| 33 | ); | |
| 34 | const [r, g, b, alpha] = yield* decodeColor( | |
| 35 | project.fill.solid.slice("srgb:".length).split(",").map(Number), | |
| 36 | ); | |
| 37 | const background = { r: r * 255, g: g * 255, b: b * 255, alpha }; | |
| 38 | const source = yield* fs.readFile(path.join(projectRoot, "Assets", "knife.png")); | |
| 39 | ||
| 40 | for (const [name, size] of [ | |
| 41 | ["android-foreground.png", 432], | |
| 42 | ["android-splash.png", 1152], | |
| 43 | ] as const) { | |
| 44 | const png = yield* Effect.tryPromise({ | |
| 45 | try: async () => { | |
| 46 | // Android masks the central two thirds; the blade must stay inside that circle. | |
| 47 | const foreground = await sharp(source) | |
| 48 | .resize(Math.round(size * 0.57), Math.round(size * 0.57), { fit: "inside" }) | |
| 49 | .png() | |
| 50 | .toBuffer(); | |
| 51 | return sharp({ | |
| 52 | create: { | |
| 53 | width: size, | |
| 54 | height: size, | |
| 55 | channels: 4, | |
| 56 | background: name === "android-foreground.png" ? "transparent" : background, | |
| 57 | }, | |
| 58 | }) | |
| 59 | .composite([{ input: foreground, gravity: "centre" }]) | |
| 60 | .png() | |
| 61 | .toBuffer(); | |
| 62 | }, | |
| 63 | catch: (cause) => new AndroidIconRenderError({ cause }), | |
| 64 | }); | |
| 65 | const output = path.join(repositoryRoot, "assets", "knife", name); | |
| 66 | yield* fs.writeFile(output, png); | |
| 67 | yield* Console.log(`wrote ${output}`); | |
| 230 | 68 | } |
| 231 | 69 | }); |
| 232 | 70 |
scripts/export-brand-icons.ts+35-63| ... | ... | @@ -12,6 +12,7 @@ import * as Schema from "effect/Schema"; |
| 12 | 12 | import * as Stream from "effect/Stream"; |
| 13 | 13 | import { Command, Flag } from "effect/cli"; |
| 14 | 14 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 15 | import sharp from "sharp"; | |
| 15 | 16 | |
| 16 | 17 | import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts"; |
| 17 | 18 | import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts"; |
| ... | ... | @@ -41,7 +42,7 @@ const decodeIconComposerVersion = Schema.decodeUnknownEffect( |
| 41 | 42 | Schema.fromJsonString(IconComposerVersion), |
| 42 | 43 | ); |
| 43 | 44 | |
| 44 | type IconPlatform = "iOS"; | |
| 45 | type IconPlatform = "iOS" | "macOS"; | |
| 45 | 46 | |
| 46 | 47 | interface VariantOutputs { |
| 47 | 48 | readonly ios: string; |
| ... | ... | @@ -204,7 +205,7 @@ export class IconExportAssetsStaleError extends Schema.TaggedError<IconExportAss |
| 204 | 205 | |
| 205 | 206 | const ICON_VARIANTS = [ |
| 206 | 207 | { |
| 207 | label: "development", | |
| 208 | label: "knife", | |
| 208 | 209 | source: BRAND_ASSET_PATHS.developmentIconComposerProject, |
| 209 | 210 | outputs: { |
| 210 | 211 | ios: BRAND_ASSET_PATHS.developmentIosIconPng, |
| ... | ... | @@ -217,44 +218,8 @@ const ICON_VARIANTS = [ |
| 217 | 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 | 221 | ] as const satisfies ReadonlyArray<IconVariant>; |
| 249 | 222 | |
| 250 | const 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 | ||
| 258 | 223 | const RepositoryRoot = Effect.service(Path.Path).pipe( |
| 259 | 224 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 260 | 225 | Effect.mapError( |
| ... | ... | @@ -586,6 +551,36 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* ( |
| 586 | 551 | }); |
| 587 | 552 | |
| 588 | 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 | 584 | const icoRenditions = yield* Effect.forEach( |
| 590 | 585 | WINDOWS_ICON_SIZES, |
| 591 | 586 | (size) => render("iOS", size).pipe(Effect.map((contents) => ({ size, contents }))), |
| ... | ... | @@ -598,6 +593,7 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* ( |
| 598 | 593 | |
| 599 | 594 | return new Map<string, Buffer>([ |
| 600 | 595 | [variant.outputs.ios, ios], |
| 596 | [variant.outputs.macos, macos], | |
| 601 | 597 | [variant.outputs.universal, ios], |
| 602 | 598 | [variant.outputs.appleTouch, yield* render("iOS", 180)], |
| 603 | 599 | [variant.outputs.favicon16, yield* render("iOS", 16)], |
| ... | ... | @@ -607,24 +603,6 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* ( |
| 607 | 603 | ]); |
| 608 | 604 | }); |
| 609 | 605 | |
| 610 | const 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 | ||
| 628 | 606 | const writeAtomically = Effect.fn("iconExport.writeAtomically")(function* ( |
| 629 | 607 | repositoryRoot: string, |
| 630 | 608 | relativePath: string, |
| ... | ... | @@ -774,7 +752,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn |
| 774 | 752 | }); |
| 775 | 753 | } |
| 776 | 754 | yield* Console.log(`All ${generated.size} generated icon assets are current.`); |
| 777 | yield* logManualMacOsExportInstructions(); | |
| 778 | 755 | return; |
| 779 | 756 | } |
| 780 | 757 | |
| ... | ... | @@ -784,7 +761,6 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn |
| 784 | 761 | { concurrency: 1, discard: true }, |
| 785 | 762 | ); |
| 786 | 763 | yield* Console.log(`Updated ${generated.size} generated icon assets.`); |
| 787 | yield* logManualMacOsExportInstructions(); | |
| 788 | 764 | }); |
| 789 | 765 | |
| 790 | 766 | export const exportBrandIconsCommand = Command.make( |
| ... | ... | @@ -796,11 +772,7 @@ export const exportBrandIconsCommand = Command.make( |
| 796 | 772 | ), |
| 797 | 773 | }, |
| 798 | 774 | ({ check }) => exportBrandIcons(check).pipe(Effect.scoped), |
| 799 | ).pipe( | |
| 800 | Command.withDescription( | |
| 801 | "Export development, preview, and production assets from Icon Composer projects.", | |
| 802 | ), | |
| 803 | ); | |
| 775 | ).pipe(Command.withDescription("Export The Knife assets from its Icon Composer project.")); | |
| 804 | 776 | |
| 805 | 777 | if (import.meta.main) { |
| 806 | 778 | Command.run(exportBrandIconsCommand, { version: "0.0.0" }).pipe( |
scripts/lib/brand-assets.test.ts-15| ... | ... | @@ -83,19 +83,4 @@ describe("brand-assets", () => { |
| 83 | 83 | expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly"); |
| 84 | 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 | 1 | export const BRAND_ASSET_PATHS = { |
| 2 | developmentIconComposerProject: "assets/dev/app-icon.icon", | |
| 3 | developmentIosIconPng: "assets/dev/blueprint-ios-1024.png", | |
| 4 | developmentUniversalIconPng: "assets/dev/blueprint-universal-1024.png", | |
| 5 | ||
| 6 | productionIconComposerProject: "assets/prod/app-icon.icon", | |
| 7 | productionIosIconPng: "assets/prod/black-ios-1024.png", | |
| 8 | productionMacIconPng: "assets/prod/black-macos-1024.png", | |
| 9 | productionLinuxIconPng: "assets/prod/black-universal-1024.png", | |
| 10 | productionWindowsIconIco: "assets/prod/t3-black-windows.ico", | |
| 11 | productionWebFaviconIco: "assets/prod/t3-black-web-favicon.ico", | |
| 12 | productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png", | |
| 13 | productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png", | |
| 14 | productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png", | |
| 15 | ||
| 16 | nightlyIconComposerProject: "assets/nightly/app-icon.icon", | |
| 17 | nightlyIosIconPng: "assets/nightly/nightly-ios-1024.png", | |
| 18 | nightlyMacIconPng: "assets/nightly/nightly-macos-1024.png", | |
| 19 | nightlyLinuxIconPng: "assets/nightly/nightly-universal-1024.png", | |
| 20 | nightlyWindowsIconIco: "assets/nightly/nightly-windows.ico", | |
| 21 | nightlyWebFaviconIco: "assets/nightly/nightly-web-favicon.ico", | |
| 22 | nightlyWebFavicon16Png: "assets/nightly/nightly-web-favicon-16x16.png", | |
| 23 | nightlyWebFavicon32Png: "assets/nightly/nightly-web-favicon-32x32.png", | |
| 24 | nightlyWebAppleTouchIconPng: "assets/nightly/nightly-web-apple-touch-180.png", | |
| 25 | ||
| 26 | developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png", | |
| 27 | developmentWindowsIconIco: "assets/dev/blueprint-windows.ico", | |
| 28 | developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico", | |
| 29 | developmentWebFavicon16Png: "assets/dev/blueprint-web-favicon-16x16.png", | |
| 30 | developmentWebFavicon32Png: "assets/dev/blueprint-web-favicon-32x32.png", | |
| 31 | developmentWebAppleTouchIconPng: "assets/dev/blueprint-web-apple-touch-180.png", | |
| 2 | developmentIconComposerProject: "assets/knife/app-icon.icon", | |
| 3 | developmentIosIconPng: "assets/knife/icon-1024.png", | |
| 4 | developmentUniversalIconPng: "assets/knife/icon-1024.png", | |
| 5 | productionIconComposerProject: "assets/knife/app-icon.icon", | |
| 6 | productionIosIconPng: "assets/knife/icon-1024.png", | |
| 7 | productionMacIconPng: "assets/knife/macos-1024.png", | |
| 8 | productionLinuxIconPng: "assets/knife/icon-1024.png", | |
| 9 | productionWindowsIconIco: "assets/knife/icon.ico", | |
| 10 | productionWebFaviconIco: "assets/knife/icon.ico", | |
| 11 | productionWebFavicon16Png: "assets/knife/favicon-16x16.png", | |
| 12 | productionWebFavicon32Png: "assets/knife/favicon-32x32.png", | |
| 13 | productionWebAppleTouchIconPng: "assets/knife/apple-touch-180.png", | |
| 14 | nightlyIconComposerProject: "assets/knife/app-icon.icon", | |
| 15 | nightlyIosIconPng: "assets/knife/icon-1024.png", | |
| 16 | nightlyMacIconPng: "assets/knife/macos-1024.png", | |
| 17 | nightlyLinuxIconPng: "assets/knife/icon-1024.png", | |
| 18 | nightlyWindowsIconIco: "assets/knife/icon.ico", | |
| 19 | nightlyWebFaviconIco: "assets/knife/icon.ico", | |
| 20 | nightlyWebFavicon16Png: "assets/knife/favicon-16x16.png", | |
| 21 | nightlyWebFavicon32Png: "assets/knife/favicon-32x32.png", | |
| 22 | nightlyWebAppleTouchIconPng: "assets/knife/apple-touch-180.png", | |
| 23 | developmentDesktopIconPng: "assets/knife/macos-1024.png", | |
| 24 | developmentWindowsIconIco: "assets/knife/icon.ico", | |
| 25 | developmentWebFaviconIco: "assets/knife/icon.ico", | |
| 26 | developmentWebFavicon16Png: "assets/knife/favicon-16x16.png", | |
| 27 | developmentWebFavicon32Png: "assets/knife/favicon-32x32.png", | |
| 28 | developmentWebAppleTouchIconPng: "assets/knife/apple-touch-180.png", | |
| 32 | 29 | } as const; |
| 33 | 30 | |
| 34 | 31 | export type WebAssetBrand = "development" | "nightly" | "production"; |
scripts/mobile-native-client.ts+3-3| ... | ... | @@ -348,9 +348,9 @@ const main = Command.make( |
| 348 | 348 | [ |
| 349 | 349 | "xcodebuild", |
| 350 | 350 | "-workspace", |
| 351 | path.join(mobile, "ios/T3CodeDev.xcworkspace"), | |
| 351 | path.join(mobile, "ios/TheKnifeDev.xcworkspace"), | |
| 352 | 352 | "-scheme", |
| 353 | "T3CodeDev", | |
| 353 | "TheKnifeDev", | |
| 354 | 354 | "-configuration", |
| 355 | 355 | "Debug", |
| 356 | 356 | "-destination", |
| ... | ... | @@ -367,7 +367,7 @@ const main = Command.make( |
| 367 | 367 | "simctl", |
| 368 | 368 | "install", |
| 369 | 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 | 372 | true, |
| 373 | 373 | ); |
scripts/mobile-showcase.ts+3-3| ... | ... | @@ -42,7 +42,7 @@ const SERVER_HOST = "0.0.0.0"; |
| 42 | 42 | const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64"; |
| 43 | 43 | const IOS_APP_PATH = NodePath.join( |
| 44 | 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 | ); |
| 47 | 47 | const ANDROID_APK_PATH = NodePath.join( |
| 48 | 48 | MOBILE_ROOT, |
| ... | ... | @@ -707,9 +707,9 @@ async function buildIos(): Promise<string> { |
| 707 | 707 | "xcodebuild", |
| 708 | 708 | [ |
| 709 | 709 | "-workspace", |
| 710 | NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"), | |
| 710 | NodePath.join(MOBILE_ROOT, "ios/TheKnife.xcworkspace"), | |
| 711 | 711 | "-scheme", |
| 712 | "T3Code", | |
| 712 | "TheKnife", | |
| 713 | 713 | "-configuration", |
| 714 | 714 | "Debug", |
| 715 | 715 | "-sdk", |