diff --git a/README.md b/README.md index c7ab1a460a22f..886f841e7cad1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ -# Ada - -**Agentic Desktop Application. Engineering is better with Agents.** - -Ada lets you direct the coding agents on your machine (Claude Code, Codex, Cursor, Grok Build, +

+ + + + Ada: Engineering is better with Agents. + +

+ +

+ Follow @jayleaton Buy Me a Coffee: Support +

+ +Ada (Agentic Desktop Application) lets you direct the coding agents on your machine (Claude Code, Codex, Cursor, Grok Build, OpenCode, Google Antigravity, and fx) from one workspace, across a desktop app, a web app, and a mobile app. Your agents run wherever Ada runs: your desktop, a server, or a VPS, and you can connect to it from anywhere. @@ -28,7 +36,7 @@ Download Ada from [GitHub Releases](https://github.com/jayleaton/t3code/releases ### Command line -The desktop app includes the `ada` command (**Settings → About → t3 command**). `ada update` and `ada service install` download Ada's releases. For a Linux machine without a desktop, see [Headless Linux host](./docs/operations/headless-linux.md). +The desktop app includes the `ada` command (**Settings → About → ada command**). `ada update` and `ada service install` download Ada's releases. For a Linux machine without a desktop, see [Headless Linux host](./docs/operations/headless-linux.md). ### Switching from T3 Code diff --git a/apps/desktop/gnome-extension/captureFeedback.js b/apps/desktop/gnome-extension/captureFeedback.js index e0a0635328d14..35ff755990250 100644 --- a/apps/desktop/gnome-extension/captureFeedback.js +++ b/apps/desktop/gnome-extension/captureFeedback.js @@ -54,7 +54,7 @@ export class CaptureFeedback { } } catch (error) { this._clearActors(); - console.warn(`T3 capture effects unavailable: ${error.message}`); + console.warn(`Ada capture effects unavailable: ${error.message}`); } return Boolean(this._actor); } diff --git a/apps/desktop/gnome-extension/extension.js b/apps/desktop/gnome-extension/extension.js index d97ee4c38d3a2..539844328f7e0 100644 --- a/apps/desktop/gnome-extension/extension.js +++ b/apps/desktop/gnome-extension/extension.js @@ -73,7 +73,7 @@ function takeSnapshot(animate) { try { content = window.get_compositor_private().paint_to_content(null); } catch (error) { - console.warn(`T3 capture preview unavailable: ${error.message}`); + console.warn(`Ada capture preview unavailable: ${error.message}`); } } const metadata = JSON.stringify({ diff --git a/apps/desktop/resources/dmg/dmg-background-latest.svg b/apps/desktop/resources/dmg/dmg-background-latest.svg index b1bc14d7b58d7..bd26c174ab265 100644 --- a/apps/desktop/resources/dmg/dmg-background-latest.svg +++ b/apps/desktop/resources/dmg/dmg-background-latest.svg @@ -17,9 +17,7 @@ - - - + Drag Ada into Applications. Once copied, open it from Applications. diff --git a/apps/desktop/resources/dmg/dmg-background-nightly.svg b/apps/desktop/resources/dmg/dmg-background-nightly.svg index af2a2bd6bd6b9..663d3034a8d75 100644 --- a/apps/desktop/resources/dmg/dmg-background-nightly.svg +++ b/apps/desktop/resources/dmg/dmg-background-nightly.svg @@ -23,9 +23,7 @@ - - - + NIGHTLY Drag Ada into Applications. diff --git a/apps/desktop/src/backend/DesktopBackendPool.ts b/apps/desktop/src/backend/DesktopBackendPool.ts index 5af3cf2059c35..91c9d90e4c5ab 100644 --- a/apps/desktop/src/backend/DesktopBackendPool.ts +++ b/apps/desktop/src/backend/DesktopBackendPool.ts @@ -26,7 +26,7 @@ import * as DesktopWindow from "../window/DesktopWindow.ts"; import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts"; import * as ElectronDialog from "../electron/ElectronDialog.ts"; import * as ElectronApp from "../electron/ElectronApp.ts"; -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; const { logWarning: logBackendPoolWarning } = DesktopObservability.makeComponentLogger("desktop-backend-pool"); @@ -179,8 +179,8 @@ export const layer = Layer.effect( onStateDirOwned: () => Effect.flatMap(HostProcess.Platform, (platform) => electronDialog.showErrorBox( - "This T3 home is unavailable", - `Another ${APP_NAME} server, such as the background service, is using this data directory, or an older background service is set up for it. Stop that server or try \`t3 update\`. If the older launcher cannot update, run \`t3 service uninstall\` and optionally reinstall with \`t3 service install\` from a current CLI. If \`t3\` is missing or outdated, install it with \`${cliInstallCommand(platform === "win32" ? "win32" : "posix")}\`, and pass \`--base-dir\` with the service's T3 home to each command. Then reopen ${APP_NAME}. You can also set a separate T3CODE_HOME and pair with the running server.`, + `This ${APP_NAME} home is unavailable`, + `Another ${APP_NAME} server, such as the background service, is using this data directory, or an older background service is set up for it. Stop that server or try \`${CLI_COMMAND} update\`. If the older launcher cannot update, run \`${CLI_COMMAND} service uninstall\` and optionally reinstall with \`${CLI_COMMAND} service install\` from a current CLI. If \`${CLI_COMMAND}\` is missing or outdated, install it with \`${cliInstallCommand(platform === "win32" ? "win32" : "posix")}\`, and pass \`--base-dir\` with the service's ${APP_NAME} home to each command. Then reopen ${APP_NAME}. You can also set a separate T3CODE_HOME and pair with the running server.`, ), ).pipe(Effect.andThen(electronApp.quit)), onDatabaseRefused: () => diff --git a/apps/desktop/src/wsl/DesktopWslBackend.ts b/apps/desktop/src/wsl/DesktopWslBackend.ts index 5066f15a80810..7974e7282a3fc 100644 --- a/apps/desktop/src/wsl/DesktopWslBackend.ts +++ b/apps/desktop/src/wsl/DesktopWslBackend.ts @@ -15,7 +15,7 @@ import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts"; import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopWslEnvironment from "./DesktopWslEnvironment.ts"; -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; export const WSL_INSTANCE_ID_PREFIX = "wsl:"; const WSL_DEFAULT_DISTRO_ID = `${WSL_INSTANCE_ID_PREFIX}default`; @@ -130,7 +130,7 @@ export const layer = Layer.effect( Ref.set(preflightErrorRef, Option.some(failure.reason)).pipe(Effect.as(false)), onStateDirOwned: () => refuse( - `Another ${APP_NAME} server is using this distro's T3 home, or an older background service is set up for it. Stop that server or pair with it, or try \`t3 update\` in the distro. If the older launcher cannot update, run \`t3 service uninstall\` and optionally reinstall with \`t3 service install\` from a current CLI in the distro. If \`t3\` is missing or outdated, install it in the distro with \`${cliInstallCommand("posix")}\`, and pass \`--base-dir\` with the service's T3 home to each command.`, + `Another ${APP_NAME} server is using this distro's ${APP_NAME} home, or an older background service is set up for it. Stop that server or pair with it, or try \`${CLI_COMMAND} update\` in the distro. If the older launcher cannot update, run \`${CLI_COMMAND} service uninstall\` and optionally reinstall with \`${CLI_COMMAND} service install\` from a current CLI in the distro. If \`${CLI_COMMAND}\` is missing or outdated, install it in the distro with \`${cliInstallCommand("posix")}\`, and pass \`--base-dir\` with the service's ${APP_NAME} home to each command.`, ), onDatabaseRefused: () => refuse( diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.ts index edff9aba1e56e..59951e7be02d8 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -326,7 +326,7 @@ export const buildWslRuntimeInstallScript = ( "trap cleanup_runtime_install EXIT", `tar -xzf ${shellQuote(linuxArchivePath)} -C "$runtime_tmp" --strip-components=1`, 'if ! runtime_entry_runs "$runtime_tmp"; then', - " printf 'WSL runtime archive does not contain a working t3 executable\\n' >&2", + ` printf 'WSL runtime archive does not contain a working ${APP_NAME} executable\\n' >&2`, " exit 1", "fi", 'installed_entry_digest=$(runtime_server_entry_digest "$runtime_tmp")', diff --git a/apps/marketing/public/apple-touch-icon.png b/apps/marketing/public/apple-touch-icon.png index 9c593ce352ce0..761c5949d68e5 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 f85017caf75f8..427bbe125ea8a 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 fae2d285b7a35..6eff5df10a9b5 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 e3ab4ae5e0248..9e249762a4ff2 100644 Binary files a/apps/marketing/public/favicon.ico and b/apps/marketing/public/favicon.ico differ diff --git a/apps/marketing/src/assets/icon-nightly.webp b/apps/marketing/src/assets/icon-nightly.webp index 8a00067e43b81..cf1e78ac9460a 100644 Binary files a/apps/marketing/src/assets/icon-nightly.webp and b/apps/marketing/src/assets/icon-nightly.webp differ diff --git a/apps/marketing/src/assets/icon.webp b/apps/marketing/src/assets/icon.webp index aa6826dd36d57..7ca76e92b2394 100644 Binary files a/apps/marketing/src/assets/icon.webp and b/apps/marketing/src/assets/icon.webp differ diff --git a/apps/marketing/src/assets/social-card.webp b/apps/marketing/src/assets/social-card.webp index cb4f9a0de722f..a2f349ecf9cb3 100644 Binary files a/apps/marketing/src/assets/social-card.webp and b/apps/marketing/src/assets/social-card.webp differ diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 3c7dc567980ce..1879ae951e93d 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -34,11 +34,10 @@ if ( ); } -// This fork ships as T3 Agents: every variant uses the Agents mark from -// assets/agents/source.png (the desktop icon), told apart by name only. iOS +// Every variant uses Pip (scripts/export-android-icons.ts), told apart by name only. iOS // gets a full-bleed square because it applies its own mask; Android gets the // mark alone over a navy adaptive background so the launcher mask never crops it. -const AGENTS_NAVY = "#0A1442"; +const AGENTS_NAVY = "#111925"; const AGENTS_ASSETS = { appIcon: "./assets/agents/ios-icon-1024.png", iosIcon: "./assets/agents/ios-icon-1024.png", diff --git a/apps/mobile/assets/agents/android-icon-foreground.png b/apps/mobile/assets/agents/android-icon-foreground.png index 6496850d6dff3..fdb8f0a2cea4c 100644 Binary files a/apps/mobile/assets/agents/android-icon-foreground.png and b/apps/mobile/assets/agents/android-icon-foreground.png differ diff --git a/apps/mobile/assets/agents/android-icon-monochrome.png b/apps/mobile/assets/agents/android-icon-monochrome.png index 1f0bbbf600ce2..049405c68b898 100644 Binary files a/apps/mobile/assets/agents/android-icon-monochrome.png and b/apps/mobile/assets/agents/android-icon-monochrome.png differ diff --git a/apps/mobile/assets/agents/android-splash-icon.png b/apps/mobile/assets/agents/android-splash-icon.png index f14b288bef7cf..3fa8bcb6d5685 100644 Binary files a/apps/mobile/assets/agents/android-splash-icon.png and b/apps/mobile/assets/agents/android-splash-icon.png differ diff --git a/apps/mobile/assets/agents/ios-icon-1024.png b/apps/mobile/assets/agents/ios-icon-1024.png index c845659ad4385..5d83f74180f27 100644 Binary files a/apps/mobile/assets/agents/ios-icon-1024.png and b/apps/mobile/assets/agents/ios-icon-1024.png differ diff --git a/apps/mobile/assets/agents/splash-mark.png b/apps/mobile/assets/agents/splash-mark.png index 7007881e30509..c988262d8405c 100644 Binary files a/apps/mobile/assets/agents/splash-mark.png and b/apps/mobile/assets/agents/splash-mark.png differ diff --git a/apps/mobile/assets/android-icon-background-dev.png b/apps/mobile/assets/android-icon-background-dev.png deleted file mode 100644 index db302240ac7d7..0000000000000 Binary files a/apps/mobile/assets/android-icon-background-dev.png and /dev/null differ diff --git a/apps/mobile/assets/android-icon-background-nightly.png b/apps/mobile/assets/android-icon-background-nightly.png deleted file mode 100644 index 9fa83d420a427..0000000000000 Binary files a/apps/mobile/assets/android-icon-background-nightly.png and /dev/null differ diff --git a/apps/mobile/assets/android-icon-foreground.png b/apps/mobile/assets/android-icon-foreground.png deleted file mode 100644 index 52271dc97c6c8..0000000000000 Binary files a/apps/mobile/assets/android-icon-foreground.png and /dev/null differ diff --git a/apps/mobile/assets/android-icon-mark.png b/apps/mobile/assets/android-icon-mark.png deleted file mode 100644 index 3300f22fd5a68..0000000000000 Binary files a/apps/mobile/assets/android-icon-mark.png and /dev/null differ diff --git a/apps/mobile/assets/android-notification-icon.png b/apps/mobile/assets/android-notification-icon.png deleted file mode 100644 index 74de6633cbb56..0000000000000 Binary files a/apps/mobile/assets/android-notification-icon.png and /dev/null differ diff --git a/apps/mobile/assets/android-splash-icon-dev.png b/apps/mobile/assets/android-splash-icon-dev.png deleted file mode 100644 index ba280062720fc..0000000000000 Binary files a/apps/mobile/assets/android-splash-icon-dev.png and /dev/null differ diff --git a/apps/mobile/assets/android-splash-icon-nightly.png b/apps/mobile/assets/android-splash-icon-nightly.png deleted file mode 100644 index 2f84f9fcfbf2a..0000000000000 Binary files a/apps/mobile/assets/android-splash-icon-nightly.png and /dev/null differ diff --git a/apps/mobile/assets/android-splash-icon-prod.png b/apps/mobile/assets/android-splash-icon-prod.png deleted file mode 100644 index 971b5c95f0bd6..0000000000000 Binary files a/apps/mobile/assets/android-splash-icon-prod.png and /dev/null differ diff --git a/apps/mobile/assets/widget/PipMark.svg b/apps/mobile/assets/widget/PipMark.svg new file mode 100644 index 0000000000000..8c0b9b2874446 --- /dev/null +++ b/apps/mobile/assets/widget/PipMark.svg @@ -0,0 +1 @@ + diff --git a/apps/mobile/assets/widget/T3Mark.svg b/apps/mobile/assets/widget/T3Mark.svg deleted file mode 100644 index 48d1b09446f69..0000000000000 --- a/apps/mobile/assets/widget/T3Mark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt index 2d1fd4a248558..16ce28ec559df 100644 --- a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt @@ -127,7 +127,7 @@ private fun saveImageToPictures(context: Context, source: Uri, mimeType: String, val values = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, name) put(MediaStore.Images.Media.MIME_TYPE, mimeType) - put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/T3 Code") + put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/Ada") put(MediaStore.Images.Media.IS_PENDING, 1) } val target = resolver.insert( diff --git a/apps/mobile/plugins/withWidgetLogoAsset.cjs b/apps/mobile/plugins/withWidgetLogoAsset.cjs index d20d199119b05..4da92866a4b1f 100644 --- a/apps/mobile/plugins/withWidgetLogoAsset.cjs +++ b/apps/mobile/plugins/withWidgetLogoAsset.cjs @@ -9,8 +9,8 @@ const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs"); const TARGET_NAME = "ExpoWidgetsTarget"; const CATALOG_NAME = "Assets.xcassets"; -const IMAGE_SET = "T3Mark.imageset"; -const SVG_NAME = "T3Mark.svg"; +const IMAGE_SET = "PipMark.imageset"; +const SVG_NAME = "PipMark.svg"; const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n"; const IMAGE_SET_CONTENTS = diff --git a/apps/mobile/src/components/PipMark.tsx b/apps/mobile/src/components/PipMark.tsx new file mode 100644 index 0000000000000..0ecf16c8e727d --- /dev/null +++ b/apps/mobile/src/components/PipMark.tsx @@ -0,0 +1,29 @@ +import type { ColorValue } from "react-native"; +import Svg, { Path } from "react-native-svg"; +import { withUniwind } from "uniwind"; + +const ThemedPath = withUniwind(Path); + +/** Pip, the app's mark, in a single colour. The path is assets/agents/pip.svg. */ +export function PipMark(props: { + readonly height: number; + readonly color?: ColorValue; + readonly colorClassName?: string; +}) { + return ( + + + + ); +} diff --git a/apps/mobile/src/components/T3Wordmark.tsx b/apps/mobile/src/components/T3Wordmark.tsx deleted file mode 100644 index d7cac80d8f58f..0000000000000 --- a/apps/mobile/src/components/T3Wordmark.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { ColorValue } from "react-native"; -import Svg, { Path } from "react-native-svg"; -import { withUniwind } from "uniwind"; - -const ThemedPath = withUniwind(Path); - -export function T3Wordmark(props: { - readonly height: number; - readonly color?: ColorValue; - readonly colorClassName?: string; -}) { - const aspectRatio = 94.3941 / 56.96; - return ( - - - - ); -} diff --git a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx index 69c7985470f81..a358a70d78983 100644 --- a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx +++ b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx @@ -343,7 +343,7 @@ function PairingScanCard(props: { )} - Scan the code from t3 pair or desktop Connections settings + Scan the code from ada pair or desktop Connections settings diff --git a/apps/mobile/src/features/connection/pairing.ts b/apps/mobile/src/features/connection/pairing.ts index 674315765078a..78eae8a051030 100644 --- a/apps/mobile/src/features/connection/pairing.ts +++ b/apps/mobile/src/features/connection/pairing.ts @@ -105,7 +105,7 @@ export function validateMobilePairingUrl(pairingUrl: string): void { hostname === "::" ) { throw new Error( - "Localhost points to this phone, not your Mac. Generate a new pairing link with t3 pair --tailscale, or use your Mac's LAN IP and a fresh pairing token.", + "Localhost points to this phone, not your Mac. Generate a new pairing link with ada pair --tailscale, or use your Mac's LAN IP and a fresh pairing token.", ); } } diff --git a/apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx index 096513c0e0a3c..2996eb6f6f8e3 100644 --- a/apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsAboutRouteScreen.tsx @@ -127,7 +127,7 @@ function AppSettingsSection() { label="Open source licenses" target="SettingsOpenSourceLicenses" /> - + {updateCheckAvailable ? ( ; + return ; } diff --git a/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx index 1a2951b2617fe..95e7381b9e141 100644 --- a/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx @@ -13,8 +13,7 @@ export function SettingsNotificationsRouteScreen() { Push notifications and Live Activities are not available in this app. - They were delivered through T3 Connect, which this build does not include. Open the app to - see what your agents are doing. + Open the app to see what your agents are doing. diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 9493420b027c7..ddaf9c62d231c 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -52,7 +52,7 @@ export function SettingsLabsRouteScreen() { icon="bolt.circle" capability="fastWorktreeCheckout" setting="fastWorktreeCheckout" - description="On a Mac, clone new worktrees from a checkout cached in the T3 home folder instead of writing every file. Uses Git’s normal checkout when it can’t. Turning it off deletes the cache." + description="On a Mac, clone new worktrees from a checkout cached in the Ada home folder instead of writing every file. Uses Git’s normal checkout when it can’t. Turning it off deletes the cache." /> diff --git a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx index 9a6db23f8e660..045cee4a8e56a 100644 --- a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx +++ b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx @@ -45,7 +45,7 @@ function render( root.render( - Use t3 or t3/ for t3/add-search. Leave empty for no prefix. + Use ada or ada/ for ada/add-search. Leave empty for no prefix. (null); const [externalUrl, setExternalUrl] = useState(documentUrl); const navigation = useNavigation(); - const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL; + const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : documentUrl; const headerOptions = ( navigation.goBack()} actions={[ { - accessibilityLabel: "Open legal documents in external browser", + accessibilityLabel: "Open in external browser", icon: "safari", onPress: () => void Linking.openURL(safeExternalUrl).catch(() => undefined), }, diff --git a/apps/mobile/src/features/settings/environment-maintenance.test.ts b/apps/mobile/src/features/settings/environment-maintenance.test.ts index c9aea054a10fc..e4b1d20381225 100644 --- a/apps/mobile/src/features/settings/environment-maintenance.test.ts +++ b/apps/mobile/src/features/settings/environment-maintenance.test.ts @@ -111,6 +111,27 @@ describe("environment release checks", () => { ); }); + it("finds stable releases from the release list, since /releases/latest never resolves on a prerelease-only repo", async () => { + const nightlies = ["v1.0.1-nightly.20261113.2", "v1.0.1-nightly.20261113.1"]; + const releases = vi.fn(async (url: string) => { + if (url === "https://github.com/jayleaton/t3code/releases.atom") return feed(...nightlies); + if (url === "https://api.github.com/repos/jayleaton/t3code/releases?per_page=100&page=1") { + return Response.json(nightlies.map((tag_name) => ({ tag_name }))); + } + throw new Error(`Unexpected release request: ${url}`); + }); + vi.stubGlobal("fetch", releases); + await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow("No stable release"); + + vi.stubGlobal( + "fetch", + vi.fn(async (url: string) => + url.endsWith(".atom") ? feed(nightlies[0]!, "v1.0.1") : releases(url), + ), + ); + expect(await findEnvironmentUpdate("1.0.0", signal)).toBe("1.0.1"); + }); + it("reports failed checks instead of claiming the server is current", async () => { for (const status of [403, 429]) { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status }))); @@ -125,7 +146,14 @@ describe("environment release checks", () => { await expect(findEnvironmentUpdate("1.0.0-nightly.20260923.1", signal)).rejects.toThrow( "No nightly release", ); - vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({ error: "invalid" }))); - await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow(); + vi.stubGlobal( + "fetch", + vi.fn(async (url: string) => + url.endsWith(".atom") ? feed() : Response.json({ message: "API rate limit exceeded" }), + ), + ); + await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow( + "Could not read the release list. Try again.", + ); }); }); diff --git a/apps/mobile/src/features/settings/environment-maintenance.ts b/apps/mobile/src/features/settings/environment-maintenance.ts index ab60946281e07..7e43e8e2e22a5 100644 --- a/apps/mobile/src/features/settings/environment-maintenance.ts +++ b/apps/mobile/src/features/settings/environment-maintenance.ts @@ -12,6 +12,7 @@ import { newestCliReleaseVersion, } from "@t3tools/shared/cliRelease"; import { compareSemverVersions } from "@t3tools/shared/semver"; +import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; export function canMaintainEnvironment(session: AuthSessionState | null, connected: boolean) { @@ -50,8 +51,7 @@ const Release = Schema.Struct({ tag_name: Schema.String, draft: Schema.optionalKey(Schema.Boolean), }); -const decodeRelease = Schema.decodeUnknownSync(Release); -const decodeReleases = Schema.decodeUnknownSync(Schema.Array(Release)); +const decodeReleases = Schema.decodeUnknownOption(Schema.Array(Release)); async function fetchReleases(url: string, accept: string, signal: AbortSignal) { const response = await fetch(url, { signal, headers: { Accept: accept } }); @@ -61,31 +61,26 @@ async function fetchReleases(url: string, accept: string, signal: AbortSignal) { export async function findEnvironmentUpdate(currentVersion: string, signal: AbortSignal) { const channel = cliReleaseChannelOf(currentVersion); - // Public release pages avoid the anonymous API quota shared by devices on the same network. - const response = await fetchReleases( - channel === "stable" ? `${CLI_RELEASES_PAGE_URL}/latest` : `${CLI_RELEASES_PAGE_URL}.atom`, - channel === "stable" ? "application/json" : "application/atom+xml", - signal, + // The public feed avoids the anonymous API quota shared by devices on the same network. + // Not /releases/latest: it redirects to the release list when every release is a prerelease. + const feed = await fetchReleases(`${CLI_RELEASES_PAGE_URL}.atom`, "application/atom+xml", signal); + let version = newestCliReleaseVersion( + Array.from( + (await feed.text()).matchAll(/tag:github\.com,2008:Repository\/\d+\/([^<]+)<\/id>/g), + ([, tag_name = ""]) => ({ tag_name }), + ), + channel, ); - const releases = - channel === "stable" - ? [decodeRelease(await response.json())] - : Array.from( - (await response.text()).matchAll( - /tag:github\.com,2008:Repository\/\d+\/([^<]+)<\/id>/g, - ), - ([, tag_name = ""]) => ({ tag_name }), - ); - let version = newestCliReleaseVersion(releases, channel); - // The feed only includes ten releases, so later nightlies can displace a published preview. + // The feed only includes ten releases, so later nightlies can displace an older release. for (let page = 1; version === undefined; page++) { const response = await fetchReleases( cliReleaseIndexPageUrl(page), "application/vnd.github+json", signal, ); - const releases = decodeReleases(await response.json()); + const releases = Option.getOrUndefined(decodeReleases(await response.json())); + if (releases === undefined) throw new Error("Could not read the release list. Try again."); version = newestCliReleaseVersion(releases, channel); if (releases.length < 100) break; } diff --git a/apps/mobile/src/features/settings/lib/legal-document-url.test.ts b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts index 001c1fbc299cd..f958289c0285b 100644 --- a/apps/mobile/src/features/settings/lib/legal-document-url.test.ts +++ b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vite-plus/test"; +import { LICENSE_URL } from "@t3tools/shared/cliRelease"; + import { isLegalDocumentUrl } from "./legal-document-url"; describe("isLegalDocumentUrl", () => { @@ -9,4 +11,8 @@ describe("isLegalDocumentUrl", () => { expect(isLegalDocumentUrl(url)).toBe(false); }, ); + + it("allows the fork's license", () => { + expect(isLegalDocumentUrl(`${LICENSE_URL}/`)).toBe(true); + }); }); diff --git a/apps/mobile/src/features/settings/lib/legal-document-url.ts b/apps/mobile/src/features/settings/lib/legal-document-url.ts index e0c90c5ec64ba..814e41a757942 100644 --- a/apps/mobile/src/features/settings/lib/legal-document-url.ts +++ b/apps/mobile/src/features/settings/lib/legal-document-url.ts @@ -1,38 +1,4 @@ -const DEFAULT_MARKETING_SITE_URL = "https://t3.codes"; - -function resolveMarketingSiteUrl(override: string | undefined): URL { - try { - const url = new URL(override?.trim() || DEFAULT_MARKETING_SITE_URL); - if (url.protocol !== "https:" && url.protocol !== "http:") { - return new URL(DEFAULT_MARKETING_SITE_URL); - } - - url.search = ""; - url.hash = ""; - url.pathname = `${url.pathname.replace(/\/+$/, "")}/`; - return url; - } catch { - return new URL(DEFAULT_MARKETING_SITE_URL); - } -} - -const MARKETING_SITE_URL = resolveMarketingSiteUrl(process.env.EXPO_PUBLIC_MARKETING_SITE_URL); - -function marketingSiteDocumentUrl(path: string): string { - return new URL(path, MARKETING_SITE_URL).toString(); -} - -const PRIVACY_POLICY_URL = marketingSiteDocumentUrl("privacy-policy"); -const SECURITY_POLICY_URL = marketingSiteDocumentUrl("security-policy"); -const TERMS_OF_SERVICE_URL = marketingSiteDocumentUrl("terms-of-service"); -export const LEGAL_URL = marketingSiteDocumentUrl("legal"); - -const ALLOWED_LEGAL_DOCUMENT_URLS = [ - LEGAL_URL, - PRIVACY_POLICY_URL, - TERMS_OF_SERVICE_URL, - SECURITY_POLICY_URL, -] as const; +import { LICENSE_URL } from "@t3tools/shared/cliRelease"; function webDocumentIdentity(value: string): string | null { try { @@ -46,13 +12,9 @@ function webDocumentIdentity(value: string): string | null { } } -const ALLOWED_LEGAL_DOCUMENT_IDENTITIES = new Set( - ALLOWED_LEGAL_DOCUMENT_URLS.map(webDocumentIdentity).filter( - (value): value is string => value !== null, - ), -); +const LICENSE_IDENTITY = webDocumentIdentity(LICENSE_URL); export function isLegalDocumentUrl(value: string): boolean { const identity = webDocumentIdentity(value); - return identity !== null && ALLOWED_LEGAL_DOCUMENT_IDENTITIES.has(identity); + return identity !== null && identity === LICENSE_IDENTITY; } diff --git a/apps/mobile/src/features/threads/thread-work-log.tsx b/apps/mobile/src/features/threads/thread-work-log.tsx index fc72af2f06b0f..8b485f0dd6917 100644 --- a/apps/mobile/src/features/threads/thread-work-log.tsx +++ b/apps/mobile/src/features/threads/thread-work-log.tsx @@ -63,7 +63,7 @@ import { } from "@t3tools/client-runtime/work-log/command-label"; import { AppText as Text } from "../../components/ui/AppText"; -import { T3Wordmark } from "../../components/T3Wordmark"; +import { PipMark } from "../../components/PipMark"; import { BrowserAppIcon } from "./BrowserAppIcon"; import { useAppActive, useReduceMotionEnabled } from "../../lib/appState"; import { cn } from "../../lib/cn"; @@ -133,7 +133,7 @@ function WorkLogIcon(props: { const colorClassName = props.highlighted ? "accent-foreground" : props.colorClassName; if (props.icon === "t3-code") { return ( - + ); } if (props.icon === "computer") { diff --git a/apps/mobile/src/features/triage/useStartTriage.ts b/apps/mobile/src/features/triage/useStartTriage.ts index dc178ec851195..acd4394c0c910 100644 --- a/apps/mobile/src/features/triage/useStartTriage.ts +++ b/apps/mobile/src/features/triage/useStartTriage.ts @@ -20,10 +20,10 @@ import { } from "../../state/use-composer-drafts"; import { useSelectWorkspaceDetail } from "../layout/workspace/useSelectWorkspaceDetail"; -const CLI_COMMAND = "t3 triage"; +const CLI_COMMAND = "ada triage"; /** - * In-app `t3 triage`, as on web: the server writes the session files and + * In-app `ada triage`, as on web: the server writes the session files and * clones the matching source, then a No project draft opens with the prompt. */ export function useStartTriage() { diff --git a/apps/mobile/src/lib/saveImageToLibrary.ts b/apps/mobile/src/lib/saveImageToLibrary.ts index 4b9fcd4d2862e..dae069f733bc3 100644 --- a/apps/mobile/src/lib/saveImageToLibrary.ts +++ b/apps/mobile/src/lib/saveImageToLibrary.ts @@ -15,7 +15,7 @@ export function canSaveImageToLibrary(mimeType: string): boolean { ); } -/** Adds a local image file to Photos on iOS or Pictures/T3 Code on Android. */ +/** Adds a local image file to Photos on iOS or Pictures/Ada on Android. */ export function saveImageToLibrary(uri: string, mimeType: string, name: string): Promise { return requireNativeModule<{ saveImageToLibrary(uri: string, mimeType: string, name: string): Promise; diff --git a/apps/server/src/cli/browser.ts b/apps/server/src/cli/browser.ts index ea3dbd347e592..ddc4c1d5a7759 100644 --- a/apps/server/src/cli/browser.ts +++ b/apps/server/src/cli/browser.ts @@ -1,3 +1,4 @@ +import { APP_NAME } from "@t3tools/shared/branding"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; @@ -105,12 +106,14 @@ const FONT_PACKAGES = ["fontconfig", "fonts-liberation"]; const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe( Command.withDescription( - "Set up this Linux host for T3's browser: allow Chrome's sandbox and install its libraries and fonts.", + `Set up this Linux host for ${APP_NAME}'s browser: allow Chrome's sandbox and install its libraries and fonts.`, ), Command.withHandler(({ baseDir }) => Effect.gen(function* () { if ((yield* HostProcess.Platform) !== "linux") { - return yield* Console.log("Nothing to set up: T3's browser runs as is on this system."); + return yield* Console.log( + `Nothing to set up: ${APP_NAME}'s browser runs as is on this system.`, + ); } const fs = yield* FileSystem.FileSystem; const isRoot = (yield* HostProcess.UserId) === 0; @@ -139,23 +142,25 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe return yield* Console.log( rootWarning ?? (Option.isSome(browser) - ? "This host is ready for T3's browser." - : "Chrome's sandbox is allowed here. T3's browser installs on first use; if it then reports missing libraries, run this again."), + ? `This host is ready for ${APP_NAME}'s browser.` + : `Chrome's sandbox is allowed here. ${APP_NAME}'s browser installs on first use; if it then reports missing libraries, run this again.`), ); } if (!isRoot) { if (needsProfile) { yield* Console.log( - `This host blocks the sandbox T3's browser runs in. Setup installs an AppArmor profile at ${PreviewBrowserHost.APPARMOR_PROFILE_PATH} that allows it.`, + `This host blocks the sandbox ${APP_NAME}'s browser runs in. Setup installs an AppArmor profile at ${PreviewBrowserHost.APPARMOR_PROFILE_PATH} that allows it.`, ); } if (missing.length > 0) { - yield* Console.log(`T3's browser is missing ${missing.join(", ")}; setup installs them.`); + yield* Console.log( + `${APP_NAME}'s browser is missing ${missing.join(", ")}; setup installs them.`, + ); } if (needsFonts) { yield* Console.log( - "T3's browser needs fontconfig and system fonts; setup installs them.", + `${APP_NAME}'s browser needs fontconfig and system fonts; setup installs them.`, ); } return yield* Console.log(`\nThis needs root. Run:\n\n ${setupCommand}\n`); @@ -180,18 +185,18 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe "-r", PreviewBrowserHost.APPARMOR_PROFILE_PATH, ]); - yield* Console.log("Allowed Chrome's sandbox for T3's browser."); + yield* Console.log(`Allowed Chrome's sandbox for ${APP_NAME}'s browser.`); } if ((missing.length > 0 || needsFonts) && !hasApt) { if (missing.length > 0) { yield* Console.log( - `T3's browser is missing ${missing.join(", ")}. Install them with your package manager, then run this again.`, + `${APP_NAME}'s browser is missing ${missing.join(", ")}. Install them with your package manager, then run this again.`, ); } if (needsFonts) { yield* Console.log( - "T3's browser needs fontconfig and system fonts. Install them with your package manager, then run this again.", + `${APP_NAME}'s browser needs fontconfig and system fonts. Install them with your package manager, then run this again.`, ); } return; @@ -226,16 +231,16 @@ const browserSetupCommand = Command.make("setup", { baseDir: baseDirFlag }).pipe ); } yield* Console.log( - "Installed the browser's fonts. Restart the T3 server if browser tabs were already open.", + `Installed the browser's fonts. Restart the ${APP_NAME} server if browser tabs were already open.`, ); } - yield* Console.log(rootWarning ?? "This host is ready for T3's browser."); + yield* Console.log(rootWarning ?? `This host is ready for ${APP_NAME}'s browser.`); }), ), ); export const browserCommand = Command.make("browser").pipe( - Command.withDescription("Manage T3's headless browser on this host."), + Command.withDescription(`Manage ${APP_NAME}'s headless browser on this host.`), Command.withSubcommands([browserSetupCommand]), ); diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 1527084062192..41fd233e1a12f 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -227,7 +227,7 @@ describe("t3 pair", () => { readonly scopes: ReadonlyArray; }>; assert.equal(credentials.length, 1); - assert.equal(credentials[0]?.label, "t3 pair"); + assert.equal(credentials[0]?.label, "ada pair"); assert.deepEqual(credentials[0]?.scopes, AuthStandardClientScopes); }), ).pipe( diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 2dec89ecf8deb..f1b33a5df918d 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -459,7 +459,7 @@ const mintPairingLink = Effect.fn("pair.mintPairingLink")(function* (input: { return yield* environmentAuth.createPairingLink({ scopes: input.scopes, subject: "one-time-token", - label: Option.getOrElse(input.label, () => "t3 pair"), + label: Option.getOrElse(input.label, () => `${CLI_COMMAND} pair`), ...(Option.isSome(input.ttl) ? { ttl: input.ttl.value } : {}), }); }).pipe( diff --git a/apps/server/src/cli/policy.ts b/apps/server/src/cli/policy.ts index 3916db93a9fff..85dcccedcda03 100644 --- a/apps/server/src/cli/policy.ts +++ b/apps/server/src/cli/policy.ts @@ -48,7 +48,7 @@ export const describeRunningServer = Effect.fn("cli.policy.describeRunningServer ) { const state = yield* readPersistedServerRuntimeState(statePath); if (Option.isNone(state) || !isProcessAlive(state.value.pid)) { - return `No ${APP_NAME} server is running from this T3 home. The installed policy applies when one starts.`; + return `No ${APP_NAME} server is running from this ${APP_NAME} home. The installed policy applies when one starts.`; } const { pid, startedAt, managedPolicy: loaded } = state.value; const server = `The running server (pid ${pid}, started ${startedAt})`; diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 14d3aeeedb50a..9016bb9282964 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -1,3 +1,4 @@ +import { CLI_COMMAND } from "@t3tools/shared/branding"; import { AuthStandardClientScopes, CommandId, @@ -230,7 +231,7 @@ const withProjectCliSessionToken = ( Effect.acquireUseRelease( environmentAuth.issueSession({ scopes: AuthStandardClientScopes, - label: "t3 project cli", + label: `${CLI_COMMAND} project cli`, }), (issued) => run(issued.token), (issued) => environmentAuth.revokeSession(issued.sessionId).pipe(Effect.ignore({ log: true })), diff --git a/apps/server/src/cli/service.ts b/apps/server/src/cli/service.ts index c48c5bdb53da7..651ae08ee1d53 100644 --- a/apps/server/src/cli/service.ts +++ b/apps/server/src/cli/service.ts @@ -143,7 +143,7 @@ function formatProtection( if (protection === undefined) return []; return [ protection.unprotected === undefined - ? ` Memory protection: on${protection.setByProtect ? " (t3 service protect)" : ""}` + ? ` Memory protection: on${protection.setByProtect ? ` (${CLI_COMMAND} service protect)` : ""}` : ` Memory protection: off, ${protection.unprotected} grants none. Run \`${CLI_COMMAND} service protect\`.`, ...(protection.oomScoreAdj === undefined ? [] : [` OOM score: ${protection.oomScoreAdj}`]), ]; @@ -163,7 +163,7 @@ function formatServiceStatus( if (status.otherBaseDir !== undefined) { return [ `${APP_NAME} service`, - " Status: installed for another T3 home", + ` Status: installed for another ${APP_NAME} home`, ` Base dir: ${status.otherBaseDir}`, ` Next: Pass \`${BootService.formatBootServiceBaseDirOption(status.otherBaseDir)}\` to manage it.`, ].join("\n"); diff --git a/apps/server/src/cli/thread.ts b/apps/server/src/cli/thread.ts index a22d715f7a679..ad7532152c9da 100644 --- a/apps/server/src/cli/thread.ts +++ b/apps/server/src/cli/thread.ts @@ -1,4 +1,4 @@ -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; import { OrchestratorMcpFailure, OrchestratorMcpThreadListResult, @@ -219,7 +219,7 @@ const withThreadTools = Effect.fn("cli.thread.withThreadTools")(function* Effect.gen(function* () { @@ -206,7 +206,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { const plan = yield* planUninstall({ baseDir: input.baseDir }); if (!plan.service && plan.launchers.length === 0 && plan.runtimeDir === undefined) { - yield* Console.log(`Nothing to remove: t3 is not installed for ${input.baseDir}.`); + yield* Console.log(`Nothing to remove: ${APP_NAME} is not installed for ${input.baseDir}.`); if (!(yield* HostProcess.IsExecutable)) { yield* Console.log( " This t3 runs from a Node script, so it was built from source or is upstream's npm package. Delete the checkout, or run `npm uninstall -g t3` for the npm package.", @@ -233,7 +233,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { }); } const confirmed = yield* Prompt.run( - Prompt.Confirm({ message: "Remove t3 from this machine?", initial: false }), + Prompt.Confirm({ message: `Remove ${APP_NAME} from this machine?`, initial: false }), ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) })); if (!confirmed) { yield* Console.log("Left as is."); @@ -272,7 +272,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: { reason: `Could not schedule removal of ${runtimeDir}. Delete it yourself once this window is closed.`, }), }); - yield* Console.log(`${runtimeDir} will be removed once t3 exits.`); + yield* Console.log(`${runtimeDir} will be removed once ${CLI_COMMAND} exits.`); } else { yield* fs .remove(plan.runtimeDir, { recursive: true, force: true }) diff --git a/apps/server/src/cli/up.ts b/apps/server/src/cli/up.ts index 8ee488a79a6d9..03a50ff45e74e 100644 --- a/apps/server/src/cli/up.ts +++ b/apps/server/src/cli/up.ts @@ -61,7 +61,7 @@ const formatSetup = (setup: Setup) => { // This home's access settings reach no service, so only the service is shown. if (service.otherBaseDir !== undefined) { return [ - ` ${"Service".padEnd(15)}installed for another T3 home · ${service.otherBaseDir}`, + ` ${"Service".padEnd(15)}installed for another ${APP_NAME} home · ${service.otherBaseDir}`, "", `Pass \`${BootService.formatBootServiceBaseDirOption(service.otherBaseDir)}\` to manage it.`, ].join("\n"); diff --git a/apps/server/src/cli/update.ts b/apps/server/src/cli/update.ts index 0db285b5d2f4c..c50f1f2c8cdca 100644 --- a/apps/server/src/cli/update.ts +++ b/apps/server/src/cli/update.ts @@ -169,7 +169,10 @@ export const repointLauncher = Effect.fn("cli.update.repoint_launcher")(function const remember = (file: string) => recordPinnedRuntimeLauncher({ baseDir: input.baseDir, launcher: file }).pipe( Effect.mapError( - () => new CliUpdateError({ reason: `Could not record the t3 launcher at ${file}.` }), + () => + new CliUpdateError({ + reason: `Could not record the ${CLI_COMMAND} launcher at ${file}.`, + }), ), ); const repoint = (file: string) => @@ -262,7 +265,7 @@ const updateFlags = { ...projectLocationFlags, channel: Flag.Literals("channel", CLI_RELEASE_CHANNELS).pipe( Flag.withDescription( - "Release channel to follow. Defaults to the channel this t3 was published on.", + `Release channel to follow. Defaults to the channel this ${APP_NAME} was published on.`, ), Flag.optional, ), @@ -291,7 +294,7 @@ export const updateCommand = Command.make("update", { version: versionArgument, }).pipe( Command.withDescription( - "Download a newer t3 and switch this machine to it, including the background service when one is installed.", + `Download a newer ${APP_NAME} and switch this machine to it, including the background service when one is installed.`, ), Command.withHandler((flags) => Effect.gen(function* () { @@ -393,7 +396,7 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: { } if (input.requestedVersion !== undefined && !isExactServiceVersion(input.requestedVersion)) { return yield* new CliUpdateError({ - reason: `'${input.requestedVersion}' is not an exact t3 version.`, + reason: `'${input.requestedVersion}' is not an exact ${APP_NAME} version.`, }); } const progress = createUpdateProgress(); @@ -495,7 +498,7 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: { let restartService = false; if (serviceInstalled && !serviceCurrent) { yield* Console.log( - " A background service is installed for this T3 home. Restarting it interrupts anything running in it: agent turns, terminals, remote clients.", + ` A background service is installed for this ${APP_NAME} home. Restarting it interrupts anything running in it: agent turns, terminals, remote clients.`, ); if (input.assumeYes) { restartService = true; @@ -641,7 +644,7 @@ export const runUpdate = Effect.fn("cli.update.run")(function* (input: { ); } else if (status.installed && !servesThisHome) { yield* Console.log( - ` The background service serves ${status.installedBaseDir ?? "another T3 home"} and was left unchanged.`, + ` The background service serves ${status.installedBaseDir ?? `another ${APP_NAME} home`} and was left unchanged.`, ); } if (foreground !== undefined) { diff --git a/apps/server/src/persistence/DatabaseMaintenance.ts b/apps/server/src/persistence/DatabaseMaintenance.ts index 475f85a74d457..bc33fd83202f2 100644 --- a/apps/server/src/persistence/DatabaseMaintenance.ts +++ b/apps/server/src/persistence/DatabaseMaintenance.ts @@ -16,7 +16,7 @@ import { isSqlError } from "effect/sql/SqlError"; import * as ProjectionMaintenance from "../orchestration-v2/ProjectionMaintenance.ts"; import { rebuildMessageSearch } from "../orchestration-v2/messageSearchIndex.ts"; -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; export class DatabaseMissingError extends Schema.TaggedError()( "DatabaseMissingError", @@ -279,7 +279,7 @@ export const runRequestedReclaim = Effect.fn("DatabaseMaintenance.runRequestedRe .writeFileString( files.failure, error._tag === "DatabaseInUseError" - ? `Another program kept the database open. Close other ${APP_NAME} apps and t3 commands on this machine, then try again.` + ? `Another program kept the database open. Close other ${APP_NAME} apps and ${CLI_COMMAND} commands on this machine, then try again.` : error.message, ) .pipe( diff --git a/apps/server/src/policy/policyTemplates.ts b/apps/server/src/policy/policyTemplates.ts index 70e15b8b15978..d83b68ca9ebf4 100644 --- a/apps/server/src/policy/policyTemplates.ts +++ b/apps/server/src/policy/policyTemplates.ts @@ -7,7 +7,7 @@ import { } from "@t3tools/shared/managedPolicyFile"; import { knownDriverLabels } from "./managedProviders.ts"; -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; type ValueName = keyof typeof WINDOWS_POLICY_VALUES; type ListKey = keyof typeof WINDOWS_POLICY_LISTS; @@ -159,7 +159,7 @@ const policies = (): ReadonlyArray => [ { id: "Settings", title: "Managed settings (JSON)", - help: `Defaults and locks for server settings, as the "settings" object of an ${APP_NAME} policy. Each key is a setting path; each value is {"value": ..., "locked": true|false}.\n\nExample: {"enableAgentBrowserAccess": {"value": false, "locked": true}}\n\nRun "t3 policy check" on a policy file with the same object to validate it first.`, + help: `Defaults and locks for server settings, as the "settings" object of an ${APP_NAME} policy. Each key is a setting path; each value is {"value": ..., "locked": true|false}.\n\nExample: {"enableAgentBrowserAccess": {"value": false, "locked": true}}\n\nRun "${CLI_COMMAND} policy check" on a policy file with the same object to validate it first.`, elements: [{ kind: "text", name: "Settings", label: "Settings JSON", json: true }], }, { diff --git a/apps/server/src/preview/PreviewBrowserHost.test.ts b/apps/server/src/preview/PreviewBrowserHost.test.ts index 9578372843323..58f97681541a4 100644 --- a/apps/server/src/preview/PreviewBrowserHost.test.ts +++ b/apps/server/src/preview/PreviewBrowserHost.test.ts @@ -83,7 +83,7 @@ describe("diagnoseLaunchFailure", () => { expect(error).toBeInstanceOf(PreviewBrowserHost.PreviewBrowserRootError); expect(error?.message).toContain("Ada server is running as root"); expect(error?.message).toContain("Ada server's environment and restart the server"); - expect(error?.message).toContain("T3CODE_SERVER_BROWSER_SANDBOX=0 t3 serve"); + expect(error?.message).toContain("T3CODE_SERVER_BROWSER_SANDBOX=0 ada serve"); expect(error?.message).toContain("Environment=T3CODE_SERVER_BROWSER_SANDBOX=0"); expect(error?.message).toContain("this disables Chrome's sandbox"); expect(commands).toEqual([]); diff --git a/apps/server/src/preview/PreviewBrowserHost.ts b/apps/server/src/preview/PreviewBrowserHost.ts index f052532e7b5b3..bb5dce75a5aee 100644 --- a/apps/server/src/preview/PreviewBrowserHost.ts +++ b/apps/server/src/preview/PreviewBrowserHost.ts @@ -4,7 +4,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Schema from "effect/Schema"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import { APP_NAME } from "@t3tools/shared/branding"; +import { APP_NAME, CLI_COMMAND } from "@t3tools/shared/branding"; export const SETUP_SUBCOMMAND = "browser setup"; @@ -18,7 +18,7 @@ const USERNS_RESTRICTION = "/proc/sys/kernel/apparmor_restrict_unprivileged_user export const APPARMOR_PROFILE_PATH = "/etc/apparmor.d/t3-chrome-headless-shell"; /** Match Ubuntu's Chrome profile; unconfined adds nothing beyond userns. */ -export const APPARMOR_PROFILE = `# Written by \`t3 browser setup\`: lets ${APP_NAME}'s headless browser use Chrome's sandbox. +export const APPARMOR_PROFILE = `# Written by \`${CLI_COMMAND} browser setup\`: lets ${APP_NAME}'s headless browser use Chrome's sandbox. abi , include @@ -74,7 +74,7 @@ export class PreviewBrowserRootError extends Schema.TaggedError()( "ServerAlreadyRunningError", @@ -50,7 +50,7 @@ export class ServerAlreadyRunningError extends Schema.TaggedError { ? [] : [ [ - `Server ownership lock at ${lockPath} was deleted or replaced. Another server may now share this T3 home. Stop any other server using this home, then restart this server. Do not delete server-owner.sqlite while a server is running.`, + `Server ownership lock at ${lockPath} was deleted or replaced. Another server may now share this Ada home. Stop any other server using this home, then restart this server. Do not delete server-owner.sqlite while a server is running.`, ], ]; assert.deepEqual(messages, expected); @@ -435,8 +435,8 @@ describe("server ownership", () => { assert.equal(result.failure._tag, "OlderBootServiceError"); assert.equal(Runtime.getErrorExitCode(result.failure), 78); assert.include(result.failure.message, t3Home); - assert.include(result.failure.message, "t3 update"); - assert.include(result.failure.message, "t3 service restart"); + assert.include(result.failure.message, "ada update"); + assert.include(result.failure.message, "ada service restart"); assert.include(result.failure.message, "No server was stopped"); } }).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))), diff --git a/apps/server/src/service/bootService.ts b/apps/server/src/service/bootService.ts index cc8db5a9598c2..44497e2968456 100644 --- a/apps/server/src/service/bootService.ts +++ b/apps/server/src/service/bootService.ts @@ -453,7 +453,7 @@ type BootServiceProblem = typeof BootServiceProblem.Type; export function formatBootServiceProblem(problem: BootServiceProblem): string { switch (problem) { case "user-manager-unavailable": - return "Cannot reach the systemd user manager. Run `systemctl --user status` in a login session for the service user. Install your distribution's systemd user-session support if it is missing; do not run T3 with sudo."; + return `Cannot reach the systemd user manager. Run \`systemctl --user status\` in a login session for the service user. Install your distribution's systemd user-session support if it is missing; do not run ${APP_NAME} with sudo.`; case "linger-unavailable": return 'Cannot check whether this user can run services after logout. Run `loginctl show-user "$(id -un)" --property=Linger` and check that systemd-logind is available.'; case "linger-disabled": @@ -837,7 +837,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { Effect.mapError( (cause) => new PinnedRuntimeInstallError({ - step: "verifying the pinned t3 runtime", + step: `verifying the pinned ${APP_NAME} runtime`, cause, }), ), @@ -847,7 +847,7 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { ? Effect.void : Effect.fail( new PinnedRuntimeInstallError({ - step: "verifying the pinned t3 runtime", + step: `verifying the pinned ${APP_NAME} runtime`, exitCode: Number(result.code), stdoutLength: result.stdout.length, stderrLength: result.stderr.length, diff --git a/apps/server/src/service/npmRelease.ts b/apps/server/src/service/npmRelease.ts index 96a32c0c87098..a5be888491325 100644 --- a/apps/server/src/service/npmRelease.ts +++ b/apps/server/src/service/npmRelease.ts @@ -14,6 +14,7 @@ import { parseChecksums, type CliReleaseChannel, } from "@t3tools/shared/cliRelease"; +import { APP_NAME } from "@t3tools/shared/branding"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Cache from "effect/Cache"; import * as Context from "effect/Context"; @@ -62,14 +63,14 @@ export const findNewestReleaseVersion = Effect.fn("NpmRelease.findNewestReleaseV token === undefined ? request : HttpClientRequest.setHeader(request, "Authorization", `Bearer ${token}`), - "Could not list t3 releases.", + `Could not list ${APP_NAME} releases.`, ); const releases = Option.getOrUndefined( decodeReleaseIndex(new TextDecoder().decode(Option.getOrElse(body, () => new Uint8Array()))), ); if (releases === undefined) { return yield* new ServerNpmReleaseError({ - reason: "The t3 release index had an unexpected shape.", + reason: `The ${APP_NAME} release index had an unexpected shape.`, }); } const version = newestCliReleaseVersion(releases, channel); diff --git a/apps/server/src/service/pinnedRuntime.test.ts b/apps/server/src/service/pinnedRuntime.test.ts index b84f3e78e136e..5af62b6f0d548 100644 --- a/apps/server/src/service/pinnedRuntime.test.ts +++ b/apps/server/src/service/pinnedRuntime.test.ts @@ -211,7 +211,7 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { validate: () => Effect.die("must not validate an unverified archive"), }).pipe(Effect.flip); assert.instanceOf(error, PinnedRuntimeInstallError); - assert.equal(error.step, "verifying the t3 release archive checksum"); + assert.equal(error.step, "verifying the release archive checksum"); assert.deepEqual(commands, []); assert.deepEqual(yield* fs.readDirectory(path.join(baseDir, "runtime", "versions")), []); }), @@ -331,7 +331,7 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { }); const withdrawn = yield* install("", []).pipe(Effect.flip); - assert.include(withdrawn.message, `finding ${archiveName} in the t3 release checksums`); + assert.include(withdrawn.message, `finding ${archiveName} in the release checksums`); const requests: string[] = []; assert.equal((yield* install(yield* validChecksums, requests)).entryPath, cached.entryPath); // Listed, so the cached runtime is used without downloading it again. diff --git a/apps/server/src/service/pinnedRuntime.ts b/apps/server/src/service/pinnedRuntime.ts index 90069f46f9173..f65c803ec5bee 100644 --- a/apps/server/src/service/pinnedRuntime.ts +++ b/apps/server/src/service/pinnedRuntime.ts @@ -21,6 +21,7 @@ import { parseChecksums, } from "@t3tools/shared/cliRelease"; +import { APP_NAME } from "@t3tools/shared/branding"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as ProcessRunner from "../processRunner.ts"; @@ -186,7 +187,7 @@ const installFromArchive = Effect.fn("cloud.pinned_runtime.install_archive")(fun const platformKey = cliArchivePlatformKey(input.platform, input.arch); if (platformKey === undefined) { return yield* new PinnedRuntimeInstallError({ - step: `selecting a t3 release archive for ${input.platform}-${input.arch}`, + step: `selecting a release archive for ${input.platform}-${input.arch}`, }); } const httpClient = input.httpClient; @@ -195,18 +196,18 @@ const installFromArchive = Effect.fn("cloud.pinned_runtime.install_archive")(fun const archive = yield* fetchReleaseAsset( httpClient, cliReleaseAssetUrl(input.version, fileName, input.releaseBaseUrl), - "downloading the t3 release archive", + "downloading the release archive", input.onProgress, ); input.onProgress?.({ stage: "verify" }); const digest = yield* Effect.tryPromise({ try: () => crypto.subtle.digest("SHA-256", archive), catch: (cause) => - new PinnedRuntimeInstallError({ step: "verifying the t3 release archive", cause }), + new PinnedRuntimeInstallError({ step: "verifying the release archive", cause }), }); if (Hex.encode(new Uint8Array(digest)) !== expected) { return yield* new PinnedRuntimeInstallError({ - step: "verifying the t3 release archive checksum", + step: "verifying the release archive checksum", }); } @@ -215,11 +216,11 @@ const installFromArchive = Effect.fn("cloud.pinned_runtime.install_archive")(fun .writeFile(archivePath, archive) .pipe( Effect.mapError( - (cause) => new PinnedRuntimeInstallError({ step: "writing the t3 release archive", cause }), + (cause) => new PinnedRuntimeInstallError({ step: "writing the release archive", cause }), ), ); input.onProgress?.({ stage: "extract" }); - const extractStep = "extracting the t3 release archive"; + const extractStep = "extracting the release archive"; // Strip the archive's stem directory so the executable lands at /t3. yield* input.runner .run({ @@ -254,14 +255,14 @@ const publishedArchive = Effect.fn("cloud.pinned_runtime.published_archive")(fun yield* fetchReleaseAsset( input.httpClient, cliReleaseAssetUrl(input.version, CLI_RELEASE_CHECKSUMS_FILE, input.releaseBaseUrl), - "downloading the t3 release checksums", + "downloading the release checksums", ), ), ); const expected = checksums.get(fileName); if (expected === undefined) { return yield* new PinnedRuntimeInstallError({ - step: `finding ${fileName} in the t3 release checksums`, + step: `finding ${fileName} in the release checksums`, }); } return { fileName, expected }; @@ -273,7 +274,7 @@ const isPinnedRuntimeInstallError = Schema.is(PinnedRuntimeInstallError); export const isUnpublishedRelease = (error: unknown): error is PinnedRuntimeInstallError => isPinnedRuntimeInstallError(error) && (error.step.startsWith("finding ") || - (error.step.startsWith("downloading the t3 release") && + (error.step.startsWith("downloading the release") && HttpClientError.isHttpClientError(error.cause) && error.cause.response?.status === 404)); @@ -298,7 +299,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( const platformKey = cliArchivePlatformKey(input.platform, input.arch); if (platformKey === undefined) { return yield* new PinnedRuntimeInstallError({ - step: `selecting a t3 release archive for ${input.platform}-${input.arch}`, + step: `selecting a release archive for ${input.platform}-${input.arch}`, }); } yield* publishedArchive(input, platformKey); @@ -663,7 +664,7 @@ const selectUnusedRuntimes = Effect.fn("cloud.pinned_runtime.select_unused")(fun Effect.andThen(fs.rename(versionDir, target)), Effect.andThen(Effect.sync(() => pruned.add(target))), Effect.catch((cause) => - Effect.logWarning("Could not remove an unused t3 runtime.", { version, cause }), + Effect.logWarning(`Could not remove an unused ${APP_NAME} runtime.`, { version, cause }), ), ); } @@ -695,7 +696,7 @@ export const prunePinnedRuntimes = Effect.fn("cloud.pinned_runtime.prune")(funct ), ), Effect.catch((cause) => - Effect.logWarning("Could not remove an unused t3 runtime.", { target, cause }), + Effect.logWarning(`Could not remove an unused ${APP_NAME} runtime.`, { target, cause }), ), ); } diff --git a/apps/server/src/service/selfUpdate.test.ts b/apps/server/src/service/selfUpdate.test.ts index 1416102c95eb7..04b6d656f0370 100644 --- a/apps/server/src/service/selfUpdate.test.ts +++ b/apps/server/src/service/selfUpdate.test.ts @@ -184,7 +184,7 @@ it.layer(NodeServices.layer)("server self update", (it) => { }); const error = yield* selfUpdate.update({ targetVersion: "1.1.0" }).pipe(Effect.flip); expect(error.reason).toBe( - `Could not prepare t3@1.1.0 while downloading the t3 release ${asset} (HTTP ${status} from cdn.example).`, + `Could not prepare t3@1.1.0 while downloading the release ${asset} (HTTP ${status} from cdn.example).`, ); expect(order).toEqual([]); const fs = yield* FileSystem.FileSystem; @@ -202,7 +202,7 @@ it.layer(NodeServices.layer)("server self update", (it) => { const web = yield* makeHarness(); expect( (yield* web.selfUpdate.update({ targetVersion: "latest" }).pipe(Effect.flip)).reason, - ).toBe("'latest' is not an exact t3 version."); + ).toBe("'latest' is not an exact Ada version."); const desktop = yield* makeHarness({ mode: "desktop" }); expect( (yield* desktop.selfUpdate.update({ targetVersion: "1.1.0" }).pipe(Effect.flip)).reason, diff --git a/apps/server/src/service/selfUpdate.ts b/apps/server/src/service/selfUpdate.ts index 1509414c8e244..80b3ec2e2596c 100644 --- a/apps/server/src/service/selfUpdate.ts +++ b/apps/server/src/service/selfUpdate.ts @@ -97,7 +97,7 @@ export const pruneRuntimesOnStart = Effect.fn("cloud.server_self_update.prune_on keep: [packageJson.version], }); if (removed.length > 0) { - yield* Effect.logInfo("Removed unused t3 runtimes.", { versions: removed }); + yield* Effect.logInfo(`Removed unused ${APP_NAME} runtimes.`, { versions: removed }); } return removed; }, @@ -194,7 +194,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { if (unsafeCrossing !== null) return yield* failWith(unsafeCrossing); const targetVersion = input.targetVersion.trim(); if (!isExactServiceVersion(targetVersion)) { - return yield* failWith(`'${targetVersion}' is not an exact t3 version.`); + return yield* failWith(`'${targetVersion}' is not an exact ${APP_NAME} version.`); } if (yield* Ref.getAndSet(inFlight, true)) { return yield* failWith("A server update is already in progress."); diff --git a/apps/server/src/service/serviceLauncherClient.ts b/apps/server/src/service/serviceLauncherClient.ts index b2a563c06e550..7566adba0419b 100644 --- a/apps/server/src/service/serviceLauncherClient.ts +++ b/apps/server/src/service/serviceLauncherClient.ts @@ -1,4 +1,5 @@ import type { ServerSelfUpdateOutcome } from "@t3tools/contracts"; +import { APP_NAME } from "@t3tools/shared/branding"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -33,7 +34,7 @@ export class ServiceLauncherClientError extends Schema.TaggedError { { refusal: "another server owns the T3 home", code: SERVER_EXIT_CODE_STATE_DIR_OWNED, - message: "owns this T3 home", + message: "owns this Ada home", }, { refusal: "the database is refused", diff --git a/apps/server/src/serviceLauncher.ts b/apps/server/src/serviceLauncher.ts index ae9faf701b7fe..22697dad782e6 100644 --- a/apps/server/src/serviceLauncher.ts +++ b/apps/server/src/serviceLauncher.ts @@ -1076,7 +1076,9 @@ export class Launcher { await discardDatabaseBackup(this.#baseDir, pending.id).catch(() => undefined); await this.#repointServiceUnit(next.activeVersion); } - this.#idle(`Another ${APP_NAME} server owns this T3 home. Stop it, then restart this service.`); + this.#idle( + `Another ${APP_NAME} server owns this ${APP_NAME} home. Stop it, then restart this service.`, + ); } /** diff --git a/apps/server/src/telemetry/AnalyticsService.test.ts b/apps/server/src/telemetry/AnalyticsService.test.ts index 4d3c2d5421279..d1574935b9876 100644 --- a/apps/server/src/telemetry/AnalyticsService.test.ts +++ b/apps/server/src/telemetry/AnalyticsService.test.ts @@ -113,20 +113,22 @@ const recordTwentyEvents = Effect.gen(function* () { } }); -const analyticsLayerWith = (client: HttpClient.HttpClient, policy?: ManagedPolicyDocument) => +const OPTED_IN = { + T3CODE_TELEMETRY_ENABLED: true, + T3CODE_POSTHOG_KEY: "phc_test_key", + T3CODE_POSTHOG_HOST: "http://localhost", +}; + +const analyticsLayerWith = ( + client: HttpClient.HttpClient, + policy?: ManagedPolicyDocument, + config: Record = OPTED_IN, +) => AnalyticsService.layer.pipe( Layer.provideMerge( ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-telemetry-retry-" }), ), - Layer.provide( - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - T3CODE_TELEMETRY_ENABLED: true, - T3CODE_POSTHOG_KEY: "phc_test_key", - T3CODE_POSTHOG_HOST: "http://localhost", - }), - ), - ), + Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(config))), Layer.provide( Layer.mergeAll( Layer.succeed(HostProcess.Platform, "win32"), @@ -151,6 +153,7 @@ const sendFor = ( AnalyticsService.AnalyticsService | Effect.Services > = recordTwentyEvents, policy?: ManagedPolicyDocument, + config?: Record, ) => Effect.gen(function* () { const batches: Array> = []; @@ -165,7 +168,7 @@ const sendFor = ( return yield* respond(request); }), ); - const runtimeLayer = analyticsLayerWith(client, policy); + const runtimeLayer = analyticsLayerWith(client, policy, config); yield* Effect.gen(function* () { yield* record; @@ -203,6 +206,46 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("AnalyticsService }), ); + it.effect("sends nothing unless T3CODE_TELEMETRY_ENABLED opts in", () => + Effect.gen(function* () { + const { T3CODE_TELEMETRY_ENABLED: _optIn, ...unset } = OPTED_IN; + const batches = yield* sendFor( + 10, + respondWithStatus(204), + recordTwentyEvents, + undefined, + unset, + ).pipe(Effect.provideService(HostProcess.Environment, {})); + assert.deepEqual(batches, []); + }), + ); + + it.effect("lets the desktop's login shell opt in after discovery", () => + Effect.gen(function* () { + const { T3CODE_TELEMETRY_ENABLED: _optIn, ...unset } = OPTED_IN; + const env: NodeJS.ProcessEnv = { T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING: "1" }; + const batches = yield* sendFor( + 0, + respondWithStatus(204), + Effect.gen(function* () { + const analytics = yield* AnalyticsService.AnalyticsService; + delete env.T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING; + yield* analytics.record("test.shell-unset"); + yield* analytics.flush; + env.T3CODE_TELEMETRY_ENABLED = "true"; + yield* analytics.record("test.shell-opted-in"); + yield* analytics.flush; + }), + undefined, + unset, + ).pipe(Effect.provideService(HostProcess.Environment, env)); + assert.deepEqual( + batches.flat().map((event) => event.event), + ["test.shell-opted-in"], + ); + }), + ); + it.effect("client owner reports cannot use up the server's owner cap", () => Effect.gen(function* () { const batches = yield* sendFor( diff --git a/apps/server/src/telemetry/AnalyticsService.ts b/apps/server/src/telemetry/AnalyticsService.ts index 384da92b3f312..2de4634ed7781 100644 --- a/apps/server/src/telemetry/AnalyticsService.ts +++ b/apps/server/src/telemetry/AnalyticsService.ts @@ -71,7 +71,8 @@ const TelemetryEnvConfig = Config.all({ posthogHost: Config.String("T3CODE_POSTHOG_HOST").pipe( Config.withDefault("https://ingest.t3.codes"), ), - enabled: Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), + // Off unless opted in: the fork does not report to upstream by default. + enabled: Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(false)), flushBatchSize: Config.Number("T3CODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), maxBufferedEvents: Config.Number("T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( Config.withDefault(1_000), @@ -127,13 +128,13 @@ export const make = Effect.gen(function* () { ManagedPolicy.deniedBy(yield* ManagedPolicy.ManagedPolicy, "telemetry") === undefined; // A cached PATH does not establish consent from the current login shell. const telemetryEnabled = Effect.suspend(() => { - if (!telemetryConfig.enabled || !policyAllows) return Effect.succeed(false); - if (!awaitingShellConsent) return Effect.succeed(true); + if (!policyAllows) return Effect.succeed(false); + if (!awaitingShellConsent) return Effect.succeed(telemetryConfig.enabled); if (hostEnvironment.T3CODE_DESKTOP_SHELL_DISCOVERY_PENDING === "1") return Effect.succeed(false); const preference = hostEnvironment.T3CODE_TELEMETRY_ENABLED; return Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe( - Config.withDefault(true), + Config.withDefault(false), Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png index 3eed25ea6b785..d1f37f18c8c4f 100644 Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ diff --git a/apps/web/public/brand-mark.png b/apps/web/public/brand-mark.png index d09cfd24614e2..761c5949d68e5 100644 Binary files a/apps/web/public/brand-mark.png and b/apps/web/public/brand-mark.png differ diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png index a3431b8c6dfef..161703c169a31 100644 Binary files a/apps/web/public/favicon-16x16.png and b/apps/web/public/favicon-16x16.png differ diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png index 862f7629971fa..bb18b62def5d3 100644 Binary files a/apps/web/public/favicon-32x32.png and b/apps/web/public/favicon-32x32.png differ diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index 750da22602eec..571b446bec91d 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/icon-192x192.png b/apps/web/public/icon-192x192.png index 3e2637386b1d1..1b395ad79a07c 100644 Binary files a/apps/web/public/icon-192x192.png and b/apps/web/public/icon-192x192.png differ diff --git a/apps/web/public/icon-512x512.png b/apps/web/public/icon-512x512.png index 1fac754a327fd..ac8802bc6fc1b 100644 Binary files a/apps/web/public/icon-512x512.png and b/apps/web/public/icon-512x512.png differ diff --git a/apps/web/src/components/HostedEmptyState.tsx b/apps/web/src/components/HostedEmptyState.tsx index edf6a00d9c2b1..9e2b706b5a672 100644 --- a/apps/web/src/components/HostedEmptyState.tsx +++ b/apps/web/src/components/HostedEmptyState.tsx @@ -58,7 +58,7 @@ function PairingSteps() { return ( <> - + } /> diff --git a/apps/web/src/components/settings/StorageSettings.tsx b/apps/web/src/components/settings/StorageSettings.tsx index 312fdde385467..666fb1a3fd40b 100644 --- a/apps/web/src/components/settings/StorageSettings.tsx +++ b/apps/web/src/components/settings/StorageSettings.tsx @@ -69,7 +69,7 @@ export function WorktreesDirectoryRow() { writeTextToClipboard(TRIAGE_CLI_COMMAND, "command").catch(() => @@ -31,7 +31,7 @@ const copyTriageCliCommand = () => export const isTriagePrompt = (prompt: string) => prompt.includes(`[${TRIAGE_PROMPT_FILE_NAME}](`); /** - * In-app `t3 triage`: the server writes the session files and starts cloning + * In-app `ada triage`: the server writes the session files and starts cloning * the matching source, then a No project draft opens with the playbook linked. * The draft hero explains the rest; the user picks a provider and describes * the problem. diff --git a/apps/web/src/legalLinks.ts b/apps/web/src/legalLinks.ts deleted file mode 100644 index 2c42b814355f0..0000000000000 --- a/apps/web/src/legalLinks.ts +++ /dev/null @@ -1 +0,0 @@ -export const PRIVACY_POLICY_URL = "https://t3.codes/privacy-policy"; diff --git a/apps/web/src/lib/bootError.test.ts b/apps/web/src/lib/bootError.test.ts index e903c13b0b287..4083990a80064 100644 --- a/apps/web/src/lib/bootError.test.ts +++ b/apps/web/src/lib/bootError.test.ts @@ -43,18 +43,18 @@ describe("resolveTriageCommand", () => { it("falls back to npx without a usable desktop launcher", async () => { using _warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - await expect(resolveTriageCommand(undefined)).resolves.toBe("t3 triage"); - await expect(resolveTriageCommand({} as DesktopBridge)).resolves.toBe("t3 triage"); + await expect(resolveTriageCommand(undefined)).resolves.toBe("ada triage"); + await expect(resolveTriageCommand({} as DesktopBridge)).resolves.toBe("ada triage"); await expect(resolveTriageCommand(desktopWith(async () => notOnPath(null)))).resolves.toBe( - "t3 triage", + "ada triage", ); const staleWindowsPathEntry = { ...notOnPath(null), supported: true, onPath: true }; await expect( resolveTriageCommand(desktopWith(async () => staleWindowsPathEntry)), - ).resolves.toBe("t3 triage"); + ).resolves.toBe("ada triage"); await expect( resolveTriageCommand(desktopWith(() => Promise.reject(new Error("ipc closed")))), - ).resolves.toBe("t3 triage"); + ).resolves.toBe("ada triage"); }); }); diff --git a/apps/web/src/lib/bootError.ts b/apps/web/src/lib/bootError.ts index d6217473a660b..486265023fc92 100644 --- a/apps/web/src/lib/bootError.ts +++ b/apps/web/src/lib/bootError.ts @@ -3,7 +3,7 @@ import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts"; // Boot code loads before any package resolves, so it repeats APP_NAME and CLI_RELEASE_REPOSITORY from @t3tools/shared. const APP_NAME = "Ada"; const REPOSITORY_URL = "https://github.com/jayleaton/t3code"; -const FALLBACK_TRIAGE_COMMAND = "t3 triage"; +const FALLBACK_TRIAGE_COMMAND = "ada triage"; const DOWNLOAD_URL = `${REPOSITORY_URL}/releases`; const ISSUE_URL = `${REPOSITORY_URL}/issues/new`; const LAST_FAILURE_KEY = "t3code:boot-failed-at"; @@ -26,12 +26,13 @@ function launcherCommand(launcherPath: string) { : `'${launcherPath.replaceAll("'", `'"'"'`)}'`; } -/** Desktop can run its bundled launcher; everything else falls back to `t3` on PATH. */ +/** Desktop can run its bundled launcher; everything else falls back to `ada` on PATH. */ export async function resolveTriageCommand(bridge: DesktopBridge | undefined) { try { const state = await bridge?.cliCommand.getState(); // Windows reports onPath from its PATH entry alone; only an existing launcher proves it runs. if (state?.launcherPath) { + // onPath reports the `t3` link; `ada` beside it is a best-effort alias. return state.onPath ? "t3 triage" : `${launcherCommand(state.launcherPath)} triage`; } } catch (error) { diff --git a/apps/web/src/state/managedPolicy.ts b/apps/web/src/state/managedPolicy.ts index 4e588b3796012..1ca30648b46a5 100644 --- a/apps/web/src/state/managedPolicy.ts +++ b/apps/web/src/state/managedPolicy.ts @@ -1,4 +1,5 @@ import type { EnvironmentId, ManagedPolicySummary, ProviderDriverKind } from "@t3tools/contracts"; +import { APP_NAME } from "@t3tools/shared/branding"; import { useServerConfigs } from "./entities"; import { usePrimaryEnvironmentId } from "./environments"; @@ -30,13 +31,11 @@ export function firstManagedDenial( return undefined; } -/** What usage-data notices say when the policy turns product analytics off. */ -export function managedTelemetryOffMessage( - policy: ManagedPolicySummary | undefined, -): string | undefined { +/** What usage-data notices say; a policy can only turn product analytics off. */ +export function usageDataNotice(policy: ManagedPolicySummary | undefined): string { return policy?.limits?.telemetry === false ? `${policy.organization} turned off anonymous usage data.` - : undefined; + : `${APP_NAME} doesn't collect usage data unless you turn it on with T3CODE_TELEMETRY_ENABLED.`; } export interface ManagedInstanceLocks { diff --git a/apps/web/src/themeBoot.test.ts b/apps/web/src/themeBoot.test.ts index 8341978986bbe..ad970a71aa264 100644 --- a/apps/web/src/themeBoot.test.ts +++ b/apps/web/src/themeBoot.test.ts @@ -153,12 +153,12 @@ describe("index.html boot script", () => { }> = [ { name: "no stored preference on a dark OS", storage: {}, prefersDark: true }, { - name: "T3 Chat follows a dark OS", + name: "Orchid follows a dark OS", storage: { [THEME_STORAGE_KEY]: "t3-chat", [THEME_FOLLOW_SYSTEM_STORAGE_KEY]: "true" }, prefersDark: true, }, { - name: "an explicit global dark mode applies to T3 Chat", + name: "an explicit global dark mode applies to Orchid", storage: { [THEME_STORAGE_KEY]: "t3-chat", [THEME_APPEARANCE_MODE_STORAGE_KEY]: "dark", @@ -192,7 +192,7 @@ describe("index.html boot script", () => { prefersDark: true, }, { - name: "legacy t3-chat-dark resolves to dark T3 Chat", + name: "legacy t3-chat-dark resolves to dark Orchid", storage: { [THEME_STORAGE_KEY]: "t3-chat-dark" }, prefersDark: true, }, diff --git a/assets/README.md b/assets/README.md index f03136d3231bc..2caecbf93d089 100644 --- a/assets/README.md +++ b/assets/README.md @@ -1,67 +1,23 @@ # Brand icons -The three Icon Composer projects are the source of truth for full application icons: +`agents/pip.svg` is the hand-edited source for Ada's mark, Pip, and every icon is generated from it. `agents/pip-lockup-{dark,light}.{svg,png}` are the approved wordmark lockups, used as-is by the README and, through the export, the social card. -- `dev/app-icon.icon` -- `nightly/app-icon.icon` -- `prod/app-icon.icon` +The three Icon Composer projects give each release channel its backdrop: -Each project uses `text.svg` for the T3 mark and `background.svg` when the background is a vector layer. Additional layers use semantic names that describe their role and placement. +- `dev/app-icon.icon`: blueprint +- `nightly/app-icon.icon`: night sky +- `prod/app-icon.icon`: solid navy -Run `vp run icons:export` from the repository root to regenerate the tracked iOS, Linux, Windows, and web assets. The development web exports are also copied to `apps/web/public` for the browser favicon, web app manifest icons, and splash screen. Run `vp run icons:check` to verify that the generated assets and public copies match their sources without changing files. +Each project draws Pip from its own `Assets/pip.svg`, which the export writes from `agents/pip.svg` in that channel's colour. Edit the master, not these copies. -Exporting requires Icon Composer 2 or newer on macOS. The script selects the newest compatible exporter from Xcode or a standalone Icon Composer installation and pins design generation 26. Set `ICON_COMPOSER_TOOL` to the full path of `Icon Composer.app/Contents/Executables/ictool` to override automatic discovery. +Run `vp run icons:export` from the repository root to regenerate the tracked iOS, macOS, Linux, Windows, and web assets, plus the fork's `agents/` set, `apps/web/public/brand-mark.png`, and the marketing favicons, icons, and social card. The development web exports are also copied to `apps/web/public` for the browser favicon and web app manifest icons. Run `vp run icons:check` to verify that every generated asset matches its source without changing files. -## macOS exports +Exporting requires Icon Composer 2 or newer on macOS. The script selects the newest compatible exporter from Xcode or a standalone Icon Composer installation and pins design generation 26. Set `ICON_COMPOSER_TOOL` to the full path of `Icon Composer.app/Contents/Executables/ictool` to override automatic discovery. The social card's text uses the system's Helvetica Neue. -Icon Composer's command-line exporter does not expose the `macOS pre-Tahoe` preset. A plain command-line `macOS` export is full bleed and is not suitable for the desktop app, so the export script intentionally leaves the tracked macOS PNGs unchanged and prints a reminder after every run. +Icon Composer's command-line exporter has no `macOS pre-Tahoe` preset; its `macOS` export is full bleed. The export therefore places the rendered icon on Apple's classic safe area, an 824×824 body inset 100 pixels on a 1024×1024 canvas, without Icon Composer's native shadow. -After changing an Icon Composer project, open it in Icon Composer and export the macOS PNG with exactly these settings: +Do not edit the generated PNG, ICO, or WebP files directly. -- Platform: `macOS pre-Tahoe` -- Appearance: `Default` -- Size: `1024pt` -- Scale: `1×` +## Mobile launcher, splash, and widget artwork -Save the three exports to: - -- `dev/app-icon.icon` -> `dev/blueprint-macos-1024.png` -- `nightly/app-icon.icon` -> `nightly/nightly-macos-1024.png` -- `prod/app-icon.icon` -> `prod/black-macos-1024.png` - -The result must be a 1024×1024 PNG with the classic macOS safe area: the opaque icon body is 824×824, inset 100 pixels on every side, with only the native Icon Composer shadow extending into the surrounding transparent canvas. - -To have Codex perform the native exports, paste this prompt into a task opened at the repository root: - -```text -Use [@Computer](plugin://computer-use@openai-bundled) and the Icon Composer app to export the three macOS app icons in this repository. - -For each project below, use Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, and Scale: 1×, then save the PNG to the exact destination: - -- assets/dev/app-icon.icon -> assets/dev/blueprint-macos-1024.png -- assets/nightly/app-icon.icon -> assets/nightly/nightly-macos-1024.png -- assets/prod/app-icon.icon -> assets/prod/black-macos-1024.png - -Do not resize, composite, or otherwise post-process the exported PNGs. - -Verify every result is 1024×1024 and has the classic macOS safe area: an 824×824 opaque body inset 100px on every side, with only Icon Composer's native shadow extending beyond it. -``` - -Do not edit the generated PNG or ICO files directly. - -## Android launcher and splash artwork - -Android masks the central 72dp of a 108dp adaptive canvas, and the Android 12+ splash screen masks -the central two thirds of a 288dp canvas, so the Icon Composer exports cannot be used directly: -their rounded-square silhouette gets framed again and the wordmark is cropped. The Android artwork -is instead rendered from the same Icon Composer SVG sources by `vp run icons:export:android`: - -- `apps/mobile/assets/android-icon-foreground.png`: the shared transparent wordmark, sized to stay - inside the safe zone -- `apps/mobile/assets/android-icon-background-dev.png` and `-nightly.png`: full-bleed variant - artwork (blueprint grid and annotations; night sky and clouds). Production uses a solid color. -- `apps/mobile/assets/android-splash-icon-*.png`: the two layers composed into one 288dp image, so - the splash mask reproduces the launcher icon's framing. - -Rerun the export after changing a layer SVG. `android-icon-mark.png` remains a flat silhouette for -Android's monochrome themed icon. +`vp run icons:export:android` renders the mobile app's assets from `agents/pip.svg` into `apps/mobile/assets/agents/` and the widget's template mark into `apps/mobile/assets/widget/PipMark.svg`. iOS gets a full-bleed navy square because it applies its own mask. Android masks the central 72dp of a 108dp adaptive canvas and the Android 12+ splash screen masks the central two thirds of a 288dp canvas, so Android gets Pip alone, sized to stay inside those safe zones over a navy background. Every app variant uses the same set. diff --git a/assets/agents/apple-touch-icon.png b/assets/agents/apple-touch-icon.png index d09cfd24614e2..761c5949d68e5 100644 Binary files a/assets/agents/apple-touch-icon.png and b/assets/agents/apple-touch-icon.png differ diff --git a/assets/agents/favicon-16x16.png b/assets/agents/favicon-16x16.png index bacf8fa640a4b..427bbe125ea8a 100644 Binary files a/assets/agents/favicon-16x16.png and b/assets/agents/favicon-16x16.png differ diff --git a/assets/agents/favicon-32x32.png b/assets/agents/favicon-32x32.png index 48ed2a804ea7e..6eff5df10a9b5 100644 Binary files a/assets/agents/favicon-32x32.png and b/assets/agents/favicon-32x32.png differ diff --git a/assets/agents/favicon.ico b/assets/agents/favicon.ico index ff018ba62d645..9e249762a4ff2 100644 Binary files a/assets/agents/favicon.ico and b/assets/agents/favicon.ico differ diff --git a/assets/agents/icon-192x192.png b/assets/agents/icon-192x192.png index e4642bf746c47..fe58dbc644de8 100644 Binary files a/assets/agents/icon-192x192.png and b/assets/agents/icon-192x192.png differ diff --git a/assets/agents/icon-512x512.png b/assets/agents/icon-512x512.png index 9280ea3167f8c..8495d609e91f9 100644 Binary files a/assets/agents/icon-512x512.png and b/assets/agents/icon-512x512.png differ diff --git a/assets/agents/icon.png b/assets/agents/icon.png index 63bf58a0e114c..28c36e309b9d6 100644 Binary files a/assets/agents/icon.png and b/assets/agents/icon.png differ diff --git a/assets/agents/pip-lockup-dark.png b/assets/agents/pip-lockup-dark.png new file mode 100644 index 0000000000000..6bf48f8ad927b Binary files /dev/null and b/assets/agents/pip-lockup-dark.png differ diff --git a/assets/agents/pip-lockup-dark.svg b/assets/agents/pip-lockup-dark.svg new file mode 100644 index 0000000000000..8abf3478d6886 --- /dev/null +++ b/assets/agents/pip-lockup-dark.svg @@ -0,0 +1 @@ +Engineering is better with Agents. \ No newline at end of file diff --git a/assets/agents/pip-lockup-light.png b/assets/agents/pip-lockup-light.png new file mode 100644 index 0000000000000..28f9577028275 Binary files /dev/null and b/assets/agents/pip-lockup-light.png differ diff --git a/assets/agents/pip-lockup-light.svg b/assets/agents/pip-lockup-light.svg new file mode 100644 index 0000000000000..c0bd60885bf9d --- /dev/null +++ b/assets/agents/pip-lockup-light.svg @@ -0,0 +1 @@ +Engineering is better with Agents. \ No newline at end of file diff --git a/assets/agents/pip.svg b/assets/agents/pip.svg new file mode 100644 index 0000000000000..c73a798ef9ab0 --- /dev/null +++ b/assets/agents/pip.svg @@ -0,0 +1 @@ + diff --git a/assets/agents/source.png b/assets/agents/source.png deleted file mode 100644 index fcff557ef6565..0000000000000 Binary files a/assets/agents/source.png and /dev/null differ diff --git a/assets/agents/windows.ico b/assets/agents/windows.ico index 8990e2635b055..9e249762a4ff2 100644 Binary files a/assets/agents/windows.ico and b/assets/agents/windows.ico differ diff --git a/assets/dev/app-icon.icon/Assets/pip.svg b/assets/dev/app-icon.icon/Assets/pip.svg new file mode 100644 index 0000000000000..6f96e6692bddb --- /dev/null +++ b/assets/dev/app-icon.icon/Assets/pip.svg @@ -0,0 +1 @@ + diff --git a/assets/dev/app-icon.icon/Assets/text.svg b/assets/dev/app-icon.icon/Assets/text.svg deleted file mode 100644 index 0b1825f2ef471..0000000000000 --- a/assets/dev/app-icon.icon/Assets/text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/dev/app-icon.icon/icon.json b/assets/dev/app-icon.icon/icon.json index ff7d43d781120..20f54bb43e78f 100644 --- a/assets/dev/app-icon.icon/icon.json +++ b/assets/dev/app-icon.icon/icon.json @@ -7,11 +7,11 @@ "layers": [ { "glass": true, - "image-name": "text.svg", - "name": "Text", + "image-name": "pip.svg", + "name": "Pip", "position": { - "scale": 8.5, - "translation-in-points": [0, 0] + "scale": 8.4, + "translation-in-points": [0, -24] } }, { diff --git a/assets/dev/blueprint-ios-1024.png b/assets/dev/blueprint-ios-1024.png index a53c8d19aa83a..3ed88f700e7d8 100644 Binary files a/assets/dev/blueprint-ios-1024.png and b/assets/dev/blueprint-ios-1024.png differ diff --git a/assets/dev/blueprint-macos-1024.png b/assets/dev/blueprint-macos-1024.png index 68a567058be10..10d4005727300 100644 Binary files a/assets/dev/blueprint-macos-1024.png and b/assets/dev/blueprint-macos-1024.png differ diff --git a/assets/dev/blueprint-universal-1024.png b/assets/dev/blueprint-universal-1024.png index a53c8d19aa83a..3ed88f700e7d8 100644 Binary files a/assets/dev/blueprint-universal-1024.png and b/assets/dev/blueprint-universal-1024.png differ diff --git a/assets/dev/blueprint-web-apple-touch-180.png b/assets/dev/blueprint-web-apple-touch-180.png index 3eed25ea6b785..d1f37f18c8c4f 100644 Binary files a/assets/dev/blueprint-web-apple-touch-180.png and b/assets/dev/blueprint-web-apple-touch-180.png differ diff --git a/assets/dev/blueprint-web-favicon-16x16.png b/assets/dev/blueprint-web-favicon-16x16.png index a3431b8c6dfef..161703c169a31 100644 Binary files a/assets/dev/blueprint-web-favicon-16x16.png and b/assets/dev/blueprint-web-favicon-16x16.png differ diff --git a/assets/dev/blueprint-web-favicon-32x32.png b/assets/dev/blueprint-web-favicon-32x32.png index 862f7629971fa..bb18b62def5d3 100644 Binary files a/assets/dev/blueprint-web-favicon-32x32.png and b/assets/dev/blueprint-web-favicon-32x32.png differ diff --git a/assets/dev/blueprint-web-favicon.ico b/assets/dev/blueprint-web-favicon.ico index 750da22602eec..571b446bec91d 100644 Binary files a/assets/dev/blueprint-web-favicon.ico and b/assets/dev/blueprint-web-favicon.ico differ diff --git a/assets/dev/blueprint-web-icon-192x192.png b/assets/dev/blueprint-web-icon-192x192.png index 3e2637386b1d1..1b395ad79a07c 100644 Binary files a/assets/dev/blueprint-web-icon-192x192.png and b/assets/dev/blueprint-web-icon-192x192.png differ diff --git a/assets/dev/blueprint-web-icon-512x512.png b/assets/dev/blueprint-web-icon-512x512.png index 1fac754a327fd..ac8802bc6fc1b 100644 Binary files a/assets/dev/blueprint-web-icon-512x512.png and b/assets/dev/blueprint-web-icon-512x512.png differ diff --git a/assets/dev/blueprint-windows.ico b/assets/dev/blueprint-windows.ico index 750da22602eec..571b446bec91d 100644 Binary files a/assets/dev/blueprint-windows.ico and b/assets/dev/blueprint-windows.ico differ diff --git a/assets/nightly/app-icon.icon/Assets/pip.svg b/assets/nightly/app-icon.icon/Assets/pip.svg new file mode 100644 index 0000000000000..6f96e6692bddb --- /dev/null +++ b/assets/nightly/app-icon.icon/Assets/pip.svg @@ -0,0 +1 @@ + diff --git a/assets/nightly/app-icon.icon/Assets/text.svg b/assets/nightly/app-icon.icon/Assets/text.svg deleted file mode 100644 index 0b1825f2ef471..0000000000000 --- a/assets/nightly/app-icon.icon/Assets/text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/nightly/app-icon.icon/icon.json b/assets/nightly/app-icon.icon/icon.json index eb48d26b4a521..06722c561c74c 100644 --- a/assets/nightly/app-icon.icon/icon.json +++ b/assets/nightly/app-icon.icon/icon.json @@ -7,11 +7,11 @@ "layers": [ { "glass": true, - "image-name": "text.svg", - "name": "Text", + "image-name": "pip.svg", + "name": "Pip", "position": { - "scale": 8.5, - "translation-in-points": [0, 0] + "scale": 8.4, + "translation-in-points": [0, -24] } }, { diff --git a/assets/nightly/nightly-ios-1024.png b/assets/nightly/nightly-ios-1024.png index 42ce5589e9cac..a9cf4b09080fc 100644 Binary files a/assets/nightly/nightly-ios-1024.png and b/assets/nightly/nightly-ios-1024.png differ diff --git a/assets/nightly/nightly-macos-1024.png b/assets/nightly/nightly-macos-1024.png index 1beacd9f34ade..a0e45a2d50535 100644 Binary files a/assets/nightly/nightly-macos-1024.png and b/assets/nightly/nightly-macos-1024.png differ diff --git a/assets/nightly/nightly-universal-1024.png b/assets/nightly/nightly-universal-1024.png index 42ce5589e9cac..a9cf4b09080fc 100644 Binary files a/assets/nightly/nightly-universal-1024.png and b/assets/nightly/nightly-universal-1024.png differ diff --git a/assets/nightly/nightly-web-apple-touch-180.png b/assets/nightly/nightly-web-apple-touch-180.png index f09a169458ab4..ab697564aa59a 100644 Binary files a/assets/nightly/nightly-web-apple-touch-180.png and b/assets/nightly/nightly-web-apple-touch-180.png differ diff --git a/assets/nightly/nightly-web-favicon-16x16.png b/assets/nightly/nightly-web-favicon-16x16.png index 20208f5f36951..8cc9e0c87cc9d 100644 Binary files a/assets/nightly/nightly-web-favicon-16x16.png and b/assets/nightly/nightly-web-favicon-16x16.png differ diff --git a/assets/nightly/nightly-web-favicon-32x32.png b/assets/nightly/nightly-web-favicon-32x32.png index b9008d518bce0..74968175bccce 100644 Binary files a/assets/nightly/nightly-web-favicon-32x32.png and b/assets/nightly/nightly-web-favicon-32x32.png differ diff --git a/assets/nightly/nightly-web-favicon.ico b/assets/nightly/nightly-web-favicon.ico index b6a0b43b93d2e..d76abcfcc4011 100644 Binary files a/assets/nightly/nightly-web-favicon.ico and b/assets/nightly/nightly-web-favicon.ico differ diff --git a/assets/nightly/nightly-web-icon-192x192.png b/assets/nightly/nightly-web-icon-192x192.png index 95e9256dc8abb..59228ef9d890d 100644 Binary files a/assets/nightly/nightly-web-icon-192x192.png and b/assets/nightly/nightly-web-icon-192x192.png differ diff --git a/assets/nightly/nightly-web-icon-512x512.png b/assets/nightly/nightly-web-icon-512x512.png index 6ce057fba4a67..8dda1571c8729 100644 Binary files a/assets/nightly/nightly-web-icon-512x512.png and b/assets/nightly/nightly-web-icon-512x512.png differ diff --git a/assets/nightly/nightly-windows.ico b/assets/nightly/nightly-windows.ico index b6a0b43b93d2e..d76abcfcc4011 100644 Binary files a/assets/nightly/nightly-windows.ico and b/assets/nightly/nightly-windows.ico differ diff --git a/assets/prod/app-icon.icon/Assets/pip.svg b/assets/prod/app-icon.icon/Assets/pip.svg new file mode 100644 index 0000000000000..c73a798ef9ab0 --- /dev/null +++ b/assets/prod/app-icon.icon/Assets/pip.svg @@ -0,0 +1 @@ + diff --git a/assets/prod/app-icon.icon/Assets/text.svg b/assets/prod/app-icon.icon/Assets/text.svg deleted file mode 100644 index 0b1825f2ef471..0000000000000 --- a/assets/prod/app-icon.icon/Assets/text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/prod/app-icon.icon/icon.json b/assets/prod/app-icon.icon/icon.json index 6976a6a724c81..48da42d07cb5f 100644 --- a/assets/prod/app-icon.icon/icon.json +++ b/assets/prod/app-icon.icon/icon.json @@ -1,16 +1,16 @@ { "fill": { - "solid": "display-p3:0.00000,0.00000,0.00000,1.00000" + "solid": "srgb:0.06667,0.09804,0.14510,1.00000" }, "groups": [ { "layers": [ { - "image-name": "text.svg", - "name": "Text", + "image-name": "pip.svg", + "name": "Pip", "position": { - "scale": 8.5, - "translation-in-points": [0, 0] + "scale": 8.4, + "translation-in-points": [0, -24] } } ], diff --git a/assets/prod/black-ios-1024.png b/assets/prod/black-ios-1024.png index bfc80410aadc5..f5e15607d7cf1 100644 Binary files a/assets/prod/black-ios-1024.png and b/assets/prod/black-ios-1024.png differ diff --git a/assets/prod/black-macos-1024.png b/assets/prod/black-macos-1024.png index 6a0fe10d3dacc..28c36e309b9d6 100644 Binary files a/assets/prod/black-macos-1024.png and b/assets/prod/black-macos-1024.png differ diff --git a/assets/prod/black-universal-1024.png b/assets/prod/black-universal-1024.png index bfc80410aadc5..f5e15607d7cf1 100644 Binary files a/assets/prod/black-universal-1024.png and b/assets/prod/black-universal-1024.png differ diff --git a/assets/prod/logo.svg b/assets/prod/logo.svg deleted file mode 100644 index 61b48db5af1a5..0000000000000 --- a/assets/prod/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/prod/t3-black-web-apple-touch-180.png b/assets/prod/t3-black-web-apple-touch-180.png index 327a8ac55fafa..761c5949d68e5 100644 Binary files a/assets/prod/t3-black-web-apple-touch-180.png and b/assets/prod/t3-black-web-apple-touch-180.png differ diff --git a/assets/prod/t3-black-web-favicon-16x16.png b/assets/prod/t3-black-web-favicon-16x16.png index 6ba6d92c03791..427bbe125ea8a 100644 Binary files a/assets/prod/t3-black-web-favicon-16x16.png and b/assets/prod/t3-black-web-favicon-16x16.png differ diff --git a/assets/prod/t3-black-web-favicon-32x32.png b/assets/prod/t3-black-web-favicon-32x32.png index fb354d60ace97..6eff5df10a9b5 100644 Binary files a/assets/prod/t3-black-web-favicon-32x32.png and b/assets/prod/t3-black-web-favicon-32x32.png differ diff --git a/assets/prod/t3-black-web-favicon.ico b/assets/prod/t3-black-web-favicon.ico index 5fac0d8b24ea6..9e249762a4ff2 100644 Binary files a/assets/prod/t3-black-web-favicon.ico and b/assets/prod/t3-black-web-favicon.ico differ diff --git a/assets/prod/t3-black-web-icon-192x192.png b/assets/prod/t3-black-web-icon-192x192.png index aa7944053b5d8..fe58dbc644de8 100644 Binary files a/assets/prod/t3-black-web-icon-192x192.png and b/assets/prod/t3-black-web-icon-192x192.png differ diff --git a/assets/prod/t3-black-web-icon-512x512.png b/assets/prod/t3-black-web-icon-512x512.png index 597cfcc592370..8495d609e91f9 100644 Binary files a/assets/prod/t3-black-web-icon-512x512.png and b/assets/prod/t3-black-web-icon-512x512.png differ diff --git a/assets/prod/t3-black-windows.ico b/assets/prod/t3-black-windows.ico index 5fac0d8b24ea6..9e249762a4ff2 100644 Binary files a/assets/prod/t3-black-windows.ico and b/assets/prod/t3-black-windows.ico differ diff --git a/docs/user/background-service.md b/docs/user/background-service.md index b0bdaf726824f..0971a35bda176 100644 --- a/docs/user/background-service.md +++ b/docs/user/background-service.md @@ -40,7 +40,7 @@ installing one. `ada uninstall` removes the background service, the `ada` launcher, and the downloaded versions after showing you the list and asking once. Your projects, threads, and settings under `~/.t3/userdata` are kept, along with `service.env` -under T3 home. Pass `--yes` from a script. +under Ada home. Pass `--yes` from a script. ## Local network access @@ -59,7 +59,7 @@ network. `ada status` says so, and `ada up --no-lan` turns it off. Any ## Configure the service environment -Put custom environment variables in `service.env` under the service's T3 home +Put custom environment variables in `service.env` under the service's Ada home directory, normally `~/.t3/service.env`. This file survives service installation and updates. For example: @@ -79,7 +79,7 @@ For a service installed before env-file support, run `ada service install` once with an up-to-date CLI. A missing file is ignored; other read failures stop service startup. If the file contains credentials, restrict access to that file with `chmod 600` -(for the default T3 home, `chmod 600 ~/.t3/service.env`). +(for the default Ada home, `chmod 600 ~/.t3/service.env`). ### Ada webhooks without the desktop app diff --git a/docs/user/faq.md b/docs/user/faq.md index 68bca6447c233..d96a64001b899 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -59,7 +59,7 @@ No. Ada is a serious developer tool. Locally runnable models are not capable of ## Does my code go to Ada servers? -No. Your code and prompts go from your machine to your provider (Anthropic, OpenAI, etc.), not to us. Your devices connect straight to your machine over your LAN or Tailscale. Ada does send basic product usage events (provider, model, token totals, that kind of thing). No prompts, no file contents. Set `T3CODE_TELEMETRY_ENABLED=false` if you want it off. [Product usage data](./telemetry.md) +No. Your code and prompts go from your machine to your provider (Anthropic, OpenAI, etc.), not to us. Your devices connect straight to your machine over your LAN or Tailscale. Ada doesn't collect usage data unless you turn it on with `T3CODE_TELEMETRY_ENABLED=true`. [Product usage data](./telemetry.md) ## Worktrees are eating my disk. How do I clean them up? diff --git a/docs/user/mobile-notifications.md b/docs/user/mobile-notifications.md index b7c15f79da510..322e1c8dfa839 100644 --- a/docs/user/mobile-notifications.md +++ b/docs/user/mobile-notifications.md @@ -1,5 +1,5 @@ # Mobile notifications -Push notifications, Live Activities, Android Live Updates, and the **Agent Activity** widget are not available. They were delivered through T3 Connect, which has been removed, and a direct or Tailscale connection cannot wake a phone. +Push notifications, Live Activities, Android Live Updates, and the **Agent Activity** widget are not available in Ada. A direct or Tailscale connection cannot wake a phone. To see what your agents are doing, open Ada on your phone. It connects to your environments and shows each thread's current state, including work that finished, failed, or is waiting for your approval or input. diff --git a/docs/user/outside-agents.md b/docs/user/outside-agents.md index 04eb257de7b5e..c9125663927df 100644 --- a/docs/user/outside-agents.md +++ b/docs/user/outside-agents.md @@ -125,7 +125,7 @@ On the machine that runs the environment, `ada thread` uses the same tools without a sign-in: ```sh -id=$(t3 thread start . "Find why the tests are slow") +id=$(ada thread start . "Find why the tests are slow") ada thread send "$id" "Now fix it" --wait ada thread show "$id" ada thread list diff --git a/docs/user/project-settings.md b/docs/user/project-settings.md index 74997d917fcd4..4142a82aea29c 100644 --- a/docs/user/project-settings.md +++ b/docs/user/project-settings.md @@ -237,7 +237,7 @@ releases and the `ada` package on npm don't include `watch`, so if `ada watch` reports an unknown command, that `ada` is too old. - **Desktop app:** the app keeps its own launcher at `~/.t3/bin/t3`, or - `%USERPROFILE%\.t3\bin\t3.cmd` on Windows (under your T3 home if you changed + `%USERPROFILE%\.t3\bin\t3.cmd` on Windows (under your Ada home if you changed it). [The `ada` command](./install.md#the-t3-command) explains how to put it on your `PATH`. - **Server without the desktop app:** use the standalone `ada` CLI built with @@ -301,7 +301,7 @@ applies when the project and environment are both on **Inherit**. ## Worktree location -New worktrees go in the `worktrees` folder of the T3 home directory. To put them somewhere else, +New worktrees go in the `worktrees` folder of the Ada home directory. To put them somewhere else, such as another drive, set **Settings → Worktrees → Worktree location** to an absolute path like `D:\worktrees` or `~/worktrees`. The setting is per machine. Existing worktrees stay where they are, and cleanup covers both the default folder and the custom one. @@ -310,12 +310,12 @@ are, and cleanup covers both the default folder and the custom one. Fast worktree checkout is a beta. Turn it on for each Mac in **Settings → Labs → Fast worktree checkout**. Ada then keeps a few recent checkouts of each repository in the `caches` folder of -the T3 home directory. A new worktree is cloned from one of them with APFS copy-on-write, which is +the Ada home directory. A new worktree is cloned from one of them with APFS copy-on-write, which is about twice as fast as writing every file and makes the first `git status` in it fast as well. Turning it off deletes the cache. The first worktree of a repository uses Git's normal checkout and fills the cache. It needs Git 2.42 -or newer. Git's normal checkout is also used for worktrees on a different drive from the T3 home +or newer. Git's normal checkout is also used for worktrees on a different drive from the Ada home directory, sparse checkouts, repositories whose attributes use filters such as Git LFS, and repositories whose checkout settings changed. diff --git a/docs/user/providers-acp.md b/docs/user/providers-acp.md index 9c21e3e3d329e..2702ce1576127 100644 --- a/docs/user/providers-acp.md +++ b/docs/user/providers-acp.md @@ -44,7 +44,7 @@ without expanding shell expressions. Registry agents always run on the machine that hosts your Ada server. That stays true when you connect through `app.t3.codes` or from another device. -Agents install under `tools///` inside T3 home. Ada verifies SHA-256 when the Registry entry +Agents install under `tools///` inside Ada home. Ada verifies SHA-256 when the Registry entry provides one; entries without a checksum retain the Registry's HTTPS distribution guarantee. Registry `npx` and `uvx` packages use Ada-owned npm prefixes and Python tool directories at the exact version published by the Registry. Their commands are available in a new server terminal for diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index 05ff7a680057c..5f424c6da01a7 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -221,7 +221,7 @@ directly. Every other device, and the desktop app for other environments, streams them from the host. Agents keep using them while no device is connected, and `localhost` addresses reach servers on the host. -The first tab downloads a headless Chrome, about 120 MB, into the T3 home. It +The first tab downloads a headless Chrome, about 120 MB, into the Ada home. It is the same browser [HTML renders](html-renders.md) use, so a host downloads it only once. Some Linux hosts need [setup](#browser-host-setup) before it can start. @@ -281,7 +281,7 @@ If you need to run as root, or cannot enable the sandbox on your host, set the server. This disables Chrome's sandbox. For a manual launch: ```sh -T3CODE_SERVER_BROWSER_SANDBOX=0 t3 serve +T3CODE_SERVER_BROWSER_SANDBOX=0 ada serve ``` For a systemd service, add `Environment=T3CODE_SERVER_BROWSER_SANDBOX=0` under @@ -778,7 +778,7 @@ desktop app, open **Settings → Environments** and switch off **Local environment**. Ada restarts without a local server: no local agents or terminals run, WSL backends stay off, and other devices can no longer connect to this computer. Your projects, history, and saved connections are kept, and you keep working through pairing or SSH. -If a [background service](./background-service.md) on this computer uses the same T3 home, the +If a [background service](./background-service.md) on this computer uses the same Ada home, the app still treats it as this computer: **Open in** and file reveal for its projects run here, whichever connection you reach it through. diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 24520502586b3..4d68dc35ac08c 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -99,7 +99,7 @@ export T3CODE_BITBUCKET_API_TOKEN="your-token" For a background service, put these `KEY=VALUE` entries in [`service.env`](./background-service.md#configure-the-service-environment) -under T3 home, then run `ada service restart`. Service installation and updates +under Ada home, then run `ada service restart`. Service installation and updates preserve that file. ### Azure DevOps diff --git a/docs/user/telemetry.md b/docs/user/telemetry.md index c9af3592ba1cc..ac052c5b6c6c4 100644 --- a/docs/user/telemetry.md +++ b/docs/user/telemetry.md @@ -1,6 +1,7 @@ # Product usage data -The Ada server sends product usage events to the endpoint at `ingest.t3.codes`, +Ada doesn't collect usage data unless you turn it on with `T3CODE_TELEMETRY_ENABLED=true`. +When it is on, the server sends product usage events to the endpoint at `ingest.t3.codes`, associated with a hashed account or installation identifier. To count how many installations one person uses, the server also sends hashed account identifiers: your OpenAI and Claude accounts when you are signed in, and a random identifier for each device that connects. The endpoint keeps a copy of each @@ -12,11 +13,11 @@ are sent only when they appear in a provider's public model list. Product events do not include prompts, responses, file contents, authentication tokens, conversation IDs, raw provider events, or child-agent output. -## Turn it off +## Turn it on -To disable product events from the server, set `T3CODE_TELEMETRY_ENABLED=false` in its -environment before starting it. An administrator can also turn them off in the server and desktop -app with a [managed policy](./managed-policy.md#other-capabilities). +To send product events from the server, set `T3CODE_TELEMETRY_ENABLED=true` in its +environment before starting it. An administrator can keep them off in the server and desktop +app with a [managed policy](./managed-policy.md#other-capabilities), which a variable cannot override. The desktop app reads `T3CODE_TELEMETRY_ENABLED` from your shell profile (for example `~/.zshrc`) on macOS and Linux, so export it there and restart the app. On Windows, set it as a user environment variable. diff --git a/docs/user/updating.md b/docs/user/updating.md index 56b193964d4a3..0158d82a671b3 100644 --- a/docs/user/updating.md +++ b/docs/user/updating.md @@ -117,7 +117,7 @@ update can roll back to the previous version. If the update still fails: 2. Check that you updated the server's machine, not only the device you are using. 3. For a command-line server, run `ada update ` with the exact version shown in the notice, then start it again. -Only one server can use a T3 home directory at a time. If startup reports that +Only one server can use a Ada home directory at a time. If startup reports that another server owns it, stop that server before retrying, or use a separate home directory and pair with the running server. diff --git a/docs/user/usage.md b/docs/user/usage.md index 2ba4c51af6fdc..45cf7d50b2df9 100644 --- a/docs/user/usage.md +++ b/docs/user/usage.md @@ -42,8 +42,8 @@ Use absolute paths or `~/` paths in the account's environment settings; relative depend on each project's working directory and cannot be reliably discovered by Usage. Accounts sharing a history directory count once. -The server saves its first hostname in `/usage-host-id`, so a recreated container keeps -its usage identity when T3 home is persisted. Set `T3CODE_HOST_ID` to choose the identity +The server saves its first hostname in `/usage-host-id`, so a recreated container keeps +its usage identity when Ada home is persisted. Set `T3CODE_HOST_ID` to choose the identity explicitly. When your app and server support different providers, usage totals may cover only the providers diff --git a/packages/client-runtime/src/preview/serverBrowserStream.ts b/packages/client-runtime/src/preview/serverBrowserStream.ts index 9710546bd3862..a498694567b24 100644 --- a/packages/client-runtime/src/preview/serverBrowserStream.ts +++ b/packages/client-runtime/src/preview/serverBrowserStream.ts @@ -1,4 +1,5 @@ // @effect-diagnostics globalTimers:off globalFetch:off - This browser and WebView transport runs without an Effect runtime. +import { CLI_COMMAND } from "@t3tools/shared/branding"; import { type DeviceHubAccess, withDeviceHubQuery } from "../device/hubAccess.ts"; import { PREVIEW_STREAM_HOST_SETUP_CLOSE_CODE, @@ -460,7 +461,7 @@ const decodeHostSetup = (reason: string): PreviewStreamHostSetup => { return { need: value.need, command: value.command }; } } catch {} - return { need: "sandbox", command: 'sudo env "PATH=$PATH" t3 browser setup' }; + return { need: "sandbox", command: `sudo env "PATH=$PATH" ${CLI_COMMAND} browser setup` }; }; export const previewStreamHostSetupMessage = (setup: PreviewStreamHostSetup) => diff --git a/packages/contracts/src/peerLink.ts b/packages/contracts/src/peerLink.ts index 76ab701d2cc83..81ccdd68b0699 100644 --- a/packages/contracts/src/peerLink.ts +++ b/packages/contracts/src/peerLink.ts @@ -41,7 +41,7 @@ export const PeerLinkCreateInput = Schema.Struct({ url: TrimmedNonEmptyString, /** More addresses for the same peer, tried after `url`. */ alternateUrls: Schema.optionalKey(Schema.Array(TrimmedNonEmptyString)), - /** A pairing code from the peer (`t3 pair` there, or its Connections settings). */ + /** A pairing code from the peer (`ada pair` there, or its Connections settings). */ pairingCode: TrimmedNonEmptyString, access: AuthMcpClientAccess, /** diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index c4ed2bf6ddb2e..e91cfc450ad92 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -1683,7 +1683,7 @@ export const ServerSettings = Schema.Struct({ branchNamingMode: BranchNamingMode.pipe( Schema.withDecodingDefault(Effect.succeed("static" as const)), ), - branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("t3"))), + branchNamePrefix: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed("ada"))), branchNameInstructions: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), removeAgentCreditsOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sourceControlWritingStyle: SourceControlWritingStyleSetting.pipe( diff --git a/packages/contracts/src/t3ProjectFile.ts b/packages/contracts/src/t3ProjectFile.ts index d09878344b8c8..37f43299676ab 100644 --- a/packages/contracts/src/t3ProjectFile.ts +++ b/packages/contracts/src/t3ProjectFile.ts @@ -49,7 +49,7 @@ export const T3ProjectFileScript = Schema.Struct({ stopOnSettle: Schema.optionalKey( Schema.Boolean.annotate({ description: - "When true, T3 stops this action's terminal and child processes when its thread settles.", + "When true, Ada stops this action's terminal and child processes when its thread settles.", }), ), async: Schema.optionalKey( @@ -110,7 +110,7 @@ export const T3ProjectFile = Schema.Struct({ .check(Schema.isMaxLength(T3_PROJECT_FILE_MAX_SCRIPTS)), ), }).annotate({ - title: "T3 project file", + title: "Ada project file", description: "Checked-in project configuration for Ada (t3.json at the repository root). See https://github.com/jayleaton/t3code for documentation.", }); diff --git a/packages/provider-acp/src/server/AcpSessionRuntime.ts b/packages/provider-acp/src/server/AcpSessionRuntime.ts index b1747ffba4f76..6ae7e21514f57 100644 --- a/packages/provider-acp/src/server/AcpSessionRuntime.ts +++ b/packages/provider-acp/src/server/AcpSessionRuntime.ts @@ -3,6 +3,7 @@ import * as NodeCrypto from "node:crypto"; import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; +import { APP_NAME } from "@t3tools/shared/branding"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -913,7 +914,7 @@ export function terminatePosixOwnedProcessTree(input: { discover(table); const byPid = new Map(table.map((entry) => [entry.pid, entry])); const current = input.controller.identity(process.pid); - if (current === undefined) throw fail("Cannot identify the current T3 process group"); + if (current === undefined) throw fail(`Cannot identify the current ${APP_NAME} process group`); const ledgerByPid = new Map( [...ledger.values()].map((process) => [process.pid, process] as const), ); diff --git a/packages/shared/src/cliRelease.ts b/packages/shared/src/cliRelease.ts index 780f96702a5c5..a7c9cb5b06353 100644 --- a/packages/shared/src/cliRelease.ts +++ b/packages/shared/src/cliRelease.ts @@ -8,6 +8,7 @@ import { compareSemverVersions } from "./semver.ts"; // scripts/install.sh and install.ps1 repeat it; scripts/install.test.ts keeps them in step. export const CLI_RELEASE_REPOSITORY = "jayleaton/t3code"; export const CLI_RELEASES_PAGE_URL = `https://github.com/${CLI_RELEASE_REPOSITORY}/releases`; +export const LICENSE_URL = `https://github.com/${CLI_RELEASE_REPOSITORY}/blob/main/LICENSE`; export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS"; /** The release's `t3` package manifest, listed in its checksums; read by npmReleaseDatabaseMigration. */ export const CLI_RELEASE_MANIFEST_FILE = "t3-release.json"; diff --git a/packages/shared/src/speech.ts b/packages/shared/src/speech.ts index c5ed9ab14ea0a..33df73389bbb2 100644 --- a/packages/shared/src/speech.ts +++ b/packages/shared/src/speech.ts @@ -7,12 +7,14 @@ import { } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; +import { APP_NAME } from "./branding.ts"; + const decodeOptions = Schema.decodeUnknownSync(Schema.fromJsonString(SpeechTranscriptionOptions)); const encodeOptions = Schema.encodeSync(Schema.fromJsonString(SpeechTranscriptionOptions)); export function cloudSpeechDisclosure(provider: CloudSpeechProvider, environment: string) { return provider === "openrouter" - ? `After you stop, ${environment} sends the recording to OpenRouter. OpenRouter chooses who runs your model. T3 requests zero data retention; your OpenRouter account may allow safety retention.` + ? `After you stop, ${environment} sends the recording to OpenRouter. OpenRouter chooses who runs your model. ${APP_NAME} requests zero data retention; your OpenRouter account may allow safety retention.` : `After you stop, ${environment} sends your recording and vocabulary hints, including project names, to ${CLOUD_SPEECH_PROVIDER_NAMES[provider]}.`; } diff --git a/packages/shared/src/themePalettes.ts b/packages/shared/src/themePalettes.ts index 19e321c5886c3..2134e2e3ccd9d 100644 --- a/packages/shared/src/themePalettes.ts +++ b/packages/shared/src/themePalettes.ts @@ -235,7 +235,7 @@ export const T3_CODE_DARK_THEME_COLORS: ThemeColors = { export const T3_CHAT_THEME: ThemeDefinition = { id: "t3-chat", - label: "T3 Chat", + label: "Orchid", appearance: "light", colors: { canvas: "oklch(0.982446 0.010114 325.653)", diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 0d9786c22fc3f..267652e542727 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -217,7 +217,7 @@ describe("ssh tunnel scripts", () => { assert.include(result.stderr, "download attempt"); } else { assert.notInclude(result.stderr, "download attempt"); - assert.include(result.stderr, "has no t3 release archive."); + assert.include(result.stderr, "has no Ada release archive."); } } finally { NodeFS.rmSync(root, { recursive: true, force: true }); diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 8d32e752f5d8a..2f9e13ddd55a7 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -456,11 +456,11 @@ if [ -n "$T3_NODE_SCRIPT_PATH" ]; then fi T3_ARCHIVE_VERSION=@@T3_ARCHIVE_VERSION@@ if [ -z "$T3_ARCHIVE_VERSION" ]; then - printf 'No t3 release version was provided for the remote runtime.\\n' >&2 + printf 'No ${APP_NAME} release version was provided for the remote runtime.\\n' >&2 exit 1 fi # Self-contained release archive: no Node, npm, or compiler on the remote. -# Unpacked into the pinned-runtime layout so \`t3 service install\` reuses it. +# Unpacked into the pinned-runtime layout so \`ada service install\` reuses it. T3_RELEASE_BASE_URL=@@T3_RELEASE_BASE_URL@@ T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION" t3_runtime_ready() { @@ -503,7 +503,7 @@ if ! t3_runtime_ready; then "Darwin arm64") T3_PLATFORM_KEY="darwin-arm64" ;; "Linux arm64" | "Linux aarch64") T3_PLATFORM_KEY="linux-arm64" ;; "Linux x86_64" | "Linux amd64") T3_PLATFORM_KEY="linux-x64" ;; - *) printf 'Remote host %s has no t3 release archive.\\n' "$(uname -s) $(uname -m)" >&2; exit 1 ;; + *) printf 'Remote host %s has no ${APP_NAME} release archive.\\n' "$(uname -s) $(uname -m)" >&2; exit 1 ;; esac T3_ARCHIVE="t3-$T3_ARCHIVE_VERSION-$T3_PLATFORM_KEY.tar.gz" T3_STAGING="$(mktemp -d "$HOME/.t3/runtime/versions/.staging-XXXXXX")" @@ -870,7 +870,7 @@ export class SshInvalidArchiveVersionError extends Schema.TaggedError()( "AndroidIconRenderError", { layer: Schema.String, cause: Schema.Defect() }, ) {} -const wordmarkTransform = (size: number) => { - const scale = (size * WORDMARK_FRACTION) / TEXT.width; - const tx = (size - TEXT.width * scale) / 2 - TEXT.x * scale; - const ty = (size - TEXT.height * scale) / 2 - TEXT.y * scale; - return `translate(${tx.toFixed(3)} ${ty.toFixed(3)}) scale(${scale.toFixed(4)})`; -}; - -const canvasSvg = (size: number, inner: string) => - `${inner}`; - -// Remove the iOS 10pt corner clip; Android's own mask needs full bleed. -const fullBleed = (svg: string) => - svg.replace(//, ''); - const rasterize = (layer: string, svg: string, width: number, height = width) => Effect.tryPromise({ try: () => @@ -48,162 +46,70 @@ const rasterize = (layer: string, svg: string, width: number, height = width) => catch: (cause) => new AndroidIconRenderError({ layer, cause }), }); -const composite = ( - layer: string, - base: Buffer, - overlays: ReadonlyArray<{ input: Buffer; left?: number; top?: number }>, -) => - Effect.tryPromise({ - try: () => - sharp(base) - .composite([...overlays]) - .png() - .toBuffer(), - catch: (cause) => new AndroidIconRenderError({ layer, cause }), - }); - -const solidCanvas = (layer: string, size: number, background: string) => - Effect.tryPromise({ - try: () => - sharp({ create: { width: size, height: size, channels: 4, background } }) - .png() - .toBuffer(), - catch: (cause) => new AndroidIconRenderError({ layer, cause }), - }); - -const readLayerSource = Effect.fn("androidIcons.readLayerSource")(function* ( - repositoryRoot: string, - variant: IconVariant, - file: string, -) { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - return yield* fs.readFileString( - path.join(repositoryRoot, "assets", variant, "app-icon.icon", "Assets", file), - ); -}); - -const renderForeground = Effect.fn("androidIcons.renderForeground")(function* ( - repositoryRoot: string, - size: number, -) { - const text = yield* readLayerSource(repositoryRoot, "prod", "text.svg"); - const paths = text.match(/]*\/>/g) ?? []; - return yield* rasterize( - "foreground", - canvasSvg(size, `${paths.join("")}`), - size, - ); -}); - -const renderDevelopmentBackground = Effect.fn("androidIcons.renderDevelopmentBackground")( - function* (repositoryRoot: string, size: number) { - const annotations = yield* readLayerSource(repositoryRoot, "dev", "annotations.svg"); - const defs = annotations.match(/[\s\S]*?<\/defs>/)?.[0] ?? ""; - const body = annotations.replace(/^[\s\S]*?<\/defs>/, "").replace(/<\/svg>\s*$/, ""); - const paper = yield* readLayerSource(repositoryRoot, "dev", "background.svg"); - const background = yield* rasterize("dev-background", fullBleed(paper), size); - const overlay = yield* rasterize( - "dev-annotations", - canvasSvg(size, `${defs}${body}`), - size, - ); - return yield* composite("dev-background", background, [{ input: overlay }]); - }, -); - -const renderNightlyBackground = Effect.fn("androidIcons.renderNightlyBackground")(function* ( - repositoryRoot: string, - size: number, -) { - // Match assets/nightly/app-icon.icon/icon.json; omit blur, which Icon Composer ignores and rasterization smears. - const clouds = [ - { file: "cloud-lower-left.svg", scale: 25, translation: [-309.6375, 268.66077693836917] }, - { - file: "cloud-upper-right.svg", - scale: 15, - translation: [387.9605131881942, -134.30064713259117], - }, - ] as const; - const k = size / COMPOSER_CANVAS_PT; - const overlays = yield* Effect.forEach(clouds, (cloud) => - Effect.gen(function* () { - const width = Math.round(64 * cloud.scale * k); - const height = Math.round(32 * cloud.scale * k); - const left = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[0]) * k - width / 2); - const top = Math.round((COMPOSER_CANVAS_PT / 2 + cloud.translation[1]) * k - height / 2); - const source = yield* readLayerSource(repositoryRoot, "nightly", cloud.file); - const png = yield* rasterize( - cloud.file, - source.replace(/ filter="url\(#soft\)"/, ""), - width, - height, - ); - const x0 = Math.max(0, left); - const y0 = Math.max(0, top); - const x1 = Math.min(size, left + width); - const y1 = Math.min(size, top + height); - const clipped = yield* Effect.tryPromise({ - try: () => - sharp(png) - .extract({ left: x0 - left, top: y0 - top, width: x1 - x0, height: y1 - y0 }) - .png() - .toBuffer(), - catch: (cause) => new AndroidIconRenderError({ layer: cloud.file, cause }), - }); - return { input: clipped, left: x0, top: y0 }; - }), - ); - const sky = yield* readLayerSource(repositoryRoot, "nightly", "background.svg"); - const background = yield* rasterize("nightly-background", fullBleed(sky), size); - return yield* composite("nightly-background", background, overlays); -}); - -const renderBackground = Effect.fn("androidIcons.renderBackground")(function* ( - repositoryRoot: string, - variant: IconVariant, - size: number, -) { - switch (variant) { - case "dev": - return yield* renderDevelopmentBackground(repositoryRoot, size); - case "nightly": - return yield* renderNightlyBackground(repositoryRoot, size); - case "prod": - return yield* solidCanvas("prod-background", size, PRODUCTION_BACKGROUND_COLOR); - } -}); - -const renderSplashIcon = Effect.fn("androidIcons.renderSplashIcon")(function* ( - repositoryRoot: string, - variant: IconVariant, -) { - const background = yield* renderBackground(repositoryRoot, variant, SPLASH_CANVAS); - const foreground = yield* renderForeground(repositoryRoot, SPLASH_CANVAS); - return yield* composite(`${variant}-splash`, background, [{ input: foreground }]); -}); +const canvas = (layer: string, pip: Pip, options: Parameters[1]) => + rasterize(layer, pipCanvasSvg(pip, options), options.size); const exportAndroidIcons = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const repositoryRoot = path.resolve(import.meta.dirname, ".."); - const outputs = [ - ["android-icon-foreground.png", yield* renderForeground(repositoryRoot, ADAPTIVE_CANVAS)], + const pip = parsePip(yield* fs.readFileString(path.join(repositoryRoot, PIP_SOURCE))); + const splashMarkHeight = Math.round((SPLASH_MARK_WIDTH * pip.height) / pip.width); + + const outputs: ReadonlyArray = [ + [ + `${OUTPUT_DIRECTORY}/ios-icon-1024.png`, + yield* canvas("ios-icon", pip, { + size: IOS_CANVAS, + fraction: ICON_FRACTION, + fill: PIP_COLORS.lavender, + background: PIP_COLORS.navy, + lift: PIP_ICON_LIFT, + }), + ], + [ + `${OUTPUT_DIRECTORY}/splash-mark.png`, + yield* rasterize( + "splash-mark", + pipGlyphSvg(pip, PIP_COLORS.lavender), + SPLASH_MARK_WIDTH, + splashMarkHeight, + ), + ], + [ + `${OUTPUT_DIRECTORY}/android-icon-foreground.png`, + yield* canvas("android-foreground", pip, { + size: ADAPTIVE_CANVAS, + fraction: ANDROID_FRACTION, + fill: PIP_COLORS.lavender, + }), + ], [ - "android-icon-background-dev.png", - yield* renderDevelopmentBackground(repositoryRoot, ADAPTIVE_CANVAS), + `${OUTPUT_DIRECTORY}/android-icon-monochrome.png`, + yield* canvas("android-monochrome", pip, { + size: ADAPTIVE_CANVAS, + fraction: ANDROID_FRACTION, + fill: PIP_COLORS.white, + }), ], [ - "android-icon-background-nightly.png", - yield* renderNightlyBackground(repositoryRoot, ADAPTIVE_CANVAS), + `${OUTPUT_DIRECTORY}/android-splash-icon.png`, + yield* canvas("android-splash", pip, { + size: SPLASH_CANVAS, + fraction: ANDROID_FRACTION, + fill: PIP_COLORS.lavender, + background: PIP_COLORS.navy, + }), ], - ["android-splash-icon-dev.png", yield* renderSplashIcon(repositoryRoot, "dev")], - ["android-splash-icon-nightly.png", yield* renderSplashIcon(repositoryRoot, "nightly")], - ["android-splash-icon-prod.png", yield* renderSplashIcon(repositoryRoot, "prod")], - ] as const; - for (const [name, contents] of outputs) { - yield* fs.writeFile(path.join(repositoryRoot, OUTPUT_DIRECTORY, name), contents); - yield* Console.log(`wrote ${OUTPUT_DIRECTORY}/${name}`); + // A template image: iOS tints it, so only the shape matters. + [WIDGET_MARK, pipGlyphSvg(pip, PIP_COLORS.black)], + ]; + for (const [relativePath, contents] of outputs) { + yield* fs.writeFile( + path.join(repositoryRoot, relativePath), + typeof contents === "string" ? new TextEncoder().encode(contents) : contents, + ); + yield* Console.log(`wrote ${relativePath}`); } }); diff --git a/scripts/export-brand-icons.ts b/scripts/export-brand-icons.ts index 052665b7c2983..a9e2ae6b0b781 100644 --- a/scripts/export-brand-icons.ts +++ b/scripts/export-brand-icons.ts @@ -12,9 +12,11 @@ import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { Command, Flag } from "effect/cli"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import sharp from "sharp"; import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts"; import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts"; +import { parsePip, PIP_COLORS, PIP_SOURCE, pipGlyphSvg } from "./lib/pip.ts"; const DESIGN_GENERATION = 26; const ICON_COMPOSER_EXECUTABLE_PARTS = [ @@ -255,6 +257,57 @@ const ICON_VARIANTS = [ }, ] as const satisfies ReadonlyArray; +// Each Icon Composer project draws Pip in its own colour over its own backdrop. +const ICON_COMPOSER_GLYPHS = [ + { project: BRAND_ASSET_PATHS.productionIconComposerProject, fill: PIP_COLORS.lavender }, + { project: BRAND_ASSET_PATHS.nightlyIconComposerProject, fill: PIP_COLORS.white }, + { project: BRAND_ASSET_PATHS.developmentIconComposerProject, fill: PIP_COLORS.white }, +] as const; + +// Byte copies of rendered assets. The fork ships the production render as its agents brand. +const GENERATED_COPIES = [ + [BRAND_ASSET_PATHS.productionMacIconPng, BRAND_ASSET_PATHS.agentsDesktopIconPng], + [BRAND_ASSET_PATHS.productionWindowsIconIco, BRAND_ASSET_PATHS.agentsWindowsIconIco], + [BRAND_ASSET_PATHS.productionWebFaviconIco, BRAND_ASSET_PATHS.agentsWebFaviconIco], + [BRAND_ASSET_PATHS.productionWebFavicon16Png, BRAND_ASSET_PATHS.agentsWebFavicon16Png], + [BRAND_ASSET_PATHS.productionWebFavicon32Png, BRAND_ASSET_PATHS.agentsWebFavicon32Png], + [BRAND_ASSET_PATHS.productionWebAppleTouchIconPng, BRAND_ASSET_PATHS.agentsWebAppleTouchIconPng], + [BRAND_ASSET_PATHS.productionWebIcon192Png, BRAND_ASSET_PATHS.agentsWebIcon192Png], + [BRAND_ASSET_PATHS.productionWebIcon512Png, BRAND_ASSET_PATHS.agentsWebIcon512Png], + [BRAND_ASSET_PATHS.productionWebAppleTouchIconPng, "apps/web/public/brand-mark.png"], + [BRAND_ASSET_PATHS.productionWebFaviconIco, "apps/marketing/public/favicon.ico"], + [BRAND_ASSET_PATHS.productionWebFavicon16Png, "apps/marketing/public/favicon-16x16.png"], + [BRAND_ASSET_PATHS.productionWebFavicon32Png, "apps/marketing/public/favicon-32x32.png"], + [BRAND_ASSET_PATHS.productionWebAppleTouchIconPng, "apps/marketing/public/apple-touch-icon.png"], +] as const; + +const MARKETING_ICONS = [ + [BRAND_ASSET_PATHS.productionIosIconPng, "apps/marketing/src/assets/icon.webp"], + [BRAND_ASSET_PATHS.nightlyIosIconPng, "apps/marketing/src/assets/icon-nightly.webp"], +] as const; +const SOCIAL_CARD = "apps/marketing/src/assets/social-card.webp"; + +const SOCIAL_CARD_LOCKUP = "assets/agents/pip-lockup-dark.svg"; + +// Open Graph card at 2x: the approved dark lockup on its own navy. The slogan uses system fonts, +// like the rest of this macOS-only export. +const socialCardSvg = (lockup: string) => { + const [width, height] = [2400, 1260]; + const lockupWidth = 2240; + const lockupHeight = (lockupWidth * 560) / 1400; + const nested = lockup.replace( + /]*>/, + ``, + ); + return `${nested}`; +}; + +const encodeWith = (label: string, encode: () => Promise) => + Effect.tryPromise({ + try: encode, + catch: (cause) => new IconExportEncodingError({ variant: label, cause }), + }); + const RepositoryRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), Effect.mapError( @@ -596,8 +649,24 @@ const renderVariant = Effect.fn("iconExport.renderVariant")(function* ( catch: (cause) => new IconExportEncodingError({ variant: variant.label, cause }), }); + // Icon Composer renders macOS full-bleed, so the Dock's margin comes from Apple's 824-on-1024 grid. + const macos = yield* encodeWith(`${variant.label}-macos`, async () => + sharp({ + create: { + width: 1024, + height: 1024, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }, + }) + .composite([{ input: await sharp(ios).resize(824, 824).png().toBuffer(), gravity: "centre" }]) + .png() + .toBuffer(), + ); + return new Map([ [variant.outputs.ios, ios], + [variant.outputs.macos, macos], [variant.outputs.universal, ios], [variant.outputs.appleTouch, yield* render("iOS", 180)], [variant.outputs.webIcon192, yield* render("iOS", 192)], @@ -722,6 +791,26 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn ); const generated = new Map(); + const path = yield* Path.Path; + const pipSource = path.join(repositoryRoot, PIP_SOURCE); + const pip = parsePip( + yield* fs + .readFileString(pipSource) + .pipe( + Effect.mapError( + (cause) => + new IconExportFileSystemError({ operation: "read-file", path: pipSource, cause }), + ), + ), + ); + for (const { project, fill } of ICON_COMPOSER_GLYPHS) { + const glyphPath = `${project}/Assets/pip.svg`; + const glyph = Buffer.from(pipGlyphSvg(pip, fill)); + generated.set(glyphPath, glyph); + // Icon Composer reads the glyph from disk, so it must be current before rendering. + if (!checkOnly) yield* writeAtomically(repositoryRoot, glyphPath, glyph); + } + for (const variant of ICON_VARIANTS) { yield* Console.log(`Rendering ${variant.label} from ${variant.source}...`); const variantAssets = yield* renderVariant( @@ -735,15 +824,46 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn } } - for (const override of DEVELOPMENT_PUBLIC_ICON_OVERRIDES) { - const sourceContents = generated.get(override.sourceRelativePath); + const copies = [ + ...DEVELOPMENT_PUBLIC_ICON_OVERRIDES.map( + (override) => [override.sourceRelativePath, override.targetRelativePath] as const, + ), + ...GENERATED_COPIES, + ]; + for (const [source, target] of copies) { + const sourceContents = generated.get(source); + if (sourceContents === undefined) { + return yield* Effect.die(new Error(`Generated icon is missing: ${source}`)); + } + generated.set(target, sourceContents); + } + for (const [source, target] of MARKETING_ICONS) { + const sourceContents = generated.get(source); if (sourceContents === undefined) { - return yield* Effect.die( - new Error(`Generated development web icon is missing: ${override.sourceRelativePath}`), - ); + return yield* Effect.die(new Error(`Generated icon is missing: ${source}`)); } - generated.set(override.targetRelativePath, sourceContents); + generated.set( + target, + yield* encodeWith(target, () => sharp(sourceContents).webp({ quality: 90 }).toBuffer()), + ); } + const lockupSource = path.join(repositoryRoot, SOCIAL_CARD_LOCKUP); + const lockup = yield* fs + .readFileString(lockupSource) + .pipe( + Effect.mapError( + (cause) => + new IconExportFileSystemError({ operation: "read-file", path: lockupSource, cause }), + ), + ); + generated.set( + SOCIAL_CARD, + yield* encodeWith(SOCIAL_CARD, () => + sharp(Buffer.from(socialCardSvg(lockup))) + .webp({ quality: 90 }) + .toBuffer(), + ), + ); if (checkOnly) { const stale = yield* Effect.filter( diff --git a/scripts/fork-guards.json b/scripts/fork-guards.json index 0745d04cce40c..8ba257a83967e 100644 --- a/scripts/fork-guards.json +++ b/scripts/fork-guards.json @@ -246,7 +246,7 @@ "protects": "the fork ships as Ada and upstream syncs cannot bring T3 Code, T3 Agents, Theo, t3.codes or pingdotgg back into user-facing copy", "test": "scripts/upstream-install-commands.test.ts", "title": "keeps upstream brand names out of user-facing fork code and docs", - "sha256": "85d9b5d8a33ac522361b474782651de8799697cb0302250dba43220b233ef46c", + "sha256": "b4783358437d945c2851a094712cc45dfa318da65cfe64d5400a7dc74b11f23c", "sources": ["packages/shared/src/branding.ts"] }, { diff --git a/scripts/lib/brand-assets.ts b/scripts/lib/brand-assets.ts index b3175d4ea89ca..adf68194aa024 100644 --- a/scripts/lib/brand-assets.ts +++ b/scripts/lib/brand-assets.ts @@ -1,6 +1,12 @@ export const BRAND_ASSET_PATHS = { agentsDesktopIconPng: "assets/agents/icon.png", agentsWindowsIconIco: "assets/agents/windows.ico", + agentsWebFaviconIco: "assets/agents/favicon.ico", + agentsWebFavicon16Png: "assets/agents/favicon-16x16.png", + agentsWebFavicon32Png: "assets/agents/favicon-32x32.png", + agentsWebAppleTouchIconPng: "assets/agents/apple-touch-icon.png", + agentsWebIcon192Png: "assets/agents/icon-192x192.png", + agentsWebIcon512Png: "assets/agents/icon-512x512.png", developmentIconComposerProject: "assets/dev/app-icon.icon", developmentIosIconPng: "assets/dev/blueprint-ios-1024.png", @@ -66,12 +72,12 @@ const WEB_ICON_TARGET_FILENAMES = { const WEB_ICON_SOURCE_PATHS_BY_BRAND = { agents: { - faviconIco: "assets/agents/favicon.ico", - favicon16Png: "assets/agents/favicon-16x16.png", - favicon32Png: "assets/agents/favicon-32x32.png", - appleTouchIconPng: "assets/agents/apple-touch-icon.png", - icon192Png: "assets/agents/icon-192x192.png", - icon512Png: "assets/agents/icon-512x512.png", + faviconIco: BRAND_ASSET_PATHS.agentsWebFaviconIco, + favicon16Png: BRAND_ASSET_PATHS.agentsWebFavicon16Png, + favicon32Png: BRAND_ASSET_PATHS.agentsWebFavicon32Png, + appleTouchIconPng: BRAND_ASSET_PATHS.agentsWebAppleTouchIconPng, + icon192Png: BRAND_ASSET_PATHS.agentsWebIcon192Png, + icon512Png: BRAND_ASSET_PATHS.agentsWebIcon512Png, }, development: { faviconIco: BRAND_ASSET_PATHS.developmentWebFaviconIco, diff --git a/scripts/lib/pip.ts b/scripts/lib/pip.ts new file mode 100644 index 0000000000000..ef8d2bbf0eedf --- /dev/null +++ b/scripts/lib/pip.ts @@ -0,0 +1,63 @@ +/** Pip, the fork's mark. assets/agents/pip.svg is the only hand-edited source; every brand asset is generated from it. */ +export const PIP_SOURCE = "assets/agents/pip.svg"; + +export const PIP_COLORS = { + navy: "#111925", + lavender: "#C5A2FF", + white: "#FFFFFF", + black: "#000000", +} as const; + +export interface Pip { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + readonly d: string; +} + +export function parsePip(svg: string): Pip { + const viewBox = svg + .match(/viewBox="([^"]+)"/)?.[1] + ?.split(/\s+/) + .map(Number); + const d = svg.match(/]*\sd="([^"]+)"/)?.[1]; + if (viewBox?.length !== 4 || viewBox.some(Number.isNaN) || d === undefined) { + throw new Error(`${PIP_SOURCE} needs a numeric viewBox and one .`); + } + const [x, y, width, height] = viewBox as [number, number, number, number]; + return { x, y, width, height, d }; +} + +const path = (pip: Pip, fill: string) => ``; + +/** The glyph alone, cropped to its own bounds. */ +export function pipGlyphSvg(pip: Pip, fill: string): string { + return `${path(pip, fill)}\n`; +} + +/** + * The glyph centred on a square canvas, `fraction` of its width wide. `lift` raises it by that + * fraction of the canvas, matching the Icon Composer layer offset so every icon sits alike. + */ +export function pipCanvasSvg( + pip: Pip, + options: { + readonly size: number; + readonly fraction: number; + readonly fill: string; + readonly background?: string; + readonly lift?: number; + }, +): string { + const { size, fraction, fill, background, lift = 0 } = options; + const scale = (size * fraction) / pip.width; + const tx = (size - pip.width * scale) / 2 - pip.x * scale; + const ty = (size - pip.height * scale) / 2 - pip.y * scale - lift * size; + const backdrop = + background === undefined ? "" : ``; + return `${backdrop}${path(pip, fill)}`; +} + +/** Icon Composer layer offset (points on its 1024pt canvas) that lifts Pip to its optical centre. */ +export const PIP_ICON_LIFT = 24 / 1024; diff --git a/scripts/mobile-showcase-environment.ts b/scripts/mobile-showcase-environment.ts index 16e013e3b1da4..0a3ab1e33ac4c 100644 --- a/scripts/mobile-showcase-environment.ts +++ b/scripts/mobile-showcase-environment.ts @@ -77,8 +77,8 @@ export function RemoteHandoffCard(props: { machine: string; latencyMs: number }) const PROJECT_FAVICONS = { t3code: ` - - + + `, react: ` diff --git a/scripts/upstream-install-commands.test.ts b/scripts/upstream-install-commands.test.ts index b530f0921bbb4..64be051043cbd 100644 --- a/scripts/upstream-install-commands.test.ts +++ b/scripts/upstream-install-commands.test.ts @@ -39,6 +39,8 @@ it("keeps upstream install commands out of user-facing fork code and docs", () = // The fork ships as Ada (packages/shared/src/branding.ts). Upstream syncs bring these back. const UPSTREAM_BRAND = /T3 Code|T3 Agents|\bTheo\b|t3\.codes|pingdotgg/; +// Upstream product phrases ("T3 Connect", "T3 home") and `t3 ` hints, which the names above miss. +const UPSTREAM_PHRASE = /\bT3 [A-Za-z]|(? = [ file: /^apps\/marketing\/src\/pages\/\.well-known\/security\.txt\.ts$/, why: "Security contact belongs with the security policy.", }, - { file: /^apps\/web\/src\/legalLinks\.ts$/, why: "Privacy policy link." }, { - file: /^apps\/mobile\/src\/features\/settings\/lib\/legal-document-url\.ts$/, - why: "Legal documents host.", + file: /^(?:apps\/desktop\/resources\/linux\/com\.t3tools\.t3code\.metainfo\.xml|apps\/marketing\/src\/components\/LegalPage\.astro|scripts\/build-desktop-artifact\.ts)$/, + text: "T3 Tools", + why: "Upstream's company, the copyright holder in LICENSE.", }, // Identities other systems already know: providers, git history, peers, installed helpers. { @@ -183,6 +185,69 @@ const BRAND_ALLOWLIST: ReadonlyArray = [ text: "Written by T3 Code", why: "Launcher marker that older desktop builds look for to recognise their own launchers.", }, + { + file: /^apps\/server\/src\/provider\/AgentInstructionFiles\.ts$/, + text: "T3 managed instructions", + why: "Marker in instruction files already written to users' projects.", + }, + { + file: /^packages\/mcp-gateway\/src\/skillImport\.ts$/, + text: "T3 syncs this text", + why: "Recognises a section older versions wrote into imported skills.", + }, + { + file: /^packages\/client-runtime\/src\/errors\/transport\.ts$/, + text: "T3 server WebSocket", + why: "Matches an error older servers send.", + }, + // The `t3` command still exists beside `ada`, so copy that names it on purpose stays. + ...["Another t3 ", "another t3", "t3 command", "t3 launcher", "t3 triage"].map((text) => ({ + file: /^apps\/(?:desktop\/src\/app\/(?:DesktopCliCommand|DesktopCliShim|DesktopApp)\.ts|web\/src\/(?:components\/settings\/CliCommandSettingsRow\.tsx|lib\/bootError\.ts))$/, + text, + why: "The desktop links `t3` and puts `ada` beside it as a best-effort alias, so these name the link it found or wrote.", + })), + { + file: /^scripts\/install\.(?:sh|ps1)$/, + text: "ada and t3", + why: "The installer sets up both commands.", + }, + { + file: /^(?:packages\/shared\/src\/cliRelease\.ts|apps\/web\/src\/components\/sidebar\/updateEverything\.logic\.ts|docs\/user\/updating\.md)$/, + text: "t3 update", + why: "The copied update command runs on hosts older than `ada`.", + }, + ...["t3 --version", "which -a t3", "older t3", "/t3 service install", "t3 service update"].map( + (text) => ({ + file: /^apps\/server\/src\/service\/servicePreflight\.ts$/, + text, + why: "Diagnoses a stale `t3` from an npm or nvm install shadowing the installed one.", + }), + ), + { + file: /^apps\/server\/src\/cli\/uninstall\.ts$/, + text: "This t3 runs", + why: "Upstream's npm package, which this message says how to remove.", + }, + { + file: /^README\.md$/, + text: "t3 service uninstall", + why: "T3 Code's own command for its background service.", + }, + { + file: /^(?:README\.md|docs\/user\/remote-access\.md|apps\/(?:web|mobile)\/src\/(?:components|features\/connection)\/ConnectOnlyNoticeCoordinator\.tsx|apps\/(?:web|mobile)\/src\/connection\/storage\.ts|apps\/server\/src\/environment\/ConnectTeardown\.ts)$/, + text: "T3 Connect", + why: "The removed upstream service, named so the people who used it know what changed.", + }, + { + file: /^docs\/user\/outside-agents\.md$/, + text: " t3 ", + why: "The MCP server name readers give the agent; renaming it renames every tool they call.", + }, + { + file: /^docs\/user\/cursor\.md$/, + text: "--filter t3", + why: "The workspace package name.", + }, ]; /** Comment lines are for maintainers, not users. Markdown has no comments worth skipping. */ @@ -208,7 +273,9 @@ it("keeps upstream brand names out of user-facing fork code and docs", () => { if (entry.text === undefined) return []; visible = visible.replaceAll(entry.text, ""); } - return UPSTREAM_BRAND.test(visible) ? [`${file}:${index + 1}`] : []; + return UPSTREAM_BRAND.test(visible) || UPSTREAM_PHRASE.test(visible) + ? [`${file}:${index + 1}`] + : []; }), ); expect(hits).toEqual([]);