diff --git a/.agents/skills/ios-debugger-agent/SKILL.md b/.agents/skills/ios-debugger-agent/SKILL.md index 7afa579383d9..b0290309a93e 100644 --- a/.agents/skills/ios-debugger-agent/SKILL.md +++ b/.agents/skills/ios-debugger-agent/SKILL.md @@ -1,6 +1,6 @@ --- name: ios-debugger-agent -description: Build, launch, inspect, and drive iOS apps with the repository-configured XcodeBuildMCP server. Use on macOS for iOS Simulator builds, focused native test runs, semantic UI automation, screenshots, logs, or debugging, including T3 Code Mobile verification. +description: Build, launch, inspect, and drive iOS apps with the repository-configured XcodeBuildMCP server. Use on macOS for iOS Simulator builds, focused native test runs, semantic UI automation, screenshots, logs, or debugging, including Phoenix Mobile verification. --- # iOS Debugger Agent diff --git a/.agents/skills/ios-simulator-browser/SKILL.md b/.agents/skills/ios-simulator-browser/SKILL.md index 74b97530799c..f58c49d68ae7 100644 --- a/.agents/skills/ios-simulator-browser/SKILL.md +++ b/.agents/skills/ios-simulator-browser/SKILL.md @@ -1,6 +1,6 @@ --- name: ios-simulator-browser -description: Stream an explicit iOS Simulator through pinned serve-sim into the T3 Code in-app browser or another agent browser. Use on Apple Silicon macOS when the user should watch simulator verification live or when browser-visible simulator evidence is needed. +description: Stream an explicit iOS Simulator through pinned serve-sim into the Phoenix in-app browser or another agent browser. Use on Apple Silicon macOS when the user should watch simulator verification live or when browser-visible simulator evidence is needed. --- # iOS Simulator Browser @@ -11,7 +11,7 @@ Use serve-sim as the shared visual feed for an iOS Simulator. Use `ios-debugger- serve-sim `0.1.45` requires Apple Silicon macOS, Xcode command-line tools, and Node.js 20 or newer. If the host is unsupported, continue with XcodeBuildMCP screenshots and report that live streaming was unavailable. -When running inside T3 Code, use its product-native browser MCP to open the stream. Other agent hosts may use their own browser or preview surface. +When running inside Phoenix, use its product-native browser MCP to open the stream. Other agent hosts may use their own browser or preview surface. Keep serve-sim on its default `127.0.0.1` binding. Do not expose its preview to a LAN or tunnel unless the user explicitly requests that access and the network is trusted; the preview includes a token-gated shell-execution route. diff --git a/.agents/skills/ios-simulator-browser/agents/openai.yaml b/.agents/skills/ios-simulator-browser/agents/openai.yaml index 9e8cdf7a16b0..4465a26c330f 100644 --- a/.agents/skills/ios-simulator-browser/agents/openai.yaml +++ b/.agents/skills/ios-simulator-browser/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "iOS Simulator Browser" short_description: "Stream iOS Simulator in the browser" - default_prompt: "Use $ios-simulator-browser to stream the selected iOS Simulator into the T3 Code in-app browser." + default_prompt: "Use $ios-simulator-browser to stream the selected iOS Simulator into the Phoenix in-app browser." diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md index 0e11b50e1c83..1a363db04ce7 100644 --- a/.agents/skills/test-t3-app/SKILL.md +++ b/.agents/skills/test-t3-app/SKILL.md @@ -1,6 +1,6 @@ --- name: test-t3-app -description: Launch, retain, and test the T3 Code web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, cross-turn dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, iteratively test UI behavior with a human, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite. +description: Launch, retain, and test the Phoenix web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, cross-turn dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, iteratively test UI behavior with a human, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite. --- # Test T3 App @@ -28,7 +28,7 @@ The dev runner disables browser auto-open by default. Do not pass `--browser` du ### Verify a shared environment before human handoff -When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request. +When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the Phoenix app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request. Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person. diff --git a/.agents/skills/test-t3-mobile/SKILL.md b/.agents/skills/test-t3-mobile/SKILL.md index 3fcf94334fd6..db2cf19b1470 100644 --- a/.agents/skills/test-t3-mobile/SKILL.md +++ b/.agents/skills/test-t3-mobile/SKILL.md @@ -1,6 +1,6 @@ --- name: test-t3-mobile -description: Launch and test T3 Code Mobile on an iOS Simulator or Android Emulator against disposable local T3 environments, including Metro and dev-client reuse, native rebuild decisions, per-client pairing, seeded projects, semantic UI control, screenshots, and iOS serve-sim streaming. Use after mobile UI or native changes, when reproducing phone or tablet behavior, pairing an emulator to isolated state, or verifying mobile behavior on macOS, Linux, or Windows. +description: Launch and test Phoenix Mobile on an iOS Simulator or Android Emulator against disposable local T3 environments, including Metro and dev-client reuse, native rebuild decisions, per-client pairing, seeded projects, semantic UI control, screenshots, and iOS serve-sim streaming. Use after mobile UI or native changes, when reproducing phone or tablet behavior, pairing an emulator to isolated state, or verifying mobile behavior on macOS, Linux, or Windows. --- # Test T3 Mobile @@ -28,15 +28,15 @@ Do not treat unavailable iOS tooling as a blocker when Android is a valid repres The development identity on both platforms is: -- App: `T3 Code Dev` -- Bundle/package identifier: `com.t3tools.t3code.dev` -- URL scheme: `t3code-dev` +- App: `Phoenix Dev` +- Bundle/package identifier: `com.goodbird.phoenix.dev` +- URL scheme: `phoenix-dev` Bundle or package presence proves the correct variant, not native compatibility. Reuse it only when the current changes did not alter its Expo SDK, native dependencies, config plugins, entitlements, generated project, or native source. ## Start one disposable T3 environment -Run backend commands from the repository root. Use the ignored, worktree-local `.t3` directory or create a fresh directory with the host OS's temporary-directory mechanism. An explicit base directory stores state in `/userdata`; never point testing at shared `~/.t3` state. +Run backend commands from the repository root. Use the ignored, worktree-local `.phoenix` directory or create a fresh directory with the host OS's temporary-directory mechanism. An explicit base directory stores state in `/userdata`; never point testing at shared `~/.phoenix` state. Seed a small number of meaningful Git projects before starting the backend: @@ -72,7 +72,7 @@ Enter the complete `http://` origin to make the test transport explicit. Bare IP Run Metro from `apps/mobile`. -1. Inspect any process on the intended Metro port and its `/status` response. Reuse it only when it is healthy, belongs to this worktree, and matches `APP_VARIANT=development`, `--dev-client`, and scheme `t3code-dev`. +1. Inspect any process on the intended Metro port and its `/status` response. Reuse it only when it is healthy, belongs to this worktree, and matches `APP_VARIANT=development`, `--dev-client`, and scheme `phoenix-dev`. 2. Never kill another worktree's Metro. Use a free explicit port when necessary. 3. Run `vp run dev:client` on the standard port. For another port, retain the complete development identity: @@ -96,12 +96,12 @@ Use `ios-debugger-agent` to select one UDID and set these XcodeBuildMCP session - Scheme: `T3CodeDev` - Configuration: `Debug` - Simulator ID: the selected UDID -- Bundle ID: `com.t3tools.t3code.dev` +- Bundle ID: `com.goodbird.phoenix.dev` Check the installed client with: ```bash -xcrun simctl get_app_container com.t3tools.t3code.dev app +xcrun simctl get_app_container com.goodbird.phoenix.dev app xcrun simctl openurl ``` @@ -112,12 +112,12 @@ Accept the iOS confirmation prompt and dismiss the developer menu when it obscur Select one running emulator serial from `adb devices` and check the installed client: ```bash -adb -s shell pm path com.t3tools.t3code.dev +adb -s shell pm path com.goodbird.phoenix.dev adb -s reverse tcp: tcp: adb -s shell am start -W \ -a android.intent.action.VIEW \ -d '' \ - com.t3tools.t3code.dev + com.goodbird.phoenix.dev ``` Do not start, stop, erase, or reconfigure an emulator owned by another task. Track and later stop only processes owned by this test. @@ -134,12 +134,12 @@ Use the bundled helper from the repository root. It issues a fresh credential ag android ``` -Run only the command for the selected platform. The helper uses `http://127.0.0.1:` for iOS and `http://10.0.2.2:` for Android. Pass a fifth argument only when testing a non-development URL scheme. +Run only the command for the selected platform. The helper uses `http://127.0.0.1:` for iOS and `http://10.0.2.2:` for Android. Pass a fifth argument only when testing a non-development URL scheme. Android resolves that scheme to its registered app instead of assuming the development package. The helper opens this registered route: ```text -t3code-dev://connections/new?pairingUrl=&autoConnect=1 +phoenix-dev://connections/new?pairingUrl=&autoConnect=1 ``` The Add Environment route owns the behavior: `pairingUrl` prefills its normal host and token inputs, while `autoConnect=1` submits once in development builds and returns to Home after success. Without `autoConnect`, the same route only prefills the form for manual inspection. @@ -154,7 +154,7 @@ Pairing credentials are secret, short-lived, and single-use. Create a different ### iOS -Use `snapshot_ui` and current element references from XcodeBuildMCP for taps and typing. Stream the same UDID through `ios-simulator-browser` so the user can watch in T3 Code when the host supports it. Use the stream as a visual feed rather than a reason to switch to fragile browser coordinates. +Use `snapshot_ui` and current element references from XcodeBuildMCP for taps and typing. Stream the same UDID through `ios-simulator-browser` so the user can watch in Phoenix when the host supports it. Use the stream as a visual feed rather than a reason to switch to fragile browser coordinates. ### Android @@ -168,7 +168,7 @@ Exercise only the affected flow on one representative device unless the change s 1. Confirm the app connected to the intended disposable environment instead of merely rendering an empty disconnected state. 2. Capture the relevant final state. -3. Remove the disposable environment from T3 Code Dev. +3. Remove the disposable environment from Phoenix Dev. 4. Remove any `adb reverse` rule created for this test with `adb -s reverse --remove tcp:`. 5. Stop only the serve-sim, Metro, backend, emulator, and log processes started by this test. 6. Remove only base directories and temporary Git repositories deliberately created for this test. Preserve them when they contain useful reproduction evidence. @@ -182,7 +182,7 @@ Keep local verification focused. Do not turn this workflow into a full repositor - **The environment remains empty:** verify the platform-specific HTTP origin, use a fresh token, and confirm project seeding used the identical base directory. - **A second client cannot pair:** pairing tokens are single-use; issue another token. - **The pairing form opens but does not connect:** confirm the deep link uses the existing `connections/new` route, includes `autoConnect=1`, and carries a freshly minted encoded `pairingUrl`. -- **Pairing text changes case or punctuation:** do not retry semantic typing. Use `scripts/pair-client.sh`; the simulator keyboard layout and HID input path are not reliable for credentials. +- **Pairing text changes case or punctuation:** do not retry semantic typing. Use `.agents/skills/test-t3-mobile/scripts/pair-client.sh`; the simulator keyboard layout and HID input path are not reliable for credentials. - **iOS semantic actions fail:** set explicit XcodeBuildMCP defaults and refresh with `snapshot_ui`. - **Android cannot reach Metro:** verify `adb reverse` for the exact Metro port and relaunch the development-client URL. - **Android cannot reach the backend:** use `10.0.2.2`, not `127.0.0.1`, for the Android Emulator. diff --git a/.agents/skills/test-t3-mobile/agents/openai.yaml b/.agents/skills/test-t3-mobile/agents/openai.yaml index e9518ce9e04a..3052e676d2d4 100644 --- a/.agents/skills/test-t3-mobile/agents/openai.yaml +++ b/.agents/skills/test-t3-mobile/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Test T3 Mobile" - short_description: "Test T3 Code on iOS or Android" - default_prompt: "Use $test-t3-mobile to launch T3 Code against an isolated backend and verify the affected flow on an available simulator or emulator." + short_description: "Test Phoenix on iOS or Android" + default_prompt: "Use $test-t3-mobile to launch Phoenix against an isolated backend and verify the affected flow on an available simulator or emulator." diff --git a/.agents/skills/test-t3-mobile/scripts/pair-client.sh b/.agents/skills/test-t3-mobile/scripts/pair-client.sh index 9caa060728ec..dbb1a68004cf 100755 --- a/.agents/skills/test-t3-mobile/scripts/pair-client.sh +++ b/.agents/skills/test-t3-mobile/scripts/pair-client.sh @@ -13,7 +13,12 @@ platform="$1" device_id="$2" server_port="$3" base_dir="$4" -url_scheme="${5:-t3code-dev}" +url_scheme="${5:-phoenix-dev}" + +if [[ ! "$url_scheme" =~ ^[A-Za-z][A-Za-z0-9+.-]*$ ]]; then + echo "URL scheme must follow RFC 3986 scheme syntax." >&2 + exit 2 +fi case "$platform" in ios) @@ -31,7 +36,7 @@ repo_root="$(git rev-parse --show-toplevel)" cd "$repo_root" if ! pairing_output="$({ - T3CODE_PORT="$server_port" node apps/server/src/bin.ts auth pairing create \ + PHOENIX_PORT="$server_port" node apps/server/src/bin.ts auth pairing create \ --base-dir "$base_dir" \ --base-url "$mobile_origin" \ --ttl 15m \ @@ -64,7 +69,7 @@ case "$platform" in # adb shell re-joins its arguments and evaluates them through the device # shell, so the deep link's `?`/`&` must be quoted once more for that shell. adb -s "$device_id" shell \ - "am start -W -a android.intent.action.VIEW -d '$deep_link' com.t3tools.t3code.dev" \ + "am start -W -a android.intent.action.VIEW -d '$deep_link'" \ >/dev/null ;; esac diff --git a/.cursor/rules/cursor-cloud.mdc b/.cursor/rules/cursor-cloud.mdc index 98fcb1d5766a..cd85b64c8b01 100644 --- a/.cursor/rules/cursor-cloud.mdc +++ b/.cursor/rules/cursor-cloud.mdc @@ -1,11 +1,11 @@ --- -description: Cursor Cloud environment setup and run notes for T3 Code (Cursor-specific; not for other editors/CI) +description: Cursor Cloud environment setup and run notes for Phoenix (Cursor-specific; not for other editors/CI) alwaysApply: true --- # Cursor Cloud specific instructions -These notes apply only to Cursor Cloud agents (kept out of the shared `AGENTS.md` since T3 Code is +These notes apply only to Cursor Cloud agents (kept out of the shared `AGENTS.md` since Phoenix is developed in many places). They cover the pre-provisioned VM environment and non-obvious run caveats. ## Toolchain @@ -22,14 +22,14 @@ Pre-installed and wired into login shells (`~/.bashrc`/`~/.profile`): Node 24 (v ## Running the web app -- `npm run dev` (from repo root) starts contracts (watch), web (Vite, port 5733), and the server - (`node --watch`, port 13773) together, auto-wiring `VITE_HTTP_URL`/`VITE_WS_URL`. Use +- `npm run dev` (from repo root) starts contracts (watch), web (Vite, port 5833), and the server + (`node --watch`, port 13873) together, auto-wiring `VITE_HTTP_URL`/`VITE_WS_URL`. Use `npm run dev:server` / `npm run dev:web` for one side. If base ports are taken it auto-offsets to the next free pair, so read the actual ports from stdout. Avoid production `build`/`start` in dev. - Auth/pairing: the server is unauthenticated by default and prints a pairing URL to stdout on startup - (e.g. `http://localhost:5733/pair#token=XXXX`). Open that URL in the browser to pair before the web - UI can talk to the server. Server state (SQLite, auth, projects) lives under `~/.t3` - (`T3CODE_HOME`); multiple `npm run dev` instances bind different ports but share that same DB. + (e.g. `http://localhost:5833/pair#token=XXXX`). Open that URL in the browser to pair before the web + UI can talk to the server. Server state (SQLite, auth, projects) lives under `~/.phoenix` + (`PHOENIX_HOME`); multiple `npm run dev` instances bind different ports but share that same DB. - Agent providers are external CLIs (`codex`, `claude`, `cursor-agent`, `opencode`) probed on `PATH`. Without one installed the UI loads but no agent can run. Claude Code (`npm i -g @anthropic-ai/claude-code`) works headlessly using `ANTHROPIC_API_KEY` from the diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cc1f11cd8baf..f5e5eb18980d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "T3 Code Dev", + "name": "Phoenix Dev", "image": "debian:bookworm", "features": { "ghcr.io/devcontainers/features/git:1": {}, diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 38a764eab6d7..8d2c46964386 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,7 +9,7 @@ body: attributes: value: | Use this form for broken behavior, regressions, crashes, or reliability problems. - Feature requests belong in [Discussions](https://github.com/pingdotgg/t3code/discussions/categories/ideas). + Feature requests should use a blank issue in the Phoenix repository. Search existing issues first and keep the report focused on one problem. - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4f4940ba6655..0086358db1eb 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1 @@ -blank_issues_enabled: false -contact_links: - - name: Feature request - url: https://github.com/pingdotgg/t3code/discussions/categories/ideas - about: Suggest an improvement or new capability in Discussions. +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/via-triage.yml b/.github/ISSUE_TEMPLATE/via-triage.yml index 5b8465b8798e..89910f963b59 100644 --- a/.github/ISSUE_TEMPLATE/via-triage.yml +++ b/.github/ISSUE_TEMPLATE/via-triage.yml @@ -1,12 +1,12 @@ name: Triage report -description: Filed with `npx t3 triage`, where a coding agent investigated the machine. For hand-written reports use the bug report template instead. +description: Filed with `phoenix triage`, where a coding agent investigated the machine. For hand-written reports use the bug report template instead. labels: - via-triage body: - type: markdown attributes: value: | - This structure is what `t3 triage` agents follow. Keep one problem per issue + This structure is what `phoenix triage` agents follow. Keep one problem per issue and redact secrets and home directory paths from anything you paste. - type: textarea @@ -37,7 +37,7 @@ body: id: version attributes: label: Version - description: Installed t3 version or commit. + description: Installed Phoenix version or commit. placeholder: 0.0.33 validations: required: true @@ -75,4 +75,4 @@ body: attributes: label: Filed by description: Which agent and model produced this report. - placeholder: claude (opus-5) via t3 triage + placeholder: claude (opus-5) via phoenix triage diff --git a/.github/triage/PLAYBOOK.md b/.github/triage/PLAYBOOK.md index 39bf3ea01052..88aa8e36a109 100644 --- a/.github/triage/PLAYBOOK.md +++ b/.github/triage/PLAYBOOK.md @@ -1,6 +1,6 @@ -# T3 Code triage playbook +# Phoenix triage playbook -You are a support engineer for T3 Code (https://github.com/pingdotgg/t3code), working +You are a support engineer for Phoenix (https://github.com/goodbirdhq/phoenix), working inside a coding-agent session on the machine of a user whose install is misbehaving: crashes, auth failures, broken setups, slow launches, or anything else. Your job is to find out what went wrong, unblock the user if you can, and turn what you learned into @@ -24,7 +24,7 @@ paths for state, logs, and the database. ## 3. Check for a newer playbook -Fetch https://raw.githubusercontent.com/pingdotgg/t3code/main/.github/triage/PLAYBOOK.md. +Fetch https://raw.githubusercontent.com/goodbirdhq/phoenix/main/.github/triage/PLAYBOOK.md. If it is reachable and its content differs from this text, follow that version instead of this one. The user may be on an old release with an old copy. @@ -34,7 +34,7 @@ Clone the repo at the tag matching the user's installed version, into the source cache directory named in the context file, one subdirectory per commit hash: git clone --depth 1 --filter=blob:none --branch \ - https://github.com/pingdotgg/t3code / + https://github.com/goodbirdhq/phoenix / If the tag does not exist (nightly builds), clone `main` instead, and treat file and line references as approximate: the user's build may not match `main` @@ -51,7 +51,7 @@ Diagnosis grounded in source beats guessing. First establish the shape of the install, because the same symptom points at different code depending on it: -- How is T3 Code running on this machine: `npx t3 serve` in a terminal, the +- How is Phoenix running on this machine: `phoenix serve` in a terminal, the background service, or the desktop app? - Which surface is the user connecting from: the website (app.t3.codes), the desktop app against a local server, the desktop app against a remote server, @@ -79,7 +79,7 @@ comes from this repo's `main` branch. ## 6. Check upstream -Search existing issues in pingdotgg/t3code (use `gh`, or the public GitHub search +Search existing issues in goodbirdhq/phoenix (use `gh`, or the public GitHub search API if `gh` is missing or not logged in). Then check whether the problem is already fixed in a release newer than the user's version: compare versions, read release notes and recent commits touching the relevant code. @@ -94,7 +94,7 @@ Present what you found and let the user choose: fix it now, file an issue, both, neither. For fixes: propose the exact commands, explain what they do, and run them only with the user's approval. Prefer configuration and service-level fixes. -Do not patch the T3 Code source as a fix. A good issue with strong repro steps +Do not patch the Phoenix source as a fix. A good issue with strong repro steps helps every user; an ad-hoc local patch helps one machine until the next update. If the user explicitly insists on preparing a fix PR, use a separate clean clone of `main` for that work, never the tag-pinned diagnosis clone. @@ -109,7 +109,7 @@ of `main` for that work, never the tag-pinned diagnosis clone. posting. Never post without it. - Note at the end of the issue which model and agent produced it. - If `gh` is not authenticated, offer `gh auth login`, or build a prefilled - https://github.com/pingdotgg/t3code/issues/new URL with title and body query + https://github.com/goodbirdhq/phoenix/issues/new URL with title and body query parameters; print the URL, and open it in their browser only after they approve. - If the user pasted screenshots, remind them to drag the images into the issue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 570abd7d0505..4d4853523201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,10 @@ jobs: test: name: Test runs-on: blacksmith-8vcpu-ubuntu-2404 + # Phoenix runs this on a standard GitHub runner, which is slower than the + # fleet upstream sizes its 10m cap against, and a cap with no headroom + # cancels healthy runs mid-cleanup. Raise with the suite, not to hide a + # regression in it: a job that needs more than this wants investigating. timeout-minutes: 10 steps: - name: Checkout @@ -264,9 +268,13 @@ jobs: mobile_native_static_analysis: name: Mobile Native Static Analysis needs: mobile_native_changes + # Parked for Phoenix: we are not building the mobile native app yet, so this + # job never needs to claim a macOS runner. Upstream's gate is kept wired + # underneath the park: drop the `false &&` to restore it when mobile native + # work restarts. See docs/internals/ci.md. # Skip only on an explicit "no": a gate job that failed or errored leaves the # output empty, and that must run the lint rather than silently skip it. - if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }} + if: ${{ false && !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }} runs-on: blacksmith-6vcpu-macos-26 timeout-minutes: 10 steps: diff --git a/.github/workflows/desktop-macos-preview.yml b/.github/workflows/desktop-macos-preview.yml index 7875aec6f36b..4da78d558f76 100644 --- a/.github/workflows/desktop-macos-preview.yml +++ b/.github/workflows/desktop-macos-preview.yml @@ -30,6 +30,8 @@ jobs: contains(github.event.pull_request.labels.*.name, 'preview:mac') && (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') runs-on: blacksmith-12vcpu-macos-26 + # GitHub-hosted macOS runners are far slower than the Blacksmith machines + # upstream sizes this cap against; the release build matrix needs 90. timeout-minutes: 30 outputs: dmg_name: ${{ steps.build.outputs.dmg_name }} diff --git a/.github/workflows/mobile-eas-preview.yml b/.github/workflows/mobile-eas-preview.yml index d53602f8f5e8..7d61131359e7 100644 --- a/.github/workflows/mobile-eas-preview.yml +++ b/.github/workflows/mobile-eas-preview.yml @@ -8,6 +8,7 @@ jobs: preview: name: EAS Preview if: | + github.repository == 'goodbirdhq/phoenix' && contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment') && (github.event.action != 'labeled' || github.event.label.name == '🚀 Mobile Continuous Deployment') runs-on: blacksmith-8vcpu-ubuntu-2404 diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml index 4ad9f4f7672b..0226ed1855a7 100644 --- a/.github/workflows/mobile-eas-production.yml +++ b/.github/workflows/mobile-eas-production.yml @@ -6,6 +6,10 @@ name: Mobile EAS Production # fingerprint (platform-specific deps + pnpm version) and errors. On this Linux # runner, with corepack pinning pnpm 10.24 in eas.json, local == build. # +# Deploys against our own identities: EAS project @neilbarton/phoenix, +# Apple team 39DYB2TD96, ASC app 6807867658 (see apps/mobile/app.config.ts +# and eas.json). +# # Every merge to main that touches the mobile app reconciles, per platform: # 1. Store builds: if the latest production build's version differs from # app.config.ts, cut a new build and submit it (TestFlight + Play internal @@ -25,6 +29,20 @@ name: Mobile EAS Production # workflow_dispatch remains as a manual override for both modes (e.g. to # retry an errored build or force an OTA). on: + push: + branches: + - main + paths: + - apps/mobile/** + - packages/client-runtime/** + - packages/contracts/** + - packages/shared/** + - assets/** + - scripts/** + - patches/** + - pnpm-lock.yaml + - pnpm-workspace.yaml + - .github/workflows/mobile-eas-production.yml workflow_dispatch: inputs: mode: @@ -52,19 +70,6 @@ on: description: "OTA update message (mode=update only)" required: false type: string - push: - branches: [main] - paths: - - apps/mobile/** - - packages/client-runtime/** - - packages/contracts/** - - packages/shared/** - - assets/** - - scripts/** - - patches/** - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .github/workflows/mobile-eas-production.yml # Serialize runs so OTAs publish in merge order. GitHub keeps at most one # queued run per group, so a burst of merges collapses into one run of the @@ -76,6 +81,7 @@ concurrency: jobs: production: name: EAS Production ${{ github.event_name == 'push' && 'auto' || inputs.mode }} + if: ${{ github.repository == 'goodbirdhq/phoenix' }} runs-on: blacksmith-8vcpu-ubuntu-2404 permissions: contents: read @@ -244,7 +250,9 @@ jobs: run: | failed=0 version="$(npx expo config --json --type public | jq -r '.version')" - for platform in ios android; do + # Android is parked: auto-submit needs a Google Play service account + # key this fork has not set up. Restore "ios android" once it exists. + for platform in ios; do latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')" if [ "$latest" = "$version" ]; then echo "$platform: production build for $version already exists (or is in progress)" diff --git a/.github/workflows/phoenix-build.yml b/.github/workflows/phoenix-build.yml new file mode 100644 index 000000000000..7245de50e6bb --- /dev/null +++ b/.github/workflows/phoenix-build.yml @@ -0,0 +1,168 @@ +# Phoenix-only workflow. Upstream T3 Code does not have this file, so it never +# conflicts on merge. Upstream's own `Release` workflow is disabled GitHub-side +# (see docs/internals/ci.md) because it needs ~29 secrets we do not have. +name: Phoenix Build + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + publish_release: + description: "Publish a GitHub pre-release with the artifact" + required: false + default: true + type: boolean + +concurrency: + group: phoenix-build-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + build_macos: + name: Build macOS (unsigned) + runs-on: blacksmith-12vcpu-macos-26 + timeout-minutes: 45 + outputs: + version: ${{ steps.version.outputs.version }} + artifact: ${{ steps.locate.outputs.artifact }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: true + run-install: true + + - name: Resolve version + id: version + run: | + base="$(node -p "require('./apps/desktop/package.json').version")" + version="${base}+${GITHUB_SHA::7}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Building Phoenix ${version}" + + - name: Ensure Electron runtime is installed + run: vp run --filter @t3tools/desktop ensure:electron + + - name: Build desktop pipeline + run: vp run build:desktop + + # T3CODE_DESKTOP_SIGNED defaults to false, and the build script clears + # CSC_LINK / disables identity auto-discovery when unsigned. Passing it + # explicitly documents the intent rather than relying on the default. + - name: Build unsigned arm64 DMG + env: + T3CODE_DESKTOP_SIGNED: "false" + T3CODE_DESKTOP_OUTPUT_DIR: ${{ runner.temp }}/phoenix-dist + run: node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64 + + - name: Locate artifact + id: locate + run: | + dmg="$(find "${{ runner.temp }}/phoenix-dist" -name '*.dmg' -maxdepth 2 | head -1)" + if [ -z "$dmg" ]; then + echo "No DMG produced. Contents:" >&2 + ls -R "${{ runner.temp }}/phoenix-dist" >&2 || true + exit 1 + fi + echo "artifact=$(basename "$dmg")" >> "$GITHUB_OUTPUT" + echo "path=${dmg}" >> "$GITHUB_OUTPUT" + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: phoenix-macos-arm64 + path: ${{ runner.temp }}/phoenix-dist/*.dmg + if-no-files-found: error + retention-days: 30 + + - name: Publish pre-release + if: github.event_name == 'push' || inputs.publish_release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + tag="build-${VERSION}" + # printf rather than a heredoc: a quoted heredoc terminator cannot be + # indented, and everything here lives inside an indented YAML block. + printf '%s\n' \ + 'Phoenix build for macOS (Apple silicon), ad-hoc signed but **not notarised**.' \ + '' \ + 'Drag `Phoenix (Alpha).app` from the DMG into /Applications. The first launch is' \ + 'blocked by Gatekeeper, because notarisation needs an Apple Developer ID we do' \ + 'not have yet:' \ + '' \ + '> "Phoenix (Alpha)" Not Opened - Apple could not verify "Phoenix (Alpha)" is' \ + '> free of malware that may harm your Mac or compromise your privacy.' \ + '' \ + 'Click **Done** (not *Move to Trash*), then open **System Settings -> Privacy &' \ + 'Security**, scroll to **Security**, and click **Open Anyway** next to the' \ + 'Phoenix entry. You only need to do this once per build.' \ + '' \ + 'Note: Control-clicking the app and choosing **Open** does *not* work. macOS 15' \ + 'removed that bypass for un-notarised apps, so System Settings is the only route.' \ + 'The Open Anyway button only appears for about an hour after a blocked launch.' \ + '' \ + 'If you instead see *"is damaged and can'"'"'t be opened"*, that is a different' \ + 'fault - a broken signature rather than a missing one - and it is a bug in this' \ + 'build. Please report it rather than working around it.' \ + '' \ + 'Phoenix installs alongside T3 Code: separate app bundle, separate state in' \ + '`~/.phoenix`, its own `phoenix://` URL scheme and port 3873. Your existing' \ + 'T3 Code install is untouched.' \ + > notes.md + gh release create "$tag" "${{ steps.locate.outputs.path }}" \ + --title "Phoenix ${VERSION}" \ + --notes-file notes.md \ + --prerelease + + notify_slack: + name: Notify Slack + needs: build_macos + if: always() && needs.build_macos.result != 'cancelled' + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 5 + steps: + # No-ops when the secret is absent so forks and first-time setup do not + # fail the run on a missing webhook. + - name: Post to Slack + env: + WEBHOOK: ${{ secrets.SLACK_RELEASE_WEBHOOK_URL }} + RESULT: ${{ needs.build_macos.result }} + VERSION: ${{ needs.build_macos.outputs.version }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + RELEASES_URL: ${{ github.server_url }}/${{ github.repository }}/releases + run: | + if [ -z "$WEBHOOK" ]; then + echo "SLACK_RELEASE_WEBHOOK_URL is not set; skipping notification." + exit 0 + fi + + if [ "$RESULT" != "success" ]; then + text=$(printf ':x: *Phoenix build failed*\n<%s|View run>' "$RUN_URL") + else + text=$(printf ':fire: *Phoenix %s is ready*\n<%s|Download the DMG>\n\n*This build is unsigned*, so macOS will block it until you clear quarantine:\n```\nxattr -dr com.apple.quarantine /Applications/Phoenix.app\n```\nDrag the app to /Applications first, then run that. "Phoenix is damaged" means you skipped it.\n\nInstalls alongside T3 Code - separate state in `~/.phoenix`, port 3873.' "$VERSION" "$RELEASES_URL") + fi + + jq -n --arg t "$text" '{text: $t}' > payload.json + code=$(curl -sS -o response.txt -w '%{http_code}' \ + -X POST -H 'Content-Type: application/json' \ + --data @payload.json "$WEBHOOK") + if [ "$code" != "200" ]; then + echo "Slack rejected the notification (HTTP $code): $(cat response.txt)" >&2 + exit 1 + fi + echo "Slack notified." diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml deleted file mode 100644 index 62f8fd1f5470..000000000000 --- a/.github/workflows/publish-aur.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Publish AUR package - -# See packaging/aur/README.md. - -on: - workflow_call: - inputs: - release_tag: - required: true - type: string - pkgrel: - required: false - default: "1" - type: string - secrets: - AUR_SSH_PRIVATE_KEY: - required: true - workflow_dispatch: - inputs: - release_tag: - description: "Release tag to publish" - required: true - type: string - pkgrel: - description: "Arch package release override" - required: false - default: "1" - type: string - -permissions: - contents: read - -concurrency: - group: publish-aur - cancel-in-progress: false - -jobs: - publish: - name: Validate and publish - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 30 - container: - image: archlinux:base-devel - - steps: - - name: Install Arch packaging tools - run: pacman -Syu --noconfirm --needed git github-cli jq namcap openssh sudo - - - name: Checkout packaging sources - uses: actions/checkout@v6 - - - name: Create unprivileged build user - run: | - useradd --create-home builder - install -Dm0440 /dev/stdin /etc/sudoers.d/builder <<'EOF' - builder ALL=(root) NOPASSWD: /usr/bin/pacman - EOF - - - name: Validate and publish package sources - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ inputs.release_tag }} - PKGREL: ${{ inputs.pkgrel || '1' }} - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - run: packaging/aur/scripts/release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a5c691a3c7e..d634f7864e7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,6 @@ jobs: release_name: ${{ steps.release_meta.outputs.name }} short_sha: ${{ steps.release_meta.outputs.short_sha }} previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} - cli_dist_tag: ${{ steps.release_meta.outputs.cli_dist_tag }} is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }} make_latest: ${{ steps.release_meta.outputs.make_latest }} ref: ${{ github.sha }} @@ -132,7 +131,6 @@ jobs: --github-output echo "release_channel=nightly" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=nightly" >> "$GITHUB_OUTPUT" echo "is_prerelease=true" >> "$GITHUB_OUTPUT" echo "make_latest=false" >> "$GITHUB_OUTPUT" else @@ -155,8 +153,7 @@ jobs: echo "release_channel=stable" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=v$version" >> "$GITHUB_OUTPUT" - echo "name=T3 Code v$version" >> "$GITHUB_OUTPUT" - echo "cli_dist_tag=latest" >> "$GITHUB_OUTPUT" + echo "name=Phoenix v$version" >> "$GITHUB_OUTPUT" if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "is_prerelease=false" >> "$GITHUB_OUTPUT" echo "make_latest=true" >> "$GITHUB_OUTPUT" @@ -215,10 +212,13 @@ jobs: # alongside preflight instead of after it. The condition mirrors preflight's: # check_changes is skipped on non-schedule events (skipped is neither failure # nor success, so success() would be wrong here). + # Forks without T3 Connect relay infrastructure (no Cloudflare access) skip + # this job; the desktop build then ships without baked-in relay config. needs: [check_changes] if: | !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') + (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') && + vars.CLOUDFLARE_ACCOUNT_ID != '' runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 5 environment: @@ -371,7 +371,7 @@ jobs: # build_wsl_node_pty failed; the Windows-only download step below then fails # that single platform if the prebuild is missing. needs: [preflight, relay_public_config, build_wsl_node_pty] - if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }} + if: ${{ !cancelled() && needs.preflight.result == 'success' && (needs.relay_public_config.result == 'success' || needs.relay_public_config.result == 'skipped') }} runs-on: ${{ matrix.runner }} timeout-minutes: 30 env: @@ -390,27 +390,29 @@ jobs: arch: arm64 rust_target: aarch64-apple-darwin resource_key: darwin-arm64 - - label: macOS x64 - runner: blacksmith-12vcpu-macos-26 - platform: mac - target: dmg - arch: x64 - rust_target: x86_64-apple-darwin - resource_key: darwin-x64 - - label: Linux x64 - runner: blacksmith-32vcpu-ubuntu-2404 - platform: linux - target: AppImage - arch: x64 - rust_target: x86_64-unknown-linux-gnu - resource_key: linux-x64 - - label: Windows x64 - runner: blacksmith-32vcpu-windows-2025 - platform: win - target: nsis - arch: x64 - rust_target: x86_64-pc-windows-msvc - resource_key: win32-x64 + # Linux, Windows, and Intel macOS builds are parked until we want + # them — Apple Silicon is the only desktop target we ship right now. + # - label: macOS x64 + # runner: macos-15 + # platform: mac + # target: dmg + # arch: x64 + # rust_target: x86_64-apple-darwin + # resource_key: darwin-x64 + # - label: Linux x64 + # runner: ubuntu-24.04 + # platform: linux + # target: AppImage + # arch: x64 + # rust_target: x86_64-unknown-linux-gnu + # resource_key: linux-x64 + # - label: Windows x64 + # runner: windows-2025 + # platform: win + # target: nsis + # arch: x64 + # rust_target: x86_64-pc-windows-msvc + # resource_key: win32-x64 # - label: Windows arm64 # runner: windows-11-arm # platform: win @@ -463,12 +465,14 @@ jobs: targets: ${{ matrix.rust_target }} - name: Download relay client tracing config + if: ${{ needs.relay_public_config.result == 'success' }} uses: actions/download-artifact@v8 with: name: relay-client-tracing-config path: ${{ runner.temp }}/relay-client-tracing - name: Load relay client tracing config + if: ${{ needs.relay_public_config.result == 'success' }} shell: bash run: | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" @@ -701,19 +705,6 @@ jobs: # done # fi - - name: Collect resource monitor - shell: bash - run: | - set -euo pipefail - binary_name="t3-resource-monitor" - if [[ "${{ matrix.platform }}" == "win" ]]; then - binary_name="${binary_name}.exe" - fi - source_path="native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}" - target_dir="resource-monitor-publish/${{ matrix.resource_key }}" - mkdir -p "$target_dir" - cp "$source_path" "$target_dir/$binary_name" - - name: Upload build artifacts uses: actions/upload-artifact@v7 with: @@ -721,18 +712,20 @@ jobs: path: release-publish/* if-no-files-found: error - - name: Upload resource monitor - uses: actions/upload-artifact@v7 - with: - name: resource-monitor-${{ matrix.resource_key }} - path: resource-monitor-publish/${{ matrix.resource_key }}/* - if-no-files-found: error - publish_cli: name: Publish CLI to npm needs: [preflight, relay_public_config, quality, build] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' }} - runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404 + # Publishes via npm trusted publishing (OIDC, id-token: write) — the + # registry trusts this workflow directly, no token secret. Gated on the + # NPM_TRUSTED_PUBLISHING repo variable so forks without the npmjs-side + # trusted-publisher config skip cleanly. Tolerates the skipped relay job + # like the build does. Unlike upstream we publish without bundled resource + # monitors: the matrix only builds darwin-arm64, and shipping one + # platform's monitor in a cross-platform npm package helps no one. + if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' && (needs.relay_public_config.result == 'success' || needs.relay_public_config.result == 'skipped') && vars.NPM_TRUSTED_PUBLISHING == 'true' }} + # ubuntu-24.04, not blacksmith: npm rejects provenance attestations from + # self-hosted runners, and Blacksmith registers as self-hosted. + runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read @@ -763,53 +756,37 @@ jobs: - --filter=@t3tools/web... - --filter=@t3tools/scripts... - - name: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - name: Align package versions to release version run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - # The t3 build task depends on @t3tools/web#build, so the web client is - # built (once) as part of this step. + # The @goodbirdhq/phoenix build task depends on @t3tools/web#build, so the + # web client is built (once) as part of this step. - name: Build CLI package run: vp run --filter t3 build - - name: Download resource monitors - uses: actions/download-artifact@v8 - with: - pattern: resource-monitor-* - path: ${{ runner.temp }}/resource-monitors - - - name: Bundle resource monitors into CLI package - shell: bash + - name: Publish CLI package run: | set -euo pipefail - for artifact_dir in "$RUNNER_TEMP"/resource-monitors/resource-monitor-*; do - resource_key="${artifact_dir##*/resource-monitor-}" - target_dir="apps/server/dist/resource-monitor/${resource_key}" - mkdir -p "$target_dir" - cp "$artifact_dir"/t3-resource-monitor* "$target_dir/" - chmod +x "$target_dir"/t3-resource-monitor 2>/dev/null || true - done + channel="${{ needs.preflight.outputs.release_channel }}" + if [[ "$channel" == "nightly" ]]; then + dist_tag="nightly" + elif [[ "${{ needs.preflight.outputs.is_prerelease }}" == "true" ]]; then + dist_tag="next" + else + dist_tag="latest" + fi - - name: Publish CLI package - run: node apps/server/scripts/cli.ts publish --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --app-version "${{ needs.preflight.outputs.version }}" --verbose + node apps/server/scripts/cli.ts publish \ + --tag "$dist_tag" \ + --app-version "${{ needs.preflight.outputs.version }}" \ + --verbose release: name: Publish GitHub Release - needs: [preflight, build, publish_cli] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.publish_cli.result == 'success' }} + # publish_cli skips itself while NPM_TOKEN is unset, so the GitHub Release + # stays the fork's reliable choke point and does not depend on npm. + needs: [preflight, quality, build] + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' }} runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 30 permissions: @@ -851,56 +828,76 @@ jobs: fi done + # - name: Merge Windows updater manifests + # run: | + # shopt -s nullglob + # found_windows_manifest=false + # for x64_manifest in release-assets/*-win-x64.yml; do + # if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then + # continue + # fi + + # arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}" + # output_manifest="${x64_manifest/-win-x64.yml/.yml}" + # if [[ ! -f "$arm64_manifest" ]]; then + # echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2 + # exit 1 + # fi + + # found_windows_manifest=true + # node scripts/merge-update-manifests.ts --platform win \ + # "$arm64_manifest" \ + # "$x64_manifest" \ + # "$output_manifest" + # rm -f "$arm64_manifest" "$x64_manifest" + # done + + # if [[ "$found_windows_manifest" != true ]]; then + # echo "No Windows updater manifests found to merge." >&2 + # exit 1 + # fi + + # gh release create, not softprops/action-gh-release: the action 403s + # creating releases on this repo even with contents:write, while gh with + # the same token demonstrably works (phoenix-build.yml publishes with it). - name: Publish release - if: needs.preflight.outputs.previous_tag != '' - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ needs.preflight.outputs.tag }} - target_commitish: ${{ needs.preflight.outputs.ref }} - name: ${{ needs.preflight.outputs.release_name }} - generate_release_notes: true - previous_tag: ${{ needs.preflight.outputs.previous_tag }} - prerelease: ${{ needs.preflight.outputs.is_prerelease }} - make_latest: ${{ needs.preflight.outputs.make_latest }} - files: | - release-assets/*.dmg - release-assets/*.zip - release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap - release-assets/*.yml - fail_on_unmatched_files: true - token: ${{ github.token }} - - - name: Publish first release - if: needs.preflight.outputs.previous_tag == '' - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ needs.preflight.outputs.tag }} - target_commitish: ${{ needs.preflight.outputs.ref }} - name: ${{ needs.preflight.outputs.release_name }} - generate_release_notes: true - prerelease: ${{ needs.preflight.outputs.is_prerelease }} - make_latest: ${{ needs.preflight.outputs.make_latest }} - files: | - release-assets/*.dmg - release-assets/*.zip - release-assets/*.AppImage - release-assets/*.exe - release-assets/*.blockmap - release-assets/*.yml - fail_on_unmatched_files: true - token: ${{ github.token }} - - publish_aur: - name: Publish AUR package - needs: [preflight, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }} - uses: ./.github/workflows/publish-aur.yml - with: - release_tag: ${{ needs.preflight.outputs.tag }} - secrets: - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.preflight.outputs.tag }} + TARGET: ${{ needs.preflight.outputs.ref }} + RELEASE_NAME: ${{ needs.preflight.outputs.release_name }} + PREVIOUS_TAG: ${{ needs.preflight.outputs.previous_tag }} + IS_PRERELEASE: ${{ needs.preflight.outputs.is_prerelease }} + MAKE_LATEST: ${{ needs.preflight.outputs.make_latest }} + run: | + set -euo pipefail + shopt -s nullglob + assets=(release-assets/*.dmg release-assets/*.zip release-assets/*.blockmap release-assets/*.yml) + if ! compgen -G "release-assets/*.dmg" >/dev/null || ! compgen -G "release-assets/*.yml" >/dev/null; then + echo "Missing expected release assets (need at least one .dmg and one .yml):" >&2 + ls -la release-assets/ >&2 + exit 1 + fi + + if gh release view "$TAG" --json id >/dev/null 2>&1; then + echo "Release $TAG already exists; replacing assets." + gh release upload "$TAG" --clobber "${assets[@]}" + exit 0 + fi + + args=( + --target "$TARGET" + --title "$RELEASE_NAME" + --generate-notes + --latest="$MAKE_LATEST" + ) + if [[ -n "$PREVIOUS_TAG" ]]; then + args+=(--notes-start-tag "$PREVIOUS_TAG") + fi + if [[ "$IS_PRERELEASE" == "true" ]]; then + args+=(--prerelease) + fi + gh release create "$TAG" "${assets[@]}" "${args[@]}" deploy_web: name: Deploy hosted web app @@ -941,12 +938,14 @@ jobs: - --filter=@t3tools/web... - name: Download relay client tracing config + if: ${{ needs.relay_public_config.result == 'success' }} uses: actions/download-artifact@v8 with: name: relay-client-tracing-config path: ${{ runner.temp }}/relay-client-tracing - name: Load relay client tracing config + if: ${{ needs.relay_public_config.result == 'success' }} shell: bash run: | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" @@ -1027,9 +1026,16 @@ jobs: needs: [preflight, release] runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 10 + permissions: + contents: write + env: + HAS_RELEASE_APP: ${{ secrets.RELEASE_APP_ID != '' }} steps: + # Forks without the release GitHub App fall back to the workflow token; + # the version-bump commit then comes from github-actions[bot]. - id: app_token name: Mint release app token + if: ${{ env.HAS_RELEASE_APP == 'true' }} uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.RELEASE_APP_ID }} @@ -1041,7 +1047,7 @@ jobs: with: ref: main fetch-depth: 0 - token: ${{ steps.app_token.outputs.token }} + token: ${{ steps.app_token.outputs.token || github.token }} persist-credentials: true sparse-checkout: | /* @@ -1050,6 +1056,7 @@ jobs: - id: app_bot name: Resolve GitHub App bot identity + if: ${{ steps.app_token.outcome == 'success' }} env: GH_TOKEN: ${{ steps.app_token.outputs.token }} APP_SLUG: ${{ steps.app_token.outputs.app-slug }} @@ -1093,8 +1100,8 @@ jobs: exit 0 fi - git config user.name "${{ steps.app_bot.outputs.name }}" - git config user.email "${{ steps.app_bot.outputs.email }}" + git config user.name "${{ steps.app_bot.outputs.name || 'github-actions[bot]' }}" + git config user.email "${{ steps.app_bot.outputs.email || '41898282+github-actions[bot]@users.noreply.github.com' }}" git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml git commit -m "chore(release): prepare $RELEASE_TAG" diff --git a/.gitignore b/.gitignore index 57262578a786..48cb7bdb21ca 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ build/ .logs/ release/ release-mock/ +.phoenix +# Upstream T3 Code's worktree state dir. Still ignored so a checkout that has run +# either app (or an upstream merge) never commits local dev state. .t3 .idea/ apps/web/.playwright @@ -38,3 +41,6 @@ node_modules/ *.log .env* !.env.example + +# OMC agent tooling state (session history, project memory) +.omc/ diff --git a/AGENTS.md b/AGENTS.md index 492fdb3c29e1..e8db71ce1b6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,34 +1,34 @@ -# T3 Code +# Phoenix -T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients. +Phoenix is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients. -You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor. +You can think of Phoenix as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor. -## What makes T3 Code special? +## What makes Phoenix special? -We have over 200,000 users who love T3 Code. It's important we maintain the things they love as we continue to iterate on the product. Here's a brief list of the things we can never compromise on. +We have over 100,000 users who love Phoenix. It's important we maintain the things they love as we continue to iterate on the product. Here's a brief list of the things we can never compromise on. ### 1. Open at the core -T3 Code is truly open. We share our roadmap, we share how we think about things, and of course we share all our code. A large number of our users run forks. We work in the open, and should strive to stay that way. +Phoenix is truly open. We share our roadmap, we share how we think about things, and of course we share all our code. A large number of our users run forks. We work in the open, and should strive to stay that way. ### 2. Performance without compromise -Lots of apps have gotten bogged down with bad tech decisions and "slop". We have not, and we're proud of the performance of T3 Code. We regularly audit for performance regressions, often caused by sending too much data over websockets, css animations causing gpu spikes, lists being hard to render, and more. Make sure all changes are considerate of performance impact. +Lots of apps have gotten bogged down with bad tech decisions and "slop". We have not, and we're proud of the performance of Phoenix. We regularly audit for performance regressions, often caused by sending too much data over websockets, css animations causing gpu spikes, lists being hard to render, and more. Make sure all changes are considerate of performance impact. ### 3. Remote ready -The architecture of T3 Code's websocket layer (npx t3) enables a lot of awesome remote features. These have become core to the product. Whether users are connecting directly over their local network, using Tailscale, or leaning in fully with T3 Connect (our tunnel solution, also in this repo), we need to make sure new features are properly supported. +The architecture of Phoenix's websocket layer (the phoenix CLI) enables a lot of awesome remote features. These have become core to the product. Whether users are connecting directly over their local network, using Tailscale, or leaning in fully with T3 Connect (our tunnel solution, also in this repo), we need to make sure new features are properly supported. ### 4. Multi-surface -T3 Code has 3 key app surfaces: **web**, **desktop**, and **mobile**. +Phoenix has 3 key app surfaces: **web**, **desktop**, and **mobile**. -**Web** is kind of two surfaces, as we have the public facing "app.t3.codes" as well as locally hosting the web app through the `npx t3` command. Both need to be supported by all new features where reasonable. +**Web** is kind of two surfaces, as we have the public facing "app.t3.codes" as well as locally hosting the web app through the `phoenix` command. Both need to be supported by all new features where reasonable. **Desktop** is the main surface most users install first. It's a full Electron app that bundles the server runner as well. The desktop app can also be used as the host server, allowing remote connections from app.t3.codes or the mobile app. -**Mobile** is a React Native app for both iOS and Android, available on the App Store and Google Play. The mobile app allows for connecting to any T3 Code server to control work remotely. +**Mobile** is a React Native app for both iOS and Android, available on the App Store and Google Play. The mobile app allows for connecting to any Phoenix server to control work remotely. ## A note from Theo @@ -38,17 +38,17 @@ Channel both "measure twice, cut once" and "yagni". Fight scope creep. Try to ho The rest of this document is meant to help you navigate the codebase and make changes effectively. Think of these instructions less as "hard rules", more as "good defaults". The developer's preferences should be able to override anything here. -Of note: Most T3 Code contributions will come from T3 Code itself, often controlled remotely. This means you should be careful about accessing data, killing dev servers, and other things that may damage the T3 Code instance that the contributor is using. +Of note: Most Phoenix contributions will come from Phoenix itself, often controlled remotely. This means you should be careful about accessing data, killing dev servers, and other things that may damage the Phoenix instance that the contributor is using. ## A small glossary We need to be on the same page with terminology. When communicating, use this language: -- **you** means the agent reading this file and changing T3 Code. -- **we, us, and maintainers** mean Theo, Julius and the people building T3 Code. These are who you are talking to now. -- **user** means the person using T3 Code to direct coding agents. -- **agent** means the coding agent a user runs inside T3 Code. Depending on context, that may also include you. -- **provider** means the agent runtime or harness T3 Code talks to, such as Codex, Claude, Cursor, or OpenCode. +- **you** means the agent reading this file and changing Phoenix. +- **we, us, and maintainers** mean Theo, Julius and the people building Phoenix. These are who you are talking to now. +- **user** means the person using Phoenix to direct coding agents. +- **agent** means the coding agent a user runs inside Phoenix. Depending on context, that may also include you. +- **provider** means the agent runtime or harness Phoenix talks to, such as Codex, Claude, Cursor, or OpenCode. - **client** means the web, desktop, or mobile UI. - **environment** means one running T3 server and the machine, filesystem, provider credentials, and state it owns. - **project** means an environment-local workspace record rooted at a directory. @@ -59,7 +59,7 @@ We need to be on the same page with terminology. When communicating, use this la ## The three ways to hurt yourself 1. **Killing by pattern.** Never `pkill -f`, `pgrep | kill`, or `kill` a PID you found by matching a name, path, or worktree string. Your own agent process has this worktree's path in its argv, and this machine runs several other dev servers at once. Kill only a PID you captured at spawn, or the owner of your port from `ss -H -ltnp` after confirming `/proc//cwd` is your worktree. -2. **Writing to the live install.** `~/.t3/userdata` is the developer's real T3 Code database, in use while you work. Reading it and copying from it are fine, and a good way to get real test data (see Test data). Never start a server against it, never open it read-write, never clean it up. +2. **Writing to the live install.** `~/.phoenix/userdata` is the developer's real Phoenix database, in use while you work. Reading it and copying from it are fine, and a good way to get real test data (see Test data). Never start a server against it, never open it read-write, never clean it up. 3. **Baking in origins.** Never set `VITE_HTTP_URL` or `VITE_WS_URL` for dev. Dev is single-origin and Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known`. Setting them bakes localhost into the bundle and silently breaks every remote browser. ## Hit every surface @@ -77,7 +77,7 @@ The most common defect in this repo is a change that works on the path you teste ## Dev servers - `vp i` installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run. -- `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins. +- `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.phoenix`, which deliberately outranks an ambient `PHOENIX_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins. - Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift. - Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, paste that full URL (token included) in your reply. Do not wire up `tailscale serve` by hand for this, and do not open the URL yourself. - The web app requires pairing. Hand over the pairing URL, not the bare origin. A URL without its token is useless to whoever you gave it to. If the token got consumed, mint a fresh one with `node apps/server/src/bin.ts pair` — note it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management). @@ -85,15 +85,15 @@ The most common defect in this repo is a change that works on the path you teste ## Test data -An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state: +An empty database is a bad test. Seed your worktree's `.phoenix` with a copy of real data instead of pointing at live state: -- Copy from `~/.t3/userdata` (the developer's real data, the most realistic test set) or `~/.t3/dev`. Worktree state lives at `/.t3/userdata`. +- Copy from `~/.phoenix/userdata` (the developer's real data, the most realistic test set) or `~/.phoenix/dev`. Worktree state lives at `/.t3/userdata`. - Snapshot the database with `VACUUM INTO`, which is safe even while a server has the source open and yields one consistent file: ```bash - mkdir -p .t3/userdata - rm -f .t3/userdata/state.sqlite* # VACUUM INTO refuses to overwrite - bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.t3/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/state.sqlite'\")" + mkdir -p .phoenix/userdata + rm -f .phoenix/userdata/state.sqlite* # VACUUM INTO refuses to overwrite + bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.t3/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.phoenix/userdata/state.sqlite'\")" ``` A plain `cp` is only safe when no server has the source open, and must bring the `-wal` and `-shm` siblings along. A live file copy is a corrupt copy. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 000000000000..cc10c0cddc74 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,57 @@ +# Phoenix Product Domain + +Phoenix lets users direct coding agents inside environment-owned projects. This glossary fixes the product language used while shaping Phoenix behavior. + +## Scheduling + +**Schedule**: +A named, saved agent prompt owned by one Environment and assigned to one of its Projects, together with a one-time or recurring timing rule interpreted in an explicitly saved user time zone. Its Environment cannot change, recurring rules have a minimum interval of five minutes, and each occurrence starts a new Thread. +_Avoid_: Scheduled, cron job, pending task + +**Occurrence**: +A time at which a Schedule becomes due to trigger a Scheduled run, either from its timing rule or from the user choosing Run now. +_Avoid_: Cron tick, invocation + +**Enabled Schedule**: +A Schedule whose timing rule can produce new Occurrences. +_Avoid_: Active Schedule + +**Paused Schedule**: +A Schedule whose timing rule produces no Occurrences until the user resumes it. Resuming begins with the next future time and does not catch up the paused period; a one-time Schedule whose time passed while paused requires a new future time or Run now. +_Avoid_: Disabled Schedule + +**Completed Schedule**: +A one-time Schedule that has triggered. It retains its configuration and history but produces no more timed Occurrences. +_Avoid_: Finished task, expired Schedule + +**Failed Schedule**: +A one-time Schedule whose Occurrence failed before it could trigger. It retains its configuration and error for inspection or a manual Run now. +_Avoid_: Failed run + +**Scheduled Run**: +The agent work started by successfully triggering an Occurrence. Scheduled runs may overlap; serialization applies to triggering them, not to their execution. +_Avoid_: Scheduled task, job + +**Scheduled Thread**: +The new Thread created by a Trigger. Its title combines the Schedule name with the local trigger time; after creation it follows the same lifecycle as any manually created Thread and remains after its Schedule is deleted. +_Avoid_: Schedule history + +**Schedule history**: +The compact record of a Schedule's Occurrences, failures, and links to the Threads it triggered. Deleting the Schedule removes this record but not the linked Threads. +_Avoid_: Archive, run log + +**Trigger**: +The durable creation of a new Thread and acceptance of its first Turn for an Occurrence. Failures after this point belong to the new Thread rather than to the Schedule. +_Avoid_: Completion, execution + +**Missed Occurrence**: +An Occurrence that became due while its Schedule could not trigger. Only the newest Missed occurrence of each Schedule remains eligible to run; older ones are skipped. +_Avoid_: Backlog + +**Failed Occurrence**: +An Occurrence Phoenix could not trigger. It records a visible failure without creating a Scheduled run. +_Avoid_: Failed run + +**Runnable environment**: +An Environment whose server is online and capable of accepting new work. +_Avoid_: Available environment diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cd75ef96edb..82068af8188f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ We are not actively accepting contributions right now. You can still report a bug or open a PR, but please do so knowing there is a high chance we close it, defer it forever, or never look at it. -Feature requests and proposals belong in [Ideas discussions](https://github.com/pingdotgg/t3code/discussions/categories/ideas), not issues. +Feature requests and proposals belong in the [Phoenix issue tracker](https://github.com/goodbirdhq/phoenix/issues), not upstream T3 Code Discussions. If that sounds annoying, that is because it is. This project is still early and we are trying to keep scope, quality, and direction under control. @@ -59,7 +59,7 @@ If we have to guess what changed, we are much less likely to review it. ## Discuss Changes First -If you are thinking about a non-trivial change, start a discussion first. Issues are reserved for bug reports. +If you are thinking about a non-trivial change, open one focused issue first and explain the user problem. Use the bug form for broken behavior and a blank issue for feature or product direction. That still does not mean we will want the PR, but it gives you a chance to avoid wasting your time. diff --git a/README.md b/README.md index 8ec101387f67..bc16ff217851 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ -# T3 Code +# Phoenix -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). +Phoenix is an "agent harness control surface". It enables control of the agents on your machine from a mobile app, a web app, and an Electron-based desktop app. -Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, and OpenCode. If they're set up on your computer, T3 Code can control them. +Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, and OpenCode. If they're set up on your computer, Phoenix can control them. -## "Wait, what are you selling me?" +## Built on T3 Code -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. +**Phoenix is a fork of [T3 Code](https://github.com/pingdotgg/t3code) by [T3 Tools Inc.](https://t3.codes) — and T3 Code is the nuts.** -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. +Essentially all of the hard engineering here — the architecture, the agent harness integrations, the mobile app, the sync layer — is theirs. They built something genuinely excellent and open-sourced it under MIT, which is the only reason this fork can exist. Phoenix tracks their upstream and merges their work continuously; we are downstream consumers of an outstanding project, not competitors with it. + +If you want the original, supported, first-party product, go get it: + +- Desktop / web: · +- iOS: [App Store](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824) · Android: [Play Store](https://play.google.com/store/apps/details?id=com.t3tools.t3code) +- Source: · Community: [Discord](https://discord.gg/jn4EGJjrvv) + +Phoenix is an unofficial fork. Please don't take Phoenix bugs to the T3 Code team — file them here instead. ## Installation > [!WARNING] -> T3 Code currently supports Codex, Claude, Cursor, Grok Build and OpenCode. Install and authenticate at least one provider before use: +> Phoenix currently supports Codex, Claude, Cursor, Grok Build and OpenCode. Install and authenticate at least one provider before use: > > - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login` > - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login` @@ -21,55 +29,48 @@ We wanted something performant, remote-ready, and truly open. If we ever go the > - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login` > - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login` -### Try it out (install-free) - -The easiest way to test T3 Code is to run the server in your terminal (requires Node.js 22.16+, 23.11+, or 24.10+): - -```bash -npx t3@latest -``` - -This will launch T3 Code's backend on your machine as well as the local web app to control your agents. - -Tip: Use `npx t3@latest --help` for the full CLI reference. +### Run the server -### Desktop app - -Install the latest version of the desktop app from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), or from your favorite package registry: - -#### Windows (`winget`) +Phoenix ships on npm as [`@goodbirdhq/phoenix`](https://www.npmjs.com/package/@goodbirdhq/phoenix) +(requires Node.js 22.16+, 23.11+, or 24.10+): ```bash -winget install T3Tools.T3Code +npx @goodbirdhq/phoenix ``` -#### macOS (Homebrew) +Or build from source and run the CLI: ```bash -brew install --cask t3-code +vp i && vp run --filter @goodbirdhq/phoenix build +node apps/server/dist/bin.mjs ``` -#### Arch Linux (AUR) +This launches Phoenix's backend on your machine along with the local web app. Installing the package +globally puts the CLI on your `PATH` as `phoenix` (not `t3`, so it will not collide with an existing +T3 Code install). -Stable: +Tip: `phoenix --help` for the full CLI reference. -```bash -yay -S t3code-bin -``` +> [!NOTE] +> `npx t3@latest` runs **upstream T3 Code**, not Phoenix. -Nightly: +### Desktop app -```bash -yay -S t3code-nightly-bin -``` +Phoenix desktop builds come from this repository's [GitHub Releases](https://github.com/goodbirdhq/phoenix/releases), or you can build from source (see [docs/internals/overview.md](./docs/internals/overview.md)). -The AUR packaging is maintained in this repository under [`packaging/aur`](./packaging/aur). +> [!NOTE] +> Phoenix is not currently published to `winget`, Homebrew, or the AUR. The upstream packages +> (`T3Tools.T3Code`, `--cask t3-code`, `t3code-bin`, and `t3code-nightly-bin`) install +> **T3 Code**, not Phoenix—which is a perfectly good choice if you would rather run the original. +> Phoenix retains upstream's AUR packaging sources under [`packaging/aur`](./packaging/aur) as a +> reference, but the release workflow does not invoke them because they remain configured for T3 +> Code. ## Some notes -We are very very early in this project. Expect bugs. +This is an early-stage fork. Expect bugs, and expect them to be ours rather than upstream's. -We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be. +Phoenix continuously merges from upstream T3 Code. See [docs/internals/branding.md](./docs/internals/branding.md) for the fork's rebranding policy — it exists specifically to keep those merges clean. ## Documentation @@ -83,7 +84,7 @@ Full docs live in [docs/](./docs). There's no docs site yet. - [Keeping app and server in sync](./docs/user/updating.md) - [Source control integrations](./docs/user/source-control.md) - Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md) -- Linux: [run T3 Code as a background service](./docs/user/background-service.md) +- Linux: [run Phoenix as a background service](./docs/user/background-service.md) Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md). @@ -91,7 +92,7 @@ Building from source? Start at [docs/internals/overview.md](./docs/internals/ove ### Install `vp` -T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool. +Phoenix uses Vite+ so you'll need to install the global `vp` command-line tool. #### macOS / Linux @@ -113,8 +114,10 @@ Checkout their getting started guide for more information: https://viteplus.dev/ vp i ``` -Read [CONTRIBUTING.md](./CONTRIBUTING.md) before reporting a bug or opening a PR. +Read [CONTRIBUTING.md](./CONTRIBUTING.md) before reporting a bug, requesting a feature, or opening a PR. + +Need support with Phoenix? Open an issue on this repository. The [T3 Code Discord](https://discord.gg/jn4EGJjrvv) is for upstream T3 Code — please don't take fork-specific problems there. -Have a feature request? Start an [Ideas discussion](https://github.com/pingdotgg/t3code/discussions/categories/ideas). +## License -Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv). +Phoenix is MIT licensed and retains the [original T3 Code copyright](./LICENSE) held by T3 Tools Inc. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 83a07cccb660..3936f6a93bb6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.0.38", + "version": "0.1.15", "private": true, "type": "module", "main": "dist-electron/main.cjs", @@ -36,5 +36,5 @@ "tailwindcss": "^4.0.0", "vite-plus": "catalog:" }, - "productName": "T3 Code (Alpha)" + "productName": "Phoenix (Alpha)" } diff --git a/apps/desktop/resources/dmg/dmg-background-latest.svg b/apps/desktop/resources/dmg/dmg-background-latest.svg index 132d829b103e..7a93a0dc343b 100644 --- a/apps/desktop/resources/dmg/dmg-background-latest.svg +++ b/apps/desktop/resources/dmg/dmg-background-latest.svg @@ -17,7 +17,7 @@ - T3 CODE + PHOENIX Desktop LATEST RELEASE @@ -27,7 +27,7 @@ - Drag T3 Code to Applications + Drag Phoenix to Applications Open it from Applications when the copy finishes. diff --git a/apps/desktop/resources/dmg/dmg-background-nightly.svg b/apps/desktop/resources/dmg/dmg-background-nightly.svg index 8df5e4c07c36..3630f3aaf448 100644 --- a/apps/desktop/resources/dmg/dmg-background-nightly.svg +++ b/apps/desktop/resources/dmg/dmg-background-nightly.svg @@ -48,7 +48,7 @@ - T3 CODE + PHOENIX Desktop NIGHTLY BUILD @@ -58,7 +58,7 @@ - Drag T3 Code to Applications + Drag Phoenix to Applications Open it from Applications when the copy finishes. diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 07fb87b051f1..eed9e13fcaa4 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -1,4 +1,4 @@ -// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron" +// This file mostly exists because we want dev mode to say "Phoenix (Dev)" instead of "electron" import * as NodeChildProcess from "node:child_process"; import * as NodeFS from "node:fs"; @@ -15,11 +15,11 @@ const repoRoot = NodePath.resolve(desktopDir, "..", ".."); const devBundleIdSuffix = NodePath.basename(repoRoot) .toLowerCase() .replaceAll(/[^a-z0-9]+/g, ""); -export const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; +export const APP_DISPLAY_NAME = isDevelopment ? "Phoenix (Dev)" : "Phoenix (Alpha)"; export const APP_BUNDLE_ID = isDevelopment - ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}` - : "com.t3tools.t3code"; -const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"]; + ? `com.goodbird.phoenix.dev.${devBundleIdSuffix || "local"}` + : "com.goodbird.phoenix"; +const APP_PROTOCOL_SCHEMES = isDevelopment ? ["phoenix-dev"] : ["phoenix"]; const LAUNCHER_VERSION = 15; const developmentMacIconPngPath = NodePath.join( repoRoot, @@ -108,8 +108,8 @@ export function makeDevelopmentLauncherScript({ }) { const envEntries = [ ["VITE_DEV_SERVER_URL", environment.VITE_DEV_SERVER_URL], - ["T3CODE_PORT", environment.T3CODE_PORT], - ["T3CODE_HOME", environment.T3CODE_HOME], + ["PHOENIX_PORT", environment.PHOENIX_PORT], + ["PHOENIX_HOME", environment.PHOENIX_HOME], ["T3CODE_COMMIT_HASH", environment.T3CODE_COMMIT_HASH], ["T3CODE_OTLP_TRACES_URL", environment.T3CODE_OTLP_TRACES_URL], ["T3CODE_OTLP_EXPORT_INTERVAL_MS", environment.T3CODE_OTLP_EXPORT_INTERVAL_MS], @@ -347,7 +347,7 @@ function buildMacLauncher(electronBinaryPath) { if (isDevelopment) { // Keep Electron's native executable inside the branded bundle. Launching the // node_modules copy makes macOS associate the process (and Dock label) with - // Electron.app even though this bundle's Info.plist has the T3 Code name. + // Electron.app even though this bundle's Info.plist has the Phoenix name. // Its conventional executable name also keeps Electron's default-app runtime // in development mode instead of making app.isPackaged report true. writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath); diff --git a/apps/desktop/scripts/electron-launcher.test.mjs b/apps/desktop/scripts/electron-launcher.test.mjs index 1ed5a1b8ebf9..ee2bdd7dddba 100644 --- a/apps/desktop/scripts/electron-launcher.test.mjs +++ b/apps/desktop/scripts/electron-launcher.test.mjs @@ -53,18 +53,18 @@ describe("electron development launcher", () => { it("keeps the native Electron executable name inside the branded macOS bundle", () => { const paths = resolveMacLauncherPaths( - "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app", - "T3 Code (Dev)", + "/repo/apps/desktop/.electron-runtime/Phoenix (Dev).app", + "Phoenix (Dev)", ); - assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher"); + assert.equal(paths.launcherExecutableName, "Phoenix (Dev) Launcher"); assert.equal( paths.launcherBinaryPath, - "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher", + "/repo/apps/desktop/.electron-runtime/Phoenix (Dev).app/Contents/MacOS/Phoenix (Dev) Launcher", ); assert.equal( paths.runtimeElectronBinaryPath, - "/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron", + "/repo/apps/desktop/.electron-runtime/Phoenix (Dev).app/Contents/MacOS/Electron", ); const script = makeDevelopmentLauncherScript({ @@ -75,7 +75,7 @@ describe("electron development launcher", () => { }); assert.include( script, - "exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'", + "exec '/repo/apps/desktop/.electron-runtime/Phoenix (Dev).app/Contents/MacOS/Electron'", ); assert.notInclude(script, "node_modules/electron"); }); diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index d5a8ac3b7836..3f33a84bc135 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -31,7 +31,10 @@ import * as DesktopRemoteUpdates from "../updates/DesktopRemoteUpdates.ts"; import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts"; -const DEFAULT_DESKTOP_BACKEND_PORT = 3773; +// 3873, matching ServerConfig.DEFAULT_PORT. The desktop scans upward from this +// for a free port, so leaving it at upstream's 3773 made Phoenix claim T3 Code's +// port when it started first, and fall forward to 3774 when it did not. +const DEFAULT_DESKTOP_BACKEND_PORT = 3873; const MAX_TCP_PORT = 65_535; const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const; @@ -130,7 +133,7 @@ const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupErr const wasQuitting = yield* Ref.getAndSet(state.quitting, true); if (!wasQuitting) { yield* electronDialog.showErrorBox( - "T3 Code failed to start", + "Phoenix failed to start", `Stage: ${stage}\n${message}${detail}`, ); } diff --git a/apps/desktop/src/app/DesktopAppActivation.ts b/apps/desktop/src/app/DesktopAppActivation.ts index f63fdffedaef..10a3f5695d3d 100644 --- a/apps/desktop/src/app/DesktopAppActivation.ts +++ b/apps/desktop/src/app/DesktopAppActivation.ts @@ -151,7 +151,7 @@ export async function startDesktopAppControlServer(input: { activeRequestId = parsed.requestId; void input.handle(parsed).then(finish, () => { finish( - invalidResponse(parsed.requestId, "T3 Code could not process the desktop app request."), + invalidResponse(parsed.requestId, "Phoenix could not process the desktop app request."), ); }); }); diff --git a/apps/desktop/src/app/DesktopAppActivationBroker.ts b/apps/desktop/src/app/DesktopAppActivationBroker.ts index 221df9ca86dd..86875bc58d5a 100644 --- a/apps/desktop/src/app/DesktopAppActivationBroker.ts +++ b/apps/desktop/src/app/DesktopAppActivationBroker.ts @@ -45,7 +45,7 @@ export class DesktopAppActivationBroker { request(request: DesktopAppActivationRequest): Promise { if (this.#closed) { return Promise.resolve( - failure(request.requestId, "renderer-unavailable", "T3 Code is shutting down."), + failure(request.requestId, "renderer-unavailable", "Phoenix is shutting down."), ); } if (this.#pending.has(request.requestId)) { @@ -90,7 +90,7 @@ export class DesktopAppActivationBroker { failure( pending.request.requestId, "renderer-unavailable", - "The T3 Code window closed before it opened the project.", + "The Phoenix window closed before it opened the project.", ), ); } @@ -103,7 +103,7 @@ export class DesktopAppActivationBroker { cancel(requestId: string): void { this.#settle( - failure(requestId, "renderer-unavailable", "The command closed before T3 Code was ready."), + failure(requestId, "renderer-unavailable", "The command closed before Phoenix was ready."), ); } @@ -112,7 +112,7 @@ export class DesktopAppActivationBroker { this.#renderer = null; for (const pending of this.#pending.values()) { this.#settle( - failure(pending.request.requestId, "renderer-unavailable", "T3 Code is shutting down."), + failure(pending.request.requestId, "renderer-unavailable", "Phoenix is shutting down."), ); } } diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index 5c39ff304b3b..59f048405c5b 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -20,9 +20,9 @@ const defaultEnvironmentInput = { platform: "darwin", processArch: "arm64", appVersion: "1.2.3", - appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar", + appPath: "/Applications/Phoenix.app/Contents/Resources/app.asar", isPackaged: true, - resourcesPath: "/Applications/T3 Code.app/Contents/Resources", + resourcesPath: "/Applications/Phoenix.app/Contents/Resources", runningUnderArm64Translation: false, } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; @@ -39,7 +39,7 @@ interface ElectronAppCalls { const makeElectronAppLayer = (calls: ElectronAppCalls) => Layer.succeed(ElectronApp.ElectronApp, { metadata: Effect.die("unexpected metadata read"), - name: Effect.succeed("T3 Code"), + name: Effect.succeed("Phoenix"), systemLocale: Effect.succeed("en-US"), whenReady: Effect.void, quit: Effect.void, @@ -144,44 +144,31 @@ const withIdentity = ( }; describe("DesktopAppIdentity", () => { - it.effect("keeps using the legacy userData path when it already exists", () => + it.effect("resolves userData to Phoenix's own directory", () => withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const userDataPath = yield* identity.resolveUserDataPath; - assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)"); + assert.equal(userDataPath, "/Users/alice/Library/Application Support/phoenix"); }), - { legacyPathExists: true }, ), ); - it.effect("preserves failures while inspecting the legacy userData path", () => { - const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)"; - const cause = PlatformError.systemError({ - _tag: "PermissionDenied", - module: "FileSystem", - method: "exists", - description: "permission denied", - pathOrDescriptor: legacyPath, - }); - - return withIdentity( + // Phoenix is designed to run alongside upstream T3 Code. If it ever adopted an + // existing T3 Code userData directory, both apps would write the same state. + it.effect("never adopts an existing T3 Code userData directory", () => + withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; - const error = yield* identity.resolveUserDataPath.pipe(Effect.flip); - - assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError); - assert.equal(error.legacyPath, legacyPath); - assert.strictEqual(error.cause, cause); - assert.equal( - error.message, - `Failed to inspect legacy desktop user-data path at "${legacyPath}".`, - ); + const userDataPath = yield* identity.resolveUserDataPath; + + assert.notInclude(userDataPath, "T3 Code"); + assert.equal(userDataPath, "/Users/alice/Library/Application Support/phoenix"); }), - { legacyPathProbeError: cause }, - ); - }); + { legacyPathExists: true }, + ), + ); it.effect("configures app identity from the environment commit override", () => { const calls: ElectronAppCalls = { @@ -195,8 +182,8 @@ describe("DesktopAppIdentity", () => { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; yield* identity.configure; - assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); - assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); + assert.deepEqual(calls.setName, ["Phoenix (Alpha)"]); + assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "Phoenix (Alpha)"); assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); // Packaged: the bundle's own icon stands, so a custom one the user diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts index c5adb8574a53..736999d5c959 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.ts @@ -18,22 +18,10 @@ const AppPackageMetadata = Schema.Struct({ }); const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata)); -export class DesktopUserDataPathResolutionError extends Schema.TaggedErrorClass()( - "DesktopUserDataPathResolutionError", - { - legacyPath: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to inspect legacy desktop user-data path at "${this.legacyPath}".`; - } -} - export class DesktopAppIdentity extends Context.Service< DesktopAppIdentity, { - readonly resolveUserDataPath: Effect.Effect; + readonly resolveUserDataPath: Effect.Effect; readonly configure: Effect.Effect; } >()("@t3tools/desktop/app/DesktopAppIdentity") {} @@ -45,25 +33,13 @@ const normalizeCommitHash = (value: string): Option.Option => { : Option.none(); }; +// Phoenix deliberately has no legacy-userData adoption step. Upstream T3 Code +// migrates its own pre-rename directory here, but Phoenix is a separate app that +// is expected to run alongside T3 Code — adopting T3 Code's userData would make +// two live applications write the same directory. export const resolveUserDataPath = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - const fileSystem = yield* FileSystem.FileSystem; - const legacyPath = environment.path.join( - environment.appDataDirectory, - environment.legacyUserDataDirName, - ); - const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe( - Effect.mapError( - (cause) => - new DesktopUserDataPathResolutionError({ - legacyPath, - cause, - }), - ), - ); - return legacyPathExists - ? legacyPath - : environment.path.join(environment.appDataDirectory, environment.userDataDirName); + return environment.path.join(environment.appDataDirectory, environment.userDataDirName); }).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath")); export const make = Effect.gen(function* () { diff --git a/apps/desktop/src/app/DesktopAssets.test.ts b/apps/desktop/src/app/DesktopAssets.test.ts index bb118d43d29a..3cc2a5c22a39 100644 --- a/apps/desktop/src/app/DesktopAssets.test.ts +++ b/apps/desktop/src/app/DesktopAssets.test.ts @@ -16,9 +16,9 @@ const environmentLayer = DesktopEnvironment.layer({ platform: "darwin", processArch: "arm64", appVersion: "1.2.3", - appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar", + appPath: "/Applications/Phoenix.app/Contents/Resources/app.asar", isPackaged: true, - resourcesPath: "/Applications/T3 Code.app/Contents/Resources", + resourcesPath: "/Applications/Phoenix.app/Contents/Resources", runningUnderArm64Translation: false, }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))); diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index 2f61ca909aef..434a7eec4226 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -34,8 +34,7 @@ const makeDesktopClerkLayer = (isDevelopment = true, events: string[] = []) => { stateDir: "/tmp/t3-state", isDevelopment, appDataDirectory: "/tmp/app-data", - userDataDirName: isDevelopment ? "t3code-dev" : "t3code", - legacyUserDataDirName: isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)", + userDataDirName: isDevelopment ? "phoenix-dev" : "phoenix", path: { join: (...parts: ReadonlyArray) => parts.join("/") }, } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); @@ -64,11 +63,11 @@ describe("DesktopClerk", () => { }); it("derives the Clerk Frontend API hostname used by the desktop CSP", () => { - const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`; + const publishableKey = `pk_test_${btoa("clerk.phoenix.codes$")}`; assert.equal( DesktopClerk.resolveDesktopClerkFrontendApiHostname(publishableKey), - "clerk.t3.codes", + "clerk.phoenix.codes", ); assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname(""), undefined); assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined); @@ -91,7 +90,7 @@ describe("DesktopClerk", () => { { storage: storageAdapter, passkeys: true, - renderer: { scheme: "t3code-dev", host: "app" }, + renderer: { scheme: "phoenix-dev", host: "app" }, }, ], ]); @@ -99,7 +98,7 @@ describe("DesktopClerk", () => { // The bridge acquires Electron's single-instance lock at creation, and // the lock both lives in and creates the userData directory — so the // real path must be set before the bridge exists. - assert.deepEqual(events, ["setPath:userData:/tmp/app-data/t3code-dev", "createClerkBridge"]); + assert.deepEqual(events, ["setPath:userData:/tmp/app-data/phoenix-dev", "createClerkBridge"]); storageMock.mockClear(); createClerkBridgeMock.mockClear(); }); @@ -210,8 +209,8 @@ describe("DesktopClerk", () => { }); it.each([ - { isDevelopment: true, scheme: "t3code-dev" }, - { isDevelopment: false, scheme: "t3code" }, + { isDevelopment: true, scheme: "phoenix-dev" }, + { isDevelopment: false, scheme: "phoenix" }, ])("configures the SDK with the $scheme renderer origin", ({ isDevelopment, scheme }) => { const bridge = { cleanup: vi.fn(), isPrimaryInstance: true }; storageMock.mockReturnValue(storageAdapter); diff --git a/apps/desktop/src/app/DesktopConfig.ts b/apps/desktop/src/app/DesktopConfig.ts index d157a4c6ba44..e2967e874389 100644 --- a/apps/desktop/src/app/DesktopConfig.ts +++ b/apps/desktop/src/app/DesktopConfig.ts @@ -10,9 +10,6 @@ const trimNonEmptyOption = (value: string): Option.Option => { const trimmedString = (name: string) => Config.string(name).pipe(Config.option, Config.map(Option.flatMap(trimNonEmptyOption))); -const optionalBoolean = (name: string) => - Config.boolean(name).pipe(Config.option, Config.map(Option.getOrElse(() => false))); - const commaSeparatedStrings = (name: string) => trimmedString(name).pipe( Config.map( @@ -27,6 +24,30 @@ const commaSeparatedStrings = (name: string) => ), ); +/** + * Phoenix reads its own `PHOENIX_*` variable first and falls back to the upstream + * `T3CODE_*` name, so existing setups keep working without configuration changes. + * + * `PHOENIX_HOME` deliberately does NOT use this helper — see `t3Home` below. + */ +const brandedString = (suffix: string) => + Config.all([trimmedString(`PHOENIX_${suffix}`), trimmedString(`T3CODE_${suffix}`)]).pipe( + Config.map(([phoenix, upstream]) => Option.orElse(phoenix, () => upstream)), + ); + +const brandedBoolean = (suffix: string) => + Config.all([ + Config.boolean(`PHOENIX_${suffix}`).pipe(Config.option), + Config.boolean(`T3CODE_${suffix}`).pipe(Config.option), + ]).pipe( + Config.map(([phoenix, upstream]) => + Option.getOrElse( + Option.orElse(phoenix, () => upstream), + () => false, + ), + ), + ); + const compactEnv = (env: Readonly>): Record => Object.fromEntries( Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined), @@ -36,21 +57,28 @@ export const DesktopConfig = Config.all({ appDataDirectory: trimmedString("APPDATA"), xdgConfigHome: trimmedString("XDG_CONFIG_HOME"), xdgDataHome: trimmedString("XDG_DATA_HOME"), - t3Home: trimmedString("T3CODE_HOME"), + // No T3CODE_HOME fallback on purpose. Every other variable is safe to share with + // upstream, but the base dir holds the SQLite database and auth state: inheriting + // a T3CODE_HOME set for T3 Code would put both apps in one directory, which is the + // exact collision Phoenix's separate identity exists to prevent. + t3Home: trimmedString("PHOENIX_HOME"), devServerUrl: Config.url("VITE_DEV_SERVER_URL").pipe(Config.option), - appUserModelIdOverride: trimmedString("T3CODE_DESKTOP_APP_USER_MODEL_ID"), - devRemoteT3ServerEntryPath: trimmedString("T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH"), - configuredBackendPort: Config.port("T3CODE_PORT").pipe(Config.option), - commitHashOverride: trimmedString("T3CODE_COMMIT_HASH"), - desktopLanHostOverride: trimmedString("T3CODE_DESKTOP_LAN_HOST"), + appUserModelIdOverride: brandedString("DESKTOP_APP_USER_MODEL_ID"), + devRemoteT3ServerEntryPath: brandedString("DEV_REMOTE_T3_SERVER_ENTRY_PATH"), + configuredBackendPort: Config.all([ + Config.port("PHOENIX_PORT").pipe(Config.option), + Config.port("T3CODE_PORT").pipe(Config.option), + ]).pipe(Config.map(([phoenix, upstream]) => Option.orElse(phoenix, () => upstream))), + commitHashOverride: brandedString("COMMIT_HASH"), + desktopLanHostOverride: brandedString("DESKTOP_LAN_HOST"), desktopHttpsEndpointUrls: commaSeparatedStrings("T3CODE_DESKTOP_HTTPS_ENDPOINTS"), - otlpTracesUrl: trimmedString("T3CODE_OTLP_TRACES_URL"), + otlpTracesUrl: brandedString("OTLP_TRACES_URL"), otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe( Config.withDefault(10_000), ), appImagePath: trimmedString("APPIMAGE"), - disableAutoUpdate: optionalBoolean("T3CODE_DISABLE_AUTO_UPDATE"), - mockUpdates: optionalBoolean("T3CODE_DESKTOP_MOCK_UPDATES"), + disableAutoUpdate: brandedBoolean("DISABLE_AUTO_UPDATE"), + mockUpdates: brandedBoolean("DESKTOP_MOCK_UPDATES"), mockUpdateServerPort: Config.port("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe( Config.withDefault(3000), ), diff --git a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts index c58830b30a7f..5309de2bf0ac 100644 --- a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts +++ b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts @@ -61,7 +61,7 @@ function makeLayer( runningUnderArm64Translation: false, }).pipe( Layer.provide( - Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), + Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ PHOENIX_HOME: baseDir })), ), ); const safeStorageLayer = makeSafeStorageLayer(encryptionAvailable, failDecrypt); diff --git a/apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts b/apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts index b7647b5cc10f..0e4e060934a0 100644 --- a/apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts +++ b/apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts @@ -12,11 +12,11 @@ describe("DesktopEarlyElectronStartup", () => { it("reads the persisted linux password-store preference before Electron is ready", () => { const preference = resolveEarlyLinuxPasswordStorePreference({ - env: { T3CODE_HOME: "/home/user/.t3-test" }, + env: { PHOENIX_HOME: "/home/user/.phoenix-test" }, homeDirectory: "/home/user", joinPath, readFileString: (path) => { - assert.equal(path, "/home/user/.t3-test/userdata/desktop-settings.json"); + assert.equal(path, "/home/user/.phoenix-test/userdata/desktop-settings.json"); return JSON.stringify({ linuxPasswordStore: "kwallet6" }); }, }); @@ -26,7 +26,7 @@ describe("DesktopEarlyElectronStartup", () => { it("accepts JSONC in the early desktop settings file", () => { const preference = resolveEarlyLinuxPasswordStorePreference({ - env: { T3CODE_HOME: "/home/user/.t3-test" }, + env: { PHOENIX_HOME: "/home/user/.phoenix-test" }, homeDirectory: "/home/user", joinPath, readFileString: () => `{ @@ -53,7 +53,7 @@ describe("DesktopEarlyElectronStartup", () => { it("preserves absolute root paths when resolving early settings", () => { const preference = resolveEarlyLinuxPasswordStorePreference({ - env: { T3CODE_HOME: "/" }, + env: { PHOENIX_HOME: "/" }, homeDirectory: "/home/user", joinPath, readFileString: (path) => { @@ -68,25 +68,25 @@ describe("DesktopEarlyElectronStartup", () => { it("resolves the early linux Electron switches", () => { const options = resolveEarlyLinuxElectronOptions({ env: { - T3CODE_HOME: "/home/user/.t3-test", + PHOENIX_HOME: "/home/user/.phoenix-test", XDG_CURRENT_DESKTOP: "niri", VITE_DEV_SERVER_URL: "http://127.0.0.1:5173", }, homeDirectory: "/home/user", joinPath, readFileString: (path) => { - assert.equal(path, "/home/user/.t3-test/userdata/desktop-settings.json"); + assert.equal(path, "/home/user/.phoenix-test/userdata/desktop-settings.json"); return JSON.stringify({ linuxPasswordStore: "auto" }); }, }); assert.deepEqual(options, { - linuxWmClass: "t3code-dev", + linuxWmClass: "phoenix-dev", passwordStore: "gnome-libsecret", }); }); - it("keeps implicit development state under ~/.t3/dev when T3CODE_HOME is unset", () => { + it("keeps implicit development state under ~/.phoenix/dev when PHOENIX_HOME is unset", () => { const preference = resolveEarlyLinuxPasswordStorePreference({ env: { VITE_DEV_SERVER_URL: "http://127.0.0.1:5173", @@ -94,7 +94,7 @@ describe("DesktopEarlyElectronStartup", () => { homeDirectory: "/home/user", joinPath, readFileString: (path) => { - assert.equal(path, "/home/user/.t3/dev/desktop-settings.json"); + assert.equal(path, "/home/user/.phoenix/dev/desktop-settings.json"); return JSON.stringify({ linuxPasswordStore: "kwallet" }); }, }); @@ -102,16 +102,16 @@ describe("DesktopEarlyElectronStartup", () => { assert.equal(preference, "kwallet"); }); - it("treats whitespace-only T3CODE_HOME as unconfigured in development", () => { + it("treats whitespace-only PHOENIX_HOME as unconfigured in development", () => { const preference = resolveEarlyLinuxPasswordStorePreference({ env: { - T3CODE_HOME: " ", + PHOENIX_HOME: " ", VITE_DEV_SERVER_URL: "http://127.0.0.1:5173", }, homeDirectory: "/home/user", joinPath, readFileString: (path) => { - assert.equal(path, "/home/user/.t3/dev/desktop-settings.json"); + assert.equal(path, "/home/user/.phoenix/dev/desktop-settings.json"); return JSON.stringify({ linuxPasswordStore: "gnome-libsecret" }); }, }); diff --git a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts index 3e11d7961a9f..23004c3e337c 100644 --- a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts +++ b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts @@ -49,7 +49,7 @@ function resolveEarlyDesktopSettingsPath(input: { readonly homeDirectory: string; readonly joinPath: JoinPath; }): string { - const t3Home = Option.fromUndefinedOr(input.env.T3CODE_HOME); + const t3Home = Option.fromUndefinedOr(input.env.PHOENIX_HOME); const baseDir = resolveDesktopBaseDir({ homeDirectory: input.homeDirectory, joinPath: input.joinPath, @@ -81,7 +81,7 @@ export function resolveEarlyLinuxElectronOptions( ): EarlyLinuxElectronOptions { const preference = resolveEarlyLinuxPasswordStorePreference(input); return { - linuxWmClass: isDevelopmentEnvironment(input.env) ? "t3code-dev" : "t3code", + linuxWmClass: isDevelopmentEnvironment(input.env) ? "phoenix-dev" : "phoenix", passwordStore: resolveLinuxPasswordStoreSwitch({ preference, env: input.env, diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts index 218e2c3e4ba2..7e738fdf9631 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts @@ -13,9 +13,9 @@ const defaultInput = { platform: "darwin", processArch: "arm64", appVersion: "0.0.22", - appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar", + appPath: "/Applications/Phoenix.app/Contents/Resources/app.asar", isPackaged: false, - resourcesPath: "/Applications/T3 Code.app/Contents/Resources", + resourcesPath: "/Applications/Phoenix.app/Contents/Resources", runningUnderArm64Translation: false, } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; @@ -40,7 +40,7 @@ describe("DesktopEnvironment", () => { const environment = yield* makeEnvironment( {}, { - T3CODE_HOME: " /tmp/t3 ", + PHOENIX_HOME: " /tmp/t3 ", T3CODE_COMMIT_HASH: " 0123456789abcdef ", T3CODE_PORT: "4949", VITE_DEV_SERVER_URL: "http://localhost:5173", @@ -68,8 +68,8 @@ describe("DesktopEnvironment", () => { assert.equal(environment.serverRoot, "/repo"); assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs"); assert.equal(environment.backendCwd, "/repo"); - assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev"); - assert.equal(environment.linuxWmClass, "t3code-dev"); + assert.equal(environment.appUserModelId, "com.goodbird.phoenix.dev"); + assert.equal(environment.linuxWmClass, "phoenix-dev"); assert.deepEqual( Option.map(environment.devServerUrl, (url) => url.href), Option.some("http://localhost:5173/"), @@ -87,7 +87,7 @@ describe("DesktopEnvironment", () => { const environment = yield* makeEnvironment( {}, { - T3CODE_HOME: "/tmp/t3", + PHOENIX_HOME: "/tmp/t3", }, ); @@ -125,8 +125,8 @@ describe("DesktopEnvironment", () => { ); const production = yield* makeEnvironment(); - assert.equal(development.stateDir, "/Users/alice/.t3/dev"); - assert.equal(production.stateDir, "/Users/alice/.t3/userdata"); + assert.equal(development.stateDir, "/Users/alice/.phoenix/dev"); + assert.equal(production.stateDir, "/Users/alice/.phoenix/userdata"); }), ); @@ -135,12 +135,12 @@ describe("DesktopEnvironment", () => { const environment = yield* makeEnvironment( {}, { - T3CODE_DESKTOP_APP_USER_MODEL_ID: " com.t3tools.t3code.dev.local ", + T3CODE_DESKTOP_APP_USER_MODEL_ID: " com.goodbird.phoenix.dev.local ", VITE_DEV_SERVER_URL: "http://localhost:5173", }, ); - assert.equal(environment.appUserModelId, "com.t3tools.t3code.dev.local"); + assert.equal(environment.appUserModelId, "com.goodbird.phoenix.dev.local"); }), ); diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index 4583e5124091..df2b47956fa5 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -77,7 +77,6 @@ export class DesktopEnvironment extends Context.Service< readonly linuxApplicationsDir: string; readonly appImagePath: Option.Option; readonly userDataDirName: string; - readonly legacyUserDataDirName: string; readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings; readonly runtimeInfo: DesktopRuntimeInfo; readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option; @@ -85,7 +84,7 @@ export class DesktopEnvironment extends Context.Service< } >()("@t3tools/desktop/app/DesktopEnvironment") {} -const APP_BASE_NAME = "T3 Code"; +const APP_BASE_NAME = "Phoenix"; function resolveDesktopAppStageLabel(input: { readonly isDevelopment: boolean; @@ -178,8 +177,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( joinPath: path.join, t3Home: config.t3Home, }); - const userDataDirName = isDevelopment ? "t3code-dev" : "t3code"; - const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; + const userDataDirName = isDevelopment ? "phoenix-dev" : "phoenix"; const linuxApplicationsDir = path.join( Option.getOrElse(config.xdgDataHome, () => path.join(homeDirectory, ".local", "share")), "applications", @@ -224,14 +222,13 @@ const make = Effect.fn("desktop.environment.make")(function* ( branding, displayName, appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () => - isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code", + isDevelopment ? "com.goodbird.phoenix.dev" : "com.goodbird.phoenix", ), - linuxDesktopEntryName: isDevelopment ? "t3code-dev.desktop" : "t3code.desktop", - linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", + linuxDesktopEntryName: isDevelopment ? "phoenix-dev.desktop" : "phoenix.desktop", + linuxWmClass: isDevelopment ? "phoenix-dev" : "phoenix", linuxApplicationsDir, appImagePath: config.appImagePath, userDataDirName, - legacyUserDataDirName, defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion), runtimeInfo: resolveDesktopRuntimeInfo({ platform: input.platform, diff --git a/apps/desktop/src/app/DesktopLifecycle.test.ts b/apps/desktop/src/app/DesktopLifecycle.test.ts index 0086edf20bf9..cebf09fcd4f4 100644 --- a/apps/desktop/src/app/DesktopLifecycle.test.ts +++ b/apps/desktop/src/app/DesktopLifecycle.test.ts @@ -32,7 +32,7 @@ function makeElectronAppLayer( return Layer.succeed(ElectronApp.ElectronApp, { metadata: Effect.die("unexpected metadata read"), - name: Effect.succeed("T3 Code"), + name: Effect.succeed("Phoenix"), systemLocale: Effect.succeed("en-US"), whenReady: Effect.void, quit, diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts index 30183808a152..16001c0b3ccd 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts @@ -22,8 +22,8 @@ const makeEnvironment = (overrides: Record = {}) => platform: "linux", isPackaged: true, isDevelopment: false, - displayName: "T3 Code (Alpha)", - linuxWmClass: "t3code", + displayName: "Phoenix (Alpha)", + linuxWmClass: "phoenix", linuxApplicationsDir: "/home/alice/.local/share/applications", appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"), path: { join: (...parts: ReadonlyArray) => parts.join("/") }, @@ -105,13 +105,13 @@ const emptyRecording = (): RecordedRegistration => ({ describe("DesktopLinuxUrlHandler", () => { it("renders a scheme-handler desktop entry with freedesktop Exec quoting", () => { const entry = DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({ - displayName: "T3 Code (Nightly)", + displayName: "Phoenix (Nightly)", execTarget: '/home/al ice/Apps/T3 "100%" $HOME\\x.AppImage', - scheme: "t3code", + scheme: "phoenix", }); assert.include(entry, "[Desktop Entry]"); - assert.include(entry, "Name=T3 Code (Nightly)"); + assert.include(entry, "Name=Phoenix (Nightly)"); // Exec composes both escaping layers: a literal backslash becomes four // backslashes in the file, a quote three characters, a dollar sign two // backslashes plus the sign. @@ -121,33 +121,33 @@ describe("DesktopLinuxUrlHandler", () => { ); assert.include(entry, "NoDisplay=true"); assert.notInclude(entry, "StartupWMClass="); - assert.include(entry, "MimeType=x-scheme-handler/t3code;"); + assert.include(entry, "MimeType=x-scheme-handler/phoenix;"); }); it("carries structured context on registration errors", () => { const writeError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({ step: "write-desktop-entry", - scheme: "t3code", - desktopEntryPath: "/home/alice/.local/share/applications/t3code-url-handler.desktop", + scheme: "phoenix", + desktopEntryPath: "/home/alice/.local/share/applications/phoenix-url-handler.desktop", cause: new Error("boom"), }); assert.equal( writeError.message, - "Failed to register the t3code:// URL handler (step: write-desktop-entry).", + "Failed to register the phoenix:// URL handler (step: write-desktop-entry).", ); assert.equal( writeError.desktopEntryPath, - "/home/alice/.local/share/applications/t3code-url-handler.desktop", + "/home/alice/.local/share/applications/phoenix-url-handler.desktop", ); const exitError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({ step: "set-default-handler", - scheme: "t3code", + scheme: "phoenix", exitCode: 4, }); assert.equal( exitError.message, - "Failed to register the t3code:// URL handler (step: set-default-handler, xdg-mime exit code 4).", + "Failed to register the phoenix:// URL handler (step: set-default-handler, xdg-mime exit code 4).", ); }); @@ -161,17 +161,17 @@ describe("DesktopLinuxUrlHandler", () => { assert.equal(recorded.files.length, 1); assert.equal( recorded.files[0]?.path, - "/home/alice/.local/share/applications/t3code-url-handler.desktop", + "/home/alice/.local/share/applications/phoenix-url-handler.desktop", ); assert.include( recorded.files[0]?.content, 'Exec="/home/alice/Applications/T3-Code.AppImage" %U', ); - assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/t3code;"); + assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/phoenix;"); assert.deepEqual(recorded.commands, [ { command: "xdg-mime", - args: ["default", "t3code-url-handler.desktop", "x-scheme-handler/t3code"], + args: ["default", "phoenix-url-handler.desktop", "x-scheme-handler/phoenix"], }, ]); }); @@ -218,7 +218,7 @@ describe("DesktopLinuxUrlHandler", () => { module: "FileSystem", method: "writeFileString", description: "read-only filesystem", - pathOrDescriptor: "/home/alice/.local/share/applications/t3code-url-handler.desktop", + pathOrDescriptor: "/home/alice/.local/share/applications/phoenix-url-handler.desktop", }), }); diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts index e531a54dfce6..11f832a07528 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts @@ -20,7 +20,7 @@ import { makeComponentLogger } from "./DesktopObservability.ts"; // our own handler entry pointing at the current AppImage and claim the // scheme default via xdg-mime, exactly what the file manager's "set as // default" checkbox would record in mimeapps.list. -export const URL_HANDLER_DESKTOP_ENTRY_NAME = "t3code-url-handler.desktop"; +export const URL_HANDLER_DESKTOP_ENTRY_NAME = "phoenix-url-handler.desktop"; const { logInfo, logWarning } = makeComponentLogger("desktop-linux-url-handler"); diff --git a/apps/desktop/src/app/DesktopObservability.test.ts b/apps/desktop/src/app/DesktopObservability.test.ts index cd90e7951b8d..3c2700b9bfb5 100644 --- a/apps/desktop/src/app/DesktopObservability.test.ts +++ b/apps/desktop/src/app/DesktopObservability.test.ts @@ -55,7 +55,7 @@ const makeEnvironmentLayer = (baseDir: string, isDevelopment = true) => Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ - T3CODE_HOME: baseDir, + PHOENIX_HOME: baseDir, VITE_DEV_SERVER_URL: isDevelopment ? "http://127.0.0.1:5733" : undefined, }), ), diff --git a/apps/desktop/src/app/DesktopStatePaths.ts b/apps/desktop/src/app/DesktopStatePaths.ts index 006dd97092d4..9e228f3e6cca 100644 --- a/apps/desktop/src/app/DesktopStatePaths.ts +++ b/apps/desktop/src/app/DesktopStatePaths.ts @@ -15,8 +15,10 @@ export function resolveDesktopBaseDir(input: { readonly joinPath: JoinPath; readonly t3Home: Option.Option; }): string { + // ".phoenix", not ".t3": Phoenix is expected to run alongside upstream T3 Code, + // and sharing a base dir would mean sharing one SQLite database and auth state. return Option.getOrElse(normalizeConfiguredBaseDir(input.t3Home), () => - input.joinPath(input.homeDirectory, ".t3"), + input.joinPath(input.homeDirectory, ".phoenix"), ); } diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index accfdf70b3a3..a0ff4bb1ec17 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -77,7 +77,7 @@ function makeEnvironmentLayer( Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ - T3CODE_HOME: baseDir, + PHOENIX_HOME: baseDir, T3CODE_PORT: "9999", T3CODE_MODE: "desktop", T3CODE_DESKTOP_LAN_HOST: "192.168.1.50", @@ -384,7 +384,7 @@ describe("DesktopBackendConfiguration", () => { }> = []; const observedNodePtyRoots: string[] = []; let legacyCleanupCount = 0; - const linuxAppRoot = "/home/test/.t3/wsl-runtime/1.2.3-x64"; + const linuxAppRoot = "/home/test/.phoenix/wsl-runtime/1.2.3-x64"; return withPackagedWslHarness( { @@ -501,7 +501,7 @@ describe("DesktopBackendConfiguration", () => { it.effect("resolveWsl retires a staged runtime that cannot load node-pty", () => { const archiveHash = "c".repeat(64); - const stagedAppRoot = `/home/test/.t3/wsl-runtime/sha256-${archiveHash}`; + const stagedAppRoot = `/home/test/.phoenix/wsl-runtime/sha256-${archiveHash}`; const observedNodePtyRoots: string[] = []; const invalidatedRuntimeIds: string[] = []; return withPackagedWslHarness( @@ -537,7 +537,7 @@ describe("DesktopBackendConfiguration", () => { }); it.effect("resolveWsl keeps the staged runtime when the mounted tree fails too", () => { - const stagedAppRoot = "/home/test/.t3/wsl-runtime/cache"; + const stagedAppRoot = "/home/test/.phoenix/wsl-runtime/cache"; const invalidatedRuntimeIds: string[] = []; return withPackagedWslHarness( { @@ -572,7 +572,7 @@ describe("DesktopBackendConfiguration", () => { }); it.effect("resolveWsl keeps WSL retryable when the mounted fallback fails transiently", () => { - const stagedAppRoot = "/home/test/.t3/wsl-runtime/cache"; + const stagedAppRoot = "/home/test/.phoenix/wsl-runtime/cache"; const invalidatedRuntimeIds: string[] = []; return withPackagedWslHarness( { @@ -617,7 +617,7 @@ describe("DesktopBackendConfiguration", () => { wsl: () => ({ prepareRuntime: () => ({ ok: true, - linuxAppRoot: "/home/test/.t3/wsl-runtime/cache", + linuxAppRoot: "/home/test/.phoenix/wsl-runtime/cache", }), invalidateRuntime: (_distro, runtimeId) => Effect.sync(() => { diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index 4c43070b5f97..75922df80f76 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -75,6 +75,10 @@ const emptyBackendObservabilitySettings: BackendObservabilitySettings = { }; const DESKTOP_BACKEND_ENV_NAMES = [ + // Cleared alongside T3CODE_PORT: the server prefers PHOENIX_PORT, so an ambient + // value (the dev runner exports one) would otherwise outrank the port this + // backend was actually configured with. + "PHOENIX_PORT", "T3CODE_PORT", "T3CODE_MODE", "T3CODE_NO_BROWSER", @@ -508,7 +512,7 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv ...backendChildEnvPatch(), ELECTRON_RUN_AS_NODE: "1", }, - // Primary wants process.env (PATH, dev-runner's T3CODE_HOME, etc.). + // Primary wants process.env (PATH, dev-runner's PHOENIX_HOME, etc.). extendEnv: true, bootstrap, bootstrapDelivery: "fd3", @@ -650,14 +654,15 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl } } - // Build an explicit copy of process.env minus T3CODE_HOME (dev-runner - // exports the Windows-side base dir for the primary; if it leaks into - // the WSL backend the Linux side ends up sharing C:\Users\...\.t3 via - // /mnt/c, which means both backends read/write the same database and - // their env-ids collide). + // Build an explicit copy of process.env minus the base-dir variables + // (dev-runner exports the Windows-side base dir for the primary; if it leaks + // into the WSL backend the Linux side ends up sharing C:\Users\...\.phoenix + // via /mnt/c, which means both backends read/write the same database and + // their env-ids collide). T3CODE_HOME is stripped too so an ambient upstream + // value cannot reach the WSL backend either. const parentEnvWithoutT3Home: Record = {}; for (const [key, value] of Object.entries(process.env)) { - if (key === "T3CODE_HOME") continue; + if (key === "PHOENIX_HOME" || key === "T3CODE_HOME") continue; parentEnvWithoutT3Home[key] = value; } const wslEnv = mergeWslEnv(parentEnvWithoutT3Home.WSLENV, forwardedEnvNames); @@ -673,7 +678,7 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl ...forwardedEnv, ...(wslEnv !== undefined ? { WSLENV: wslEnv } : {}), }, - // env is already a complete process.env minus T3CODE_HOME; pass it + // env is already a complete process.env minus the base-dir variables; pass it // verbatim instead of letting the spawner re-merge process.env on top. extendEnv: false, bootstrap, diff --git a/apps/desktop/src/backend/DesktopBackendPool.ts b/apps/desktop/src/backend/DesktopBackendPool.ts index 27e24d55c6bd..ef499a7c571c 100644 --- a/apps/desktop/src/backend/DesktopBackendPool.ts +++ b/apps/desktop/src/backend/DesktopBackendPool.ts @@ -243,7 +243,7 @@ export const layer = Layer.effect( ); yield* electronDialog.showErrorBox( "WSL backend is still unavailable", - `${reason}\n\nT3 Code will use the Windows backend for this launch and retry WSL the next time the app starts.`, + `${reason}\n\nPhoenix will use the Windows backend for this launch and retry WSL the next time the app starts.`, ); yield* appSettings.applyWslWindowsFallbackInMemory; return true; @@ -254,7 +254,7 @@ export const layer = Layer.effect( }); yield* electronDialog.showErrorBox( "WSL backend couldn't start", - `${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.`, + `${reason}\n\nFalling back to the Windows backend so Phoenix can open. Re-enable the WSL backend from Settings > Connections once the WSL distro is fixed.`, ); // Fully disable the WSL backend — both flags, matching the "Switch to // Windows" recovery path — so the manager's next restart re-resolves the diff --git a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts index 201492f0e4c1..c7c7172757d3 100644 --- a/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts +++ b/apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts @@ -71,7 +71,7 @@ export const make = Effect.gen(function* () { httpBaseUrl: config.httpBaseUrl.href, credential, clientMetadata: { - label: "T3 Code Desktop", + label: "Phoenix Desktop", deviceType: "desktop", }, }).pipe( diff --git a/apps/desktop/src/backend/DesktopServerExposure.test.ts b/apps/desktop/src/backend/DesktopServerExposure.test.ts index dcfee93778d1..bc0c7f69fac3 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.test.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.test.ts @@ -81,7 +81,10 @@ function makeEnvironmentLayer(baseDir: string, env: Record; readonly desktopSettingsLayer?: Layer.Layer; }) { - const env = { T3CODE_HOME: input.baseDir, ...input.env }; + const env = { PHOENIX_HOME: input.baseDir, ...input.env }; const environmentLayer = makeEnvironmentLayer(input.baseDir, env); const networkLayer = Layer.succeed(DesktopNetworkInterfaces.DesktopNetworkInterfaces, { read: Effect.succeed(input.networkInterfaces ?? emptyNetworkInterfaces), diff --git a/apps/desktop/src/electron/ElectronApp.test.ts b/apps/desktop/src/electron/ElectronApp.test.ts index 4189ea793e2d..f1f1a36db45a 100644 --- a/apps/desktop/src/electron/ElectronApp.test.ts +++ b/apps/desktop/src/electron/ElectronApp.test.ts @@ -66,7 +66,7 @@ vi.mock("electron", () => ({ getVersion: getVersionMock, isDefaultProtocolClient: isDefaultProtocolClientMock, isPackaged: true, - name: "T3 Code", + name: "Phoenix", on: onMock, quit: quitMock, relaunch: relaunchMock, diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index fabd598d7ffa..f8ab4461380f 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -9,8 +9,10 @@ import * as Scope from "effect/Scope"; import * as Electron from "electron"; export const DESKTOP_HOST = "app"; -export const DESKTOP_PRODUCTION_SCHEME = "t3code"; -export const DESKTOP_DEVELOPMENT_SCHEME = "t3code-dev"; +// Phoenix registers its own scheme so it can be installed alongside upstream +// T3 Code; two apps claiming "t3code://" would leave the OS to pick a winner. +export const DESKTOP_PRODUCTION_SCHEME = "phoenix"; +export const DESKTOP_DEVELOPMENT_SCHEME = "phoenix-dev"; export function getDesktopScheme(isDevelopment: boolean): string { return isDevelopment ? DESKTOP_DEVELOPMENT_SCHEME : DESKTOP_PRODUCTION_SCHEME; diff --git a/apps/desktop/src/electron/ElectronShell.test.ts b/apps/desktop/src/electron/ElectronShell.test.ts index 9ae6f502b000..6a45c1240a53 100644 --- a/apps/desktop/src/electron/ElectronShell.test.ts +++ b/apps/desktop/src/electron/ElectronShell.test.ts @@ -19,6 +19,19 @@ vi.mock("electron", () => ({ import * as ElectronShell from "./ElectronShell.ts"; describe("ElectronShell", () => { + it.effect("allows the narrow VS Code Remote-SSH protocol", () => + Effect.gen(function* () { + openExternalMock.mockResolvedValue(undefined); + const shell = yield* ElectronShell.ElectronShell; + assert.equal( + yield* shell.openExternal("vscode://vscode-remote/ssh-remote+work/home/me/project/file.ts"), + true, + ); + assert.deepEqual(openExternalMock.mock.calls, [ + ["vscode://vscode-remote/ssh-remote+work/home/me/project/file.ts"], + ]); + }).pipe(Effect.provide(ElectronShell.layer)), + ); beforeEach(() => { openExternalMock.mockReset(); writeTextMock.mockReset(); diff --git a/apps/desktop/src/electron/ElectronWindow.test.ts b/apps/desktop/src/electron/ElectronWindow.test.ts index bebb0e5c4178..de165d4c71cd 100644 --- a/apps/desktop/src/electron/ElectronWindow.test.ts +++ b/apps/desktop/src/electron/ElectronWindow.test.ts @@ -53,7 +53,7 @@ describe("ElectronWindow", () => { throw cause; }); const options = { - title: "T3 Code", + title: "Phoenix", width: 1100, height: 780, minWidth: 840, @@ -81,7 +81,7 @@ describe("ElectronWindow", () => { assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError); assert.isTrue(ElectronWindow.isElectronWindowCreateError(error)); assert.deepEqual(error.options, { - title: "T3 Code", + title: "Phoenix", width: 1100, height: 780, minWidth: 840, @@ -104,7 +104,7 @@ describe("ElectronWindow", () => { assert.isFalse("icon" in error.options); assert.isFalse("spellcheck" in error.options.webPreferences); assert.strictEqual(error.cause, cause); - assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).'); + assert.equal(error.message, 'Failed to create Electron BrowserWindow "Phoenix" (1100x780).'); assert.notInclude(error.message, cause.message); assert.deepEqual(browserWindowMock.mock.calls, [[options]]); }).pipe(Effect.provide(TestLayer)), diff --git a/apps/desktop/src/linuxSecretStorage.ts b/apps/desktop/src/linuxSecretStorage.ts index fe3e21eadb92..82045dc880cd 100644 --- a/apps/desktop/src/linuxSecretStorage.ts +++ b/apps/desktop/src/linuxSecretStorage.ts @@ -112,11 +112,11 @@ export function resolveLinuxSecretStorageUnavailableMessage(input: { } function getGnomeKeyringRemediationMessage(): string { - return "T3 Code could not access GNOME Keyring to save this environment credential. Install and start GNOME Keyring, then restart T3 Code."; + return "Phoenix could not access GNOME Keyring to save this environment credential. Install and start GNOME Keyring, then restart Phoenix."; } function getKWalletRemediationMessage(): string { - return "T3 Code could not access KWallet to save this environment credential. Enable the KDE wallet subsystem in System Settings, then restart T3 Code."; + return "Phoenix could not access KWallet to save this environment credential. Enable the KDE wallet subsystem in System Settings, then restart Phoenix."; } // Advisory only: this picks between the GNOME Keyring and KWallet wording in the failure notice. It diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c826c56e1a70..7b9519399d58 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -16,7 +16,6 @@ import * as Electron from "electron"; import * as NetService from "@t3tools/shared/Net"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; -import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; import serverPackageJson from "../../server/package.json" with { type: "json" }; @@ -84,7 +83,6 @@ const desktopEnvironmentLayer = Layer.unwrap( const resolveDesktopSshCliRunner = ( environment: DesktopEnvironment.DesktopEnvironment["Service"], - settings: DesktopAppSettings.DesktopSettings, ): RemoteT3RunnerOptions => { const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath); if (environment.isDevelopment && devRemoteEntryPath !== undefined) { @@ -94,11 +92,6 @@ const resolveDesktopSshCliRunner = ( }; } return { - packageSpec: resolveRemoteT3CliPackageSpec({ - appVersion: environment.appVersion, - updateChannel: settings.updateChannel, - isDevelopment: environment.isDevelopment, - }), nodeEngineRange: serverPackageJson.engines.node, }; }; @@ -106,11 +99,8 @@ const resolveDesktopSshCliRunner = ( const desktopSshEnvironmentLayer = Layer.unwrap( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - const settings = yield* DesktopAppSettings.DesktopAppSettings; return DesktopSshEnvironment.layer({ - resolveCliRunner: settings.get.pipe( - Effect.map((currentSettings) => resolveDesktopSshCliRunner(environment, currentSettings)), - ), + resolveCliRunner: Effect.succeed(resolveDesktopSshCliRunner(environment)), }); }), ); diff --git a/apps/desktop/src/settings/DesktopAppSettings.test.ts b/apps/desktop/src/settings/DesktopAppSettings.test.ts index 64c59749abe9..2df267c1f125 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.test.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.test.ts @@ -52,7 +52,7 @@ function makeEnvironmentLayer(baseDir: string, appVersion = "0.0.17") { runningUnderArm64Translation: false, }).pipe( Layer.provide( - Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), + Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ PHOENIX_HOME: baseDir })), ), ); } diff --git a/apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts b/apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts index 5034df44cf70..bf171ee59a01 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts @@ -32,7 +32,7 @@ function makeLayer(fileSystemLayer: Layer.Layer) { runningUnderArm64Translation: false, }).pipe( Layer.provide( - Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), + Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ PHOENIX_HOME: baseDir })), ), ); diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 95c5cc022c6d..14029415971e 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -1,6 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; -import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; +import { ClientSettingsSchema, EnvironmentId, type ClientSettings } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -27,6 +27,10 @@ const clientSettings: ClientSettings = { contextWindowMeterEnabled: false, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, + environmentAppearance: { + [EnvironmentId.make("environment-1")]: { icon: "server", alias: "Build server" }, + }, + environmentEditorHandoffs: {}, environmentIdentificationMode: "artwork", favorites: [], fontFamilyCode: "", @@ -49,6 +53,8 @@ const clientSettings: ClientSettings = { sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, + sidebarSessionHierarchyEnabled: true, + sidebarAttentionFirstEnabled: true, legacySidebarEnabled: false, timestampFormat: "24-hour", wordWrap: true, @@ -71,7 +77,7 @@ function makeLayer(baseDir: string) { runningUnderArm64Translation: false, }).pipe( Layer.provide( - Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), + Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ PHOENIX_HOME: baseDir })), ), ); diff --git a/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts b/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts index 05b1ca144444..c6f1fefd5e3b 100644 --- a/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts +++ b/apps/desktop/src/settings/DesktopSavedEnvironments.test.ts @@ -112,7 +112,7 @@ function makeLayer( runningUnderArm64Translation: false, }).pipe( Layer.provide( - Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), + Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ PHOENIX_HOME: baseDir })), ), ); diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.test.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.test.ts index 1fe2b86aae7b..bf455e9c701e 100644 --- a/apps/desktop/src/ssh/DesktopSshEnvironment.test.ts +++ b/apps/desktop/src/ssh/DesktopSshEnvironment.test.ts @@ -30,7 +30,7 @@ describe("sshEnvironment", () => { assert.equal(cause.message, "Failed to present SSH password prompt for devbox."); assert.equal( DesktopSshEnvironment.toSshPasswordPromptError(cause).message, - "T3 Code window is not available for SSH authentication.", + "Phoenix window is not available for SSH authentication.", ); }); diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.ts index 31e84ae995ed..5b3da66ead53 100644 --- a/apps/desktop/src/ssh/DesktopSshEnvironment.ts +++ b/apps/desktop/src/ssh/DesktopSshEnvironment.ts @@ -65,7 +65,6 @@ export class DesktopSshEnvironment extends Context.Service< >()("@t3tools/desktop/ssh/DesktopSshEnvironment") {} export interface DesktopSshEnvironmentLayerOptions { - readonly resolveCliPackageSpec?: () => string; readonly resolveCliRunner?: Effect.Effect; } @@ -96,7 +95,7 @@ export function toSshPasswordPromptError( break; case "DesktopSshPromptWindowUnavailableError": case "DesktopSshPromptPresentationError": - message = "T3 Code window is not available for SSH authentication."; + message = "Phoenix window is not available for SSH authentication."; break; case "DesktopSshPromptTimedOutError": message = `SSH authentication timed out for ${cause.destination}.`; @@ -157,14 +156,5 @@ export const make = Effect.gen(function* () { export const layer = (options: DesktopSshEnvironmentLayerOptions = {}) => Layer.effect(DesktopSshEnvironment, make).pipe( - Layer.provide( - SshTunnel.SshEnvironmentManager.layer({ - ...(options.resolveCliPackageSpec === undefined - ? {} - : { resolveCliPackageSpec: options.resolveCliPackageSpec }), - ...(options.resolveCliRunner === undefined - ? {} - : { resolveCliRunner: options.resolveCliRunner }), - }), - ), + Layer.provide(SshTunnel.SshEnvironmentManager.layer(options)), ); diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts index aa25d8135c7a..92127aabe168 100644 --- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts +++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts @@ -63,7 +63,7 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorCl ) { override get message(): string { const request = this.requestId === null ? "before a request id was assigned" : this.requestId; - return `T3 Code window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`; + return `Phoenix window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`; } } diff --git a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts index d705252ef588..53d9bfc47e5d 100644 --- a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts +++ b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts @@ -25,7 +25,7 @@ function makeElectronAppLayer( ) { return Layer.succeed(ElectronApp.ElectronApp, { metadata: Effect.die("unexpected metadata read"), - name: Effect.succeed("T3 Code"), + name: Effect.succeed("Phoenix"), systemLocale: Effect.succeed("en-US"), whenReady: Effect.void, quit: Effect.void, diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index 6f10d69d7a31..46f16334f1a9 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.test.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.test.ts @@ -30,7 +30,7 @@ const environmentInput = { const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, { metadata: Effect.die("unexpected metadata read"), - name: Effect.succeed("T3 Code"), + name: Effect.succeed("Phoenix"), systemLocale: Effect.succeed("en-US"), whenReady: Effect.void, quit: Effect.void, diff --git a/apps/desktop/src/window/DesktopApplicationMenu.ts b/apps/desktop/src/window/DesktopApplicationMenu.ts index 66244534debf..29b049a5f8f3 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.ts @@ -66,7 +66,7 @@ const checkForUpdatesFromMenu = Effect.gen(function* () { yield* electronDialog.showMessageBox({ type: "info", title: "You're up to date!", - message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`, + message: `Phoenix ${updateState.currentVersion} is currently the newest version available.`, buttons: ["OK"], }); } else if (updateState.status === "error") { diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index abf6f220eca4..6e20cf5adefc 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -18,7 +18,7 @@ vi.mock("electron", async (importOriginal) => ({ ...(await importOriginal()), session: { fromPartition: vi.fn(() => ({ - getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/1.2.3"), + getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 phoenix/1.2.3"), setPermissionRequestHandler: vi.fn(), setUserAgent: vi.fn(), })), @@ -66,7 +66,7 @@ function makeFakeBrowserWindow() { let zoomLevel = 0; const webContents = { copyImageAt: vi.fn(), - getURL: vi.fn(() => "t3code-dev://app/"), + getURL: vi.fn(() => "phoenix-dev://app/"), getZoomLevel: vi.fn(() => zoomLevel), setZoomLevel: vi.fn((level: number) => { zoomLevel = level; @@ -287,8 +287,8 @@ function makeTestLayer(input: { Layer.mock(PreviewManager.PreviewManager)({ getBrowserSession: () => Effect.succeed({} as Electron.Session), setMainWindow: () => Effect.void, - isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"), - getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"), + isBrowserPartition: (partition) => partition.startsWith("persist:phoenix-preview-"), + getBrowserPartition: () => Effect.succeed("persist:phoenix-preview-test"), reapplyZoom: () => Effect.sync(() => { input.previewZoomReapplies?.push(input.window.webContents.getZoomLevel()); @@ -387,8 +387,8 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n Layer.mock(PreviewManager.PreviewManager)({ getBrowserSession: () => Effect.succeed({} as Electron.Session), setMainWindow: () => Effect.void, - isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"), - getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"), + isBrowserPartition: (partition) => partition.startsWith("persist:phoenix-preview-"), + getBrowserPartition: () => Effect.succeed("persist:phoenix-preview-test"), }), ), ), @@ -437,19 +437,19 @@ describe("DesktopWindow", () => { it("recognizes only same-origin renderer navigations", () => { assert.isTrue( DesktopWindow.isSameOriginRendererNavigation({ - applicationUrl: "t3code://app/", - navigationUrl: "t3code://app/settings/connections", + applicationUrl: "phoenix://app/", + navigationUrl: "phoenix://app/settings/connections", }), ); assert.isFalse( DesktopWindow.isSameOriginRendererNavigation({ - applicationUrl: "t3code://app/", + applicationUrl: "phoenix://app/", navigationUrl: "https://accounts.microsoft.com/oauth", }), ); assert.isFalse( DesktopWindow.isSameOriginRendererNavigation({ - applicationUrl: "t3code://app/", + applicationUrl: "phoenix://app/", navigationUrl: "not a url", }), ); @@ -482,7 +482,7 @@ describe("DesktopWindow", () => { assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor); assert.isFalse(createdWindowOptions[0]?.webPreferences?.backgroundThrottling); assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]); - assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]); + assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["phoenix-dev://app/"]); assert.equal(fakeWindow.openDevTools.mock.calls.length, 1); }).pipe(Effect.provide(layer)); }), @@ -1071,17 +1071,17 @@ describe("DesktopWindow", () => { return yield* Effect.die("renderer load listeners were not registered"); } - didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true); + didFailLoad({}, -9, "ERR_UNEXPECTED", "phoenix-dev://app/", true); assert.equal(fakeWindow.loadURL.mock.calls.length, 1); yield* TestClock.adjust(100); assert.deepEqual(fakeWindow.loadURL.mock.calls, [ - ["t3code-dev://app/"], - ["t3code-dev://app/"], + ["phoenix-dev://app/"], + ["phoenix-dev://app/"], ]); assert.equal(fakeWindow.reload.mock.calls.length, 0); - didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true); + didFailLoad({}, -9, "ERR_UNEXPECTED", "phoenix-dev://app/", true); didFinishLoad(); yield* TestClock.adjust(250); assert.equal(fakeWindow.loadURL.mock.calls.length, 2); @@ -1093,23 +1093,23 @@ describe("DesktopWindow", () => { it("retries only transient failures for the development renderer", () => { assert.isTrue( DesktopWindow.isRetryableDevelopmentRendererLoadFailure({ - applicationUrl: "t3code-dev://app/", + applicationUrl: "phoenix-dev://app/", errorCode: -102, isMainFrame: true, - validatedUrl: "t3code-dev://app/", + validatedUrl: "phoenix-dev://app/", }), ); assert.isFalse( DesktopWindow.isRetryableDevelopmentRendererLoadFailure({ - applicationUrl: "t3code-dev://app/", + applicationUrl: "phoenix-dev://app/", errorCode: -3, isMainFrame: true, - validatedUrl: "t3code-dev://app/", + validatedUrl: "phoenix-dev://app/", }), ); assert.isFalse( DesktopWindow.isRetryableDevelopmentRendererLoadFailure({ - applicationUrl: "t3code-dev://app/", + applicationUrl: "phoenix-dev://app/", errorCode: -102, isMainFrame: true, validatedUrl: "https://example.com/", diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index d87c74428a99..97026a562dd8 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -126,7 +126,7 @@ function getIconOption( } function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { - return shouldUseDarkColors ? "#0a0a0a" : "#ffffff"; + return shouldUseDarkColors ? "#101012" : "#fcfcfc"; } type DisplayBounds = Pick; diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts index 9dbe43b9650d..a4af19d54cf9 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts @@ -193,7 +193,7 @@ describe("WSL runtime cache", () => { "b".repeat(64), ); - expect(script).toContain('runtime_parent="$HOME/.t3/wsl-runtime"'); + expect(script).toContain('runtime_parent="$HOME/.phoenix/wsl-runtime"'); expect(script).toContain(' [ -f "$ready_marker" ] &&'); expect(script).toContain(' [ -f "$runtime_root/apps/server/dist/bin.mjs" ] &&'); expect(script).toContain(' [ -f "$runtime_root/node_modules/node-pty/package.json" ] &&'); @@ -361,8 +361,8 @@ describe("WSL runtime cache", () => { }); it("parses only absolute Linux runtime paths", () => { - expect(parseWslRuntimeRoot("runtimeRoot:/home/josh/.t3/wsl-runtime/1.2.3-x64\n")).toBe( - "/home/josh/.t3/wsl-runtime/1.2.3-x64", + expect(parseWslRuntimeRoot("runtimeRoot:/home/josh/.phoenix/wsl-runtime/1.2.3-x64\n")).toBe( + "/home/josh/.phoenix/wsl-runtime/1.2.3-x64", ); expect(parseWslRuntimeRoot("runtimeRoot:relative/path\n")).toBeNull(); expect(parseWslRuntimeRoot("noise\n")).toBeNull(); @@ -417,7 +417,9 @@ describe("WSL runtime cache", () => { // Readiness is a presence check, so a tree whose pty.node is present but // unloadable stays ready forever unless the probe can revoke the marker. - expect(script).toContain('rm -f "$HOME/.t3/wsl-runtime/1.2.3_x64/.t3code-wsl-runtime-ready"'); + expect(script).toContain( + 'rm -f "$HOME/.phoenix/wsl-runtime/1.2.3_x64/.t3code-wsl-runtime-ready"', + ); // Deleting the tree here would pull it out from under any backend still // running from it; the next install moves an unready root aside instead. expect(script).not.toContain("rm -rf"); @@ -472,9 +474,9 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed archivePath, archiveSha, runtimeId, - runtimeParent: `${work}/home/.t3/wsl-runtime`, - runtimeRoot: `${work}/home/.t3/wsl-runtime/${runtimeId}`, - serverEntry: `${work}/home/.t3/wsl-runtime/${runtimeId}/apps/server/dist/bin.mjs`, + runtimeParent: `${work}/home/.phoenix/wsl-runtime`, + runtimeRoot: `${work}/home/.phoenix/wsl-runtime/${runtimeId}`, + serverEntry: `${work}/home/.phoenix/wsl-runtime/${runtimeId}/apps/server/dist/bin.mjs`, installScript, install: (archive?: string, sha?: string) => runShell(installScript(archive, sha)), }; @@ -701,7 +703,7 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed "set -eu", "work=$(mktemp -d)", 'home="$work/home"', - 'runtime_parent="$home/.t3/wsl-runtime"', + 'runtime_parent="$home/.phoenix/wsl-runtime"', 'mkdir -p "$runtime_parent"', 'make_ready() { mkdir -p "$runtime_parent/$1/apps/server/dist"; printf ready > "$runtime_parent/$1/.t3code-wsl-runtime-ready"; }', "make_ready sha256-current", diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.ts index b0c9f5ffe44b..0f9d6660b4b5 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -277,7 +277,7 @@ export const buildWslRuntimeInstallScript = ( const safeRuntimeId = sanitizeWslRuntimeId(runtimeId); return [ "set -eu", - 'runtime_parent="$HOME/.t3/wsl-runtime"', + 'runtime_parent="$HOME/.phoenix/wsl-runtime"', `runtime_root="$runtime_parent/${safeRuntimeId}"`, `ready_marker="$runtime_root/${WSL_RUNTIME_READY_MARKER}"`, // The native payload is the part of the tree the WSL backend actually @@ -413,7 +413,7 @@ export const buildWslRuntimePruneScript = (runtimeId: string): string => { const safeRuntimeId = sanitizeWslRuntimeId(runtimeId); return [ "set -eu", - 'runtime_parent="$HOME/.t3/wsl-runtime"', + 'runtime_parent="$HOME/.phoenix/wsl-runtime"', `current_runtime="$runtime_parent/${safeRuntimeId}"`, '[ -d "$runtime_parent" ] || exit 0', // Serialize the whole retention decision so two backends cannot select @@ -477,7 +477,7 @@ export const buildWslRuntimeInvalidateScript = (runtimeId: string): string => { const safeRuntimeId = sanitizeWslRuntimeId(runtimeId); return [ "set -eu", - `rm -f "$HOME/.t3/wsl-runtime/${safeRuntimeId}/${WSL_RUNTIME_READY_MARKER}"`, + `rm -f "$HOME/.phoenix/wsl-runtime/${safeRuntimeId}/${WSL_RUNTIME_READY_MARKER}"`, ].join("\n"); }; @@ -493,7 +493,7 @@ const NODE_PTY_PREBUILD_MISSING_EXIT_CODE = 4; export const formatNodePtyProbeFailureReason = (exitCode: number): string | null => exitCode === NODE_PTY_PREBUILD_MISSING_EXIT_CODE - ? "WSL support is missing from this T3 Code build: the packaged Linux node-pty binary was not included. Rebuild the Windows artifact with `--wsl-prebuild ` or install a build that includes WSL support." + ? "WSL support is missing from this Phoenix build: the packaged Linux node-pty binary was not included. Rebuild the Windows artifact with `--wsl-prebuild ` or install a build that includes WSL support." : null; const NODE_PTY_PROBE_SCRIPT = ( diff --git a/apps/desktop/src/wsl/DesktopWslServerTree.test.ts b/apps/desktop/src/wsl/DesktopWslServerTree.test.ts index 0c020806166a..14f503ef32e6 100644 --- a/apps/desktop/src/wsl/DesktopWslServerTree.test.ts +++ b/apps/desktop/src/wsl/DesktopWslServerTree.test.ts @@ -37,7 +37,7 @@ const environmentLayer = (input: { Layer.mergeAll( NodeServices.layer, DesktopConfig.layerTest({ - T3CODE_HOME: input.baseDir, + PHOENIX_HOME: input.baseDir, T3CODE_MODE: "desktop", }), ), @@ -243,7 +243,7 @@ describe("DesktopWslServerTree", () => { }); yield* fileSystem.writeFileString(path.join(serverRoot, "apps/server/dist/bin.mjs"), "x"); - // T3CODE_HOME is set to tempDir, so the desktop state dir resolves to + // PHOENIX_HOME is set to tempDir, so the desktop state dir resolves to // /userdata (no .t3 segment). const treeRoot = path.join(tempDir, "userdata", "wsl-server-tree"); yield* fileSystem.makeDirectory(path.join(treeRoot, "1.0.0"), { recursive: true }); diff --git a/apps/marketing/public/apple-touch-icon.png b/apps/marketing/public/apple-touch-icon.png index 9c593ce352ce..2abef5075076 100644 Binary files a/apps/marketing/public/apple-touch-icon.png and b/apps/marketing/public/apple-touch-icon.png differ diff --git a/apps/marketing/public/favicon-16x16.png b/apps/marketing/public/favicon-16x16.png index f85017caf75f..1fbf3e45e45e 100644 Binary files a/apps/marketing/public/favicon-16x16.png and b/apps/marketing/public/favicon-16x16.png differ diff --git a/apps/marketing/public/favicon-32x32.png b/apps/marketing/public/favicon-32x32.png index fae2d285b7a3..911ec595bc85 100644 Binary files a/apps/marketing/public/favicon-32x32.png and b/apps/marketing/public/favicon-32x32.png differ diff --git a/apps/marketing/public/favicon.ico b/apps/marketing/public/favicon.ico index e3ab4ae5e024..888e645d1b73 100644 Binary files a/apps/marketing/public/favicon.ico and b/apps/marketing/public/favicon.ico differ diff --git a/apps/marketing/public/icon.webp b/apps/marketing/public/icon.webp index aa6826dd36d5..c493385e2b0d 100644 Binary files a/apps/marketing/public/icon.webp and b/apps/marketing/public/icon.webp differ diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro index f5e34d0e0485..b8f0621d1897 100644 --- a/apps/marketing/src/layouts/Layout.astro +++ b/apps/marketing/src/layouts/Layout.astro @@ -13,8 +13,8 @@ interface Props { } const { - title = "T3 Code", - description = "T3 Code — The open-source control plane for coding agents.", + title = "Phoenix", + description = "Phoenix — The open-source control plane for coding agents.", pageClass, } = Astro.props; --- @@ -40,8 +40,8 @@ const {