From f2aa0f22292fdec9f21a54058024ec35d63f2206 Mon Sep 17 00:00:00 2001 From: TB516 <77514919+TB516@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:07:30 +0000 Subject: [PATCH 1/9] chore(flatpak): update T3 Code to v0.1.0 --- flatpak/com.t3tools.t3code.metainfo.xml | 4 ++++ flatpak/modules/t3code.yml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flatpak/com.t3tools.t3code.metainfo.xml b/flatpak/com.t3tools.t3code.metainfo.xml index 39d1da6..a9c106c 100644 --- a/flatpak/com.t3tools.t3code.metainfo.xml +++ b/flatpak/com.t3tools.t3code.metainfo.xml @@ -13,6 +13,10 @@ com.t3tools.t3code.desktop https://t3.codes + + https://github.com/pingdotgg/t3code/releases/tag/v0.1.0 + + https://github.com/pingdotgg/t3code/releases/tag/v0.0.45 diff --git a/flatpak/modules/t3code.yml b/flatpak/modules/t3code.yml index 158df89..460788e 100644 --- a/flatpak/modules/t3code.yml +++ b/flatpak/modules/t3code.yml @@ -50,8 +50,8 @@ test-commands: sources: - type: git url: https://github.com/pingdotgg/t3code.git - tag: v0.0.45 - commit: 6c8fed35dded9ff71c5b46807125457acbb76be6 + tag: v0.1.0 + commit: 81493efb71e8e8244f95d3e9f71d8369654ab151 x-checker-data: type: json url: https://api.github.com/repos/pingdotgg/t3code/releases/latest From dd7a55e1f27a7bcb95b7fcca0c85a072c122eb89 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 15 Oct 2026 21:10:13 -0400 Subject: [PATCH 2/9] Adapt Flatpak integration to T3 Code v0.1.0 --- flatpak/modules/t3code.yml | 1 + flatpak/package/t3code | 2 +- .../0001-flatpak-desktop-integration.patch | 44 +++++++++---------- patches/0002-flatpak-host-backend.patch | 34 +++++++++----- patches/0003-flatpak-host-environment.patch | 24 +++++----- .../0006-flatpak-document-portal-paths.patch | 8 ++-- patches/0007-flatpak-editor-discovery.patch | 14 +++--- patches/0008-flatpak-cli-launcher.patch | 31 +++++++++++++ 8 files changed, 102 insertions(+), 56 deletions(-) create mode 100644 patches/0008-flatpak-cli-launcher.patch diff --git a/flatpak/modules/t3code.yml b/flatpak/modules/t3code.yml index 460788e..5154a61 100644 --- a/flatpak/modules/t3code.yml +++ b/flatpak/modules/t3code.yml @@ -68,6 +68,7 @@ sources: - ../../patches/0005-flatpak-build-artifact.patch - ../../patches/0006-flatpak-document-portal-paths.patch - ../../patches/0007-flatpak-editor-discovery.patch + - ../../patches/0008-flatpak-cli-launcher.patch use-git: true - type: dir path: ../package diff --git a/flatpak/package/t3code b/flatpak/package/t3code index b32b7c1..5f98666 100755 --- a/flatpak/package/t3code +++ b/flatpak/package/t3code @@ -25,7 +25,7 @@ esac # then pass only PATH back to the host server through flatpak-spawn. host_path=$( /usr/bin/flatpak-spawn --host "${SHELL:-/bin/sh}" -ilc 'printf %s "$PATH"' \ - 3>&- 4>&- 5>&- 2>/dev/null || true + 3>&- 4>&- 5>&- 6>&- 7>&- 2>/dev/null || true ) if [ -n "$host_path" ]; then export T3CODE_FLATPAK_HOST_PATH="$host_path" diff --git a/patches/0001-flatpak-desktop-integration.patch b/patches/0001-flatpak-desktop-integration.patch index 23f4b5a..f2f9dc6 100644 --- a/patches/0001-flatpak-desktop-integration.patch +++ b/patches/0001-flatpak-desktop-integration.patch @@ -1,5 +1,5 @@ diff --git a/apps/desktop/src/app/DesktopConfig.ts b/apps/desktop/src/app/DesktopConfig.ts -index 773ea245..8eb05c26 100644 +index 773ea2450..8eb05c265 100644 --- a/apps/desktop/src/app/DesktopConfig.ts +++ b/apps/desktop/src/app/DesktopConfig.ts @@ -37,6 +37,7 @@ export const DesktopConfig = Config.all({ @@ -11,7 +11,7 @@ index 773ea245..8eb05c26 100644 devServerUrl: Config.URL("VITE_DEV_SERVER_URL").pipe(Config.option), appUserModelIdOverride: trimmedString("T3CODE_DESKTOP_APP_USER_MODEL_ID"), diff --git a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts -index 95922bdf..a77901ce 100644 +index c4adf8f3c..06a5cb46d 100644 --- a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts +++ b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts @@ -1,3 +1,5 @@ @@ -20,7 +20,7 @@ index 95922bdf..a77901ce 100644 import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -@@ -86,10 +88,11 @@ export function resolveEarlyLinuxElectronOptions( +@@ -158,10 +160,11 @@ export function resolveEarlyLinuxElectronOptions( ): EarlyLinuxElectronOptions { const preference = resolveEarlyLinuxPasswordStorePreference(input); const isDevelopment = isDevelopmentEnvironment(input.env); @@ -35,11 +35,11 @@ index 95922bdf..a77901ce 100644 preference, env: input.env, diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts -index b5aabf25..1ddedc22 100644 +index fb6c5a8b1..b8be6acf4 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts -@@ -40,6 +40,18 @@ const makeEnvironment = ( - DesktopEnvironment.DesktopEnvironment.pipe(Effect.provide(makeEnvironmentLayer(overrides, env))); +@@ -39,6 +39,18 @@ const makeEnvironment = ( + ) => DesktopEnvironment.DesktopEnvironment.pipe(Effect.provide(layerEnvironment(overrides, env))); describe("DesktopEnvironment", () => { + it.effect("uses a distinct Flatpak display name", () => @@ -58,10 +58,10 @@ index b5aabf25..1ddedc22 100644 Effect.gen(function* () { const environment = yield* makeEnvironment( diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts -index beb09bd5..17053c3c 100644 +index f697e0456..bb0a28ece 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts -@@ -177,10 +177,14 @@ const make = Effect.fn("desktop.environment.make")(function* ( +@@ -175,10 +175,14 @@ const make = Effect.fn("desktop.environment.make")(function* ( input.isPackaged && input.platform === "win32" ? path.join(input.resourcesPath, "server.asar") : appRoot; @@ -77,7 +77,7 @@ index beb09bd5..17053c3c 100644 const displayName = branding.displayName; const stateDir = resolveDesktopStateDir({ baseDir, -@@ -242,8 +246,10 @@ const make = Effect.fn("desktop.environment.make")(function* ( +@@ -238,8 +242,10 @@ const make = Effect.fn("desktop.environment.make")(function* ( appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () => isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code", ), @@ -89,9 +89,9 @@ index beb09bd5..17053c3c 100644 + linuxWmClass: flatpakId ?? (isDevelopment ? "t3code-dev" : "t3code"), linuxApplicationsDir, appImagePath: config.appImagePath, - userDataDirName, + defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion), diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts -index 893c1aae..9eb024a2 100644 +index cbabe14dd..209853d37 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts @@ -1,4 +1,4 @@ @@ -117,10 +117,10 @@ index 893c1aae..9eb024a2 100644 const entry = DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({ displayName: "T3 Code (Nightly)", diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts -index f01e81d8..9dcf500e 100644 +index e67cdcefe..f61264212 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.ts -@@ -9,6 +9,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne +@@ -9,6 +9,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; import * as DesktopAssets from "./DesktopAssets.ts"; @@ -141,10 +141,10 @@ index f01e81d8..9dcf500e 100644 } yield* writeDesktopEntry; diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts -index 7e859aaf..c13bd25f 100644 +index dd15f879f..08731decb 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.test.ts -@@ -52,6 +52,28 @@ vi.mock("node:fs", () => ({ +@@ -58,6 +58,28 @@ vi.mock("node:child_process", () => ({ import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts"; describe("DesktopPreReadyPlatform", () => { @@ -163,7 +163,7 @@ index 7e859aaf..c13bd25f 100644 + }).pipe( + Effect.provide( + DesktopPreReadyPlatform.layer.pipe( -+ Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), ++ Layer.provide(Layer.succeed(HostProcess.Platform, "linux")), + ), + ), + Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())), @@ -174,25 +174,25 @@ index 7e859aaf..c13bd25f 100644 appendSwitchMock.mockReset(); getSwitchValueMock.mockReset(); diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts -index 9b73a5d8..321fc028 100644 +index b021ae02e..b434b2a77 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -9,6 +9,7 @@ import * as Layer from "effect/Layer"; import * as Electron from "electron"; - import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; + import * as HostProcess from "@t3tools/shared/HostProcess"; +import { flatpakAppId } from "./Flatpak.ts"; import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts"; import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts"; import { renderUrlHandlerDesktopEntry } from "./DesktopLinuxUrlHandler.ts"; -@@ -61,50 +62,52 @@ export const make = Effect.gen(function* () { +@@ -64,50 +65,52 @@ export const make = Effect.gen(function* () { if (linux !== null) { // The portal also requires a valid desktop entry. An AppImage update may // have removed the executable referenced by the previous launch's entry. - try { - const applicationsDir = NodePath.posix.join( - process.env.XDG_DATA_HOME?.trim() || -- NodePath.posix.join(NodeOS.homedir(), ".local", "share"), +- NodePath.posix.join(homeDirectory, ".local", "share"), - "applications", - ); - NodeFS.mkdirSync(applicationsDir, { recursive: true }); @@ -220,7 +220,7 @@ index 9b73a5d8..321fc028 100644 + try { + const applicationsDir = NodePath.posix.join( + process.env.XDG_DATA_HOME?.trim() || -+ NodePath.posix.join(NodeOS.homedir(), ".local", "share"), ++ NodePath.posix.join(homeDirectory, ".local", "share"), + "applications", + ); + NodeFS.mkdirSync(applicationsDir, { recursive: true }); @@ -282,7 +282,7 @@ index 9b73a5d8..321fc028 100644 // before any asynchronous work can initialize it with Electron's default. diff --git a/apps/desktop/src/app/Flatpak.ts b/apps/desktop/src/app/Flatpak.ts new file mode 100644 -index 00000000..006f94e0 +index 000000000..006f94e00 --- /dev/null +++ b/apps/desktop/src/app/Flatpak.ts @@ -0,0 +1,5 @@ diff --git a/patches/0002-flatpak-host-backend.patch b/patches/0002-flatpak-host-backend.patch index 01189df..bc10a23 100644 --- a/patches/0002-flatpak-host-backend.patch +++ b/patches/0002-flatpak-host-backend.patch @@ -1,5 +1,5 @@ diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts -index c2012299..4edb253f 100644 +index b306975be..7f70a3cfc 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -1,5 +1,8 @@ @@ -9,9 +9,9 @@ index c2012299..4edb253f 100644 +import { flatpakBackendConfig } from "./flatpakBackend.ts"; + import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; - import * as Context from "effect/Context"; - import * as Crypto from "effect/Crypto"; -@@ -567,7 +570,7 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv + import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken"; + import * as Clock from "effect/Clock"; +@@ -584,7 +587,7 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv ...buildObservabilityFragment(input.observabilitySettings), }; @@ -20,7 +20,7 @@ index c2012299..4edb253f 100644 executablePath: process.execPath, // Packaged builds only, so a dev instance never shares the cache with the // prod app it is often run from. `--require` rather than NODE_COMPILE_CACHE, -@@ -593,6 +596,15 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv +@@ -612,6 +615,15 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv captureOutput: true, preflightFailure: Option.none(), } satisfies DesktopBackendManager.DesktopBackendStartConfig; @@ -38,10 +38,10 @@ index c2012299..4edb253f 100644 diff --git a/apps/desktop/src/backend/flatpakBackend.test.ts b/apps/desktop/src/backend/flatpakBackend.test.ts new file mode 100644 -index 00000000..4839d211 +index 000000000..0a940012a --- /dev/null +++ b/apps/desktop/src/backend/flatpakBackend.test.ts -@@ -0,0 +1,78 @@ +@@ -0,0 +1,87 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Option from "effect/Option"; + @@ -53,7 +53,7 @@ index 00000000..4839d211 + entryPath: "/app/server/bin.mjs", + args: ["/app/server/bin.mjs", "--bootstrap-fd", "3"], + cwd: "/home/alice", -+ env: { ELECTRON_RUN_AS_NODE: "1" }, ++ env: { ELECTRON_RUN_AS_NODE: "1", T3CODE_CLI_PATH: "/home/alice/.var/app/com.t3tools.t3code/data/t3/bin/t3" }, + extendEnv: true, + bootstrap: { + mode: "desktop", @@ -62,10 +62,13 @@ index 00000000..4839d211 + host: "127.0.0.1", + t3Home: "/home/alice/.var/app/com.t3tools.t3code/data/t3", + desktopBootstrapToken: "test-token", ++ shellEnvironmentPrepared: true, + tailscaleServeEnabled: false, + tailscaleServePort: 443, + desktopTelemetryFd: 4, + desktopTelemetryControlFd: 5, ++ desktopBrowserFd: 6, ++ desktopBrowserControlFd: 7, + resourceMonitorPath: "/app/resources/resource-monitor/t3-resource-monitor", + }, + bootstrapDelivery: "fd3", @@ -89,11 +92,14 @@ index 00000000..4839d211 + "--forward-fd=3", + "--forward-fd=4", + "--forward-fd=5", ++ "--forward-fd=6", ++ "--forward-fd=7", + `--directory=${runtime}`, + `--env=T3CODE_HOME=${config.bootstrap.t3Home}`, + `--env=T3CODE_FLATPAK_SERVER=${runtime}/t3code`, + `--env=T3CODE_FLATPAK_ENTRY=${runtime}/resources/app.asar/apps/server/dist/bin.mjs`, + `--env=T3CODE_FLATPAK_COMPILE_CACHE=${runtime}/resources/app.asar/apps/desktop/dist-electron/compileCache.cjs`, ++ `--env=T3CODE_CLI_PATH=${config.env.T3CODE_CLI_PATH}`, + "--env=PATH=/home/alice/.local/bin:/usr/bin", + "/bin/sh", + `${runtime}/host-server.sh`, @@ -105,6 +111,9 @@ index 00000000..4839d211 + assert.equal(result.bootstrap.desktopBootstrapToken, "test-token"); + assert.equal(result.bootstrap.desktopTelemetryFd, 4); + assert.equal(result.bootstrap.desktopTelemetryControlFd, 5); ++ assert.isFalse(result.bootstrap.shellEnvironmentPrepared); ++ assert.equal(result.bootstrap.desktopBrowserFd, 6); ++ assert.equal(result.bootstrap.desktopBrowserControlFd, 7); + assert.equal( + result.bootstrap.resourceMonitorPath, + `${runtime}/resources/resource-monitor/t3-resource-monitor`, @@ -122,10 +131,10 @@ index 00000000..4839d211 +}); diff --git a/apps/desktop/src/backend/flatpakBackend.ts b/apps/desktop/src/backend/flatpakBackend.ts new file mode 100644 -index 00000000..76d37f9e +index 000000000..bc8c61a2d --- /dev/null +++ b/apps/desktop/src/backend/flatpakBackend.ts -@@ -0,0 +1,52 @@ +@@ -0,0 +1,57 @@ +import * as Option from "effect/Option"; + +import type { DesktopBackendStartConfig } from "./DesktopBackendManager.ts"; @@ -149,6 +158,8 @@ index 00000000..76d37f9e + // Keep entryPath in the sandbox for preflight checks; pass host paths to the server. + const bootstrap = { + ...config.bootstrap, ++ // The sandbox skips shell hydration; the host server must prepare its own environment. ++ shellEnvironmentPrepared: false, + resourceMonitorPath: `${runtimeDir}/resources/resource-monitor/t3-resource-monitor`, + }; + return { @@ -160,11 +171,14 @@ index 00000000..76d37f9e + "--forward-fd=3", + "--forward-fd=4", + "--forward-fd=5", ++ "--forward-fd=6", ++ "--forward-fd=7", + `--directory=${runtimeDir}`, + `--env=T3CODE_HOME=${bootstrap.t3Home}`, + `--env=T3CODE_FLATPAK_SERVER=${runtimeDir}/t3code`, + `--env=T3CODE_FLATPAK_ENTRY=${runtimeDir}/resources/app.asar/apps/server/dist/bin.mjs`, + `--env=T3CODE_FLATPAK_COMPILE_CACHE=${runtimeDir}/resources/app.asar/apps/desktop/dist-electron/compileCache.cjs`, ++ ...(config.env.T3CODE_CLI_PATH ? [`--env=T3CODE_CLI_PATH=${config.env.T3CODE_CLI_PATH}`] : []), + ...(hostPath?.trim() ? [`--env=PATH=${hostPath}`] : []), + "/bin/sh", + `${runtimeDir}/host-server.sh`, diff --git a/patches/0003-flatpak-host-environment.patch b/patches/0003-flatpak-host-environment.patch index 54e1ae9..b2aa5e4 100644 --- a/patches/0003-flatpak-host-environment.patch +++ b/patches/0003-flatpak-host-environment.patch @@ -1,8 +1,8 @@ diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts -index 6a369a45..6ebdaf06 100644 +index 6efe81757..c453e6274 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts -@@ -100,6 +100,21 @@ function runShellEnvironment(input: { +@@ -120,6 +120,21 @@ function runShellEnvironment(input: { } describe("DesktopShellEnvironment", () => { @@ -21,22 +21,22 @@ index 6a369a45..6ebdaf06 100644 + }).pipe(Effect.tap(() => Effect.sync(() => assert.equal(env.PATH, "/app/bin:/usr/bin")))); + }); + - it.effect("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on macOS", () => + it.effect("rejects PATH output from a failed shell and tries the next candidate", () => Effect.gen(function* () { - const env: NodeJS.ProcessEnv = { + const env: NodeJS.ProcessEnv = { SHELL: "/bin/failing", PATH: "/usr/bin" }; diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts -index 704b061d..71b33253 100644 +index 2b41b6ea7..dfe68ac88 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts -@@ -8,6 +8,7 @@ import * as Schema from "effect/Schema"; - import * as ChildProcess from "effect/unstable/process/ChildProcess"; - import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; +@@ -10,6 +10,7 @@ import * as Stream from "effect/Stream"; + import * as ChildProcess from "effect/process/ChildProcess"; + import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import { flatpakAppId } from "../app/Flatpak.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; type EnvironmentPatch = Record; -@@ -500,6 +501,7 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix +@@ -515,6 +516,7 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix const installShellEnvironment = ( config: ShellEnvironmentConfig, ): Effect.Effect => { @@ -45,7 +45,7 @@ index 704b061d..71b33253 100644 return installWindowsEnvironment(config); } diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts -index 9745662c..39c39503 100644 +index 26ad0f1c4..fc5f324df 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -1,4 +1,4 @@ @@ -84,7 +84,7 @@ index 9745662c..39c39503 100644 const cause = Cause.combine( Cause.fail(new Error("updater failed")), diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts -index 245925a6..3aac79ec 100644 +index 77069d3ba..03ff73f2a 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -1,3 +1,4 @@ @@ -92,7 +92,7 @@ index 245925a6..3aac79ec 100644 import { DESKTOP_UPDATE_RESTART_MARKER_FILE, DesktopUpdateChannelSchema, -@@ -254,6 +255,9 @@ function getAutoUpdateDisabledReason(args: { +@@ -321,6 +322,9 @@ function getAutoUpdateDisabledReason(args: { disabledByEnv: boolean; hasUpdateFeedConfig: boolean; }): string | null { diff --git a/patches/0006-flatpak-document-portal-paths.patch b/patches/0006-flatpak-document-portal-paths.patch index 4ebb83e..83bdffb 100644 --- a/patches/0006-flatpak-document-portal-paths.patch +++ b/patches/0006-flatpak-document-portal-paths.patch @@ -1,5 +1,5 @@ diff --git a/apps/desktop/src/electron/ElectronDialog.test.ts b/apps/desktop/src/electron/ElectronDialog.test.ts -index 2ed5a1f2f..321003f9f 100644 +index 2ed5a1f2f..aa1b59fde 100644 --- a/apps/desktop/src/electron/ElectronDialog.test.ts +++ b/apps/desktop/src/electron/ElectronDialog.test.ts @@ -3,7 +3,7 @@ import * as Cause from "effect/Cause"; @@ -92,7 +92,7 @@ index 2ed5a1f2f..321003f9f 100644 Effect.gen(function* () { const cause = new Error("folder picker failed"); diff --git a/apps/desktop/src/electron/ElectronDialog.ts b/apps/desktop/src/electron/ElectronDialog.ts -index 182803337..baa503e20 100644 +index b48953f11..2e868e089 100644 --- a/apps/desktop/src/electron/ElectronDialog.ts +++ b/apps/desktop/src/electron/ElectronDialog.ts @@ -1,3 +1,7 @@ @@ -103,7 +103,7 @@ index 182803337..baa503e20 100644 import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -@@ -6,6 +9,58 @@ import * as Schema from "effect/Schema"; +@@ -6,6 +10,58 @@ import * as Schema from "effect/Schema"; import * as Electron from "electron"; @@ -162,7 +162,7 @@ index 182803337..baa503e20 100644 export class ElectronDialogPickFolderError extends Schema.TaggedError()( "ElectronDialogPickFolderError", { -@@ -136,7 +191,21 @@ export const make = ElectronDialog.of({ +@@ -138,7 +194,21 @@ export const make = ElectronDialog.of({ if (result.canceled) { return Option.none(); } diff --git a/patches/0007-flatpak-editor-discovery.patch b/patches/0007-flatpak-editor-discovery.patch index c908089..e87a054 100644 --- a/patches/0007-flatpak-editor-discovery.patch +++ b/patches/0007-flatpak-editor-discovery.patch @@ -1,8 +1,8 @@ diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts -index aab4d78f..0230bc5a 100644 +index 63e89541e..242d8c5ac 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts -@@ -159,6 +159,55 @@ it.effect("launches an installed editor with platform-safe arguments", () => +@@ -210,6 +210,55 @@ it.effect("launches an installed editor with platform-safe arguments", () => }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); @@ -59,10 +59,10 @@ index aab4d78f..0230bc5a 100644 it.effect.skipIf(windowsHost)(`launches Cursor in classic IDE mode on ${platform}`, () => Effect.gen(function* () { diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts -index ab287e4d..20be8a1c 100644 +index 3f2dab8f4..5e2c9e15b 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts -@@ -125,6 +125,67 @@ const CommandLookupEnvConfig = Config.all({ +@@ -146,6 +146,67 @@ const CommandLookupEnvConfig = Config.all({ const readBrowserLaunchEnv = BrowserLaunchEnvConfig.pipe(Effect.orElseSucceed(() => ({}))); const readCommandLookupEnv = CommandLookupEnvConfig.pipe(Effect.orElseSucceed(() => ({}))); @@ -130,7 +130,7 @@ index ab287e4d..20be8a1c 100644 function parseTargetPathAndPosition(target: string): Option.Option { const match = TARGET_WITH_POSITION_PATTERN.exec(target); if (!match?.[1] || !match[2]) { -@@ -417,6 +478,7 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" +@@ -446,6 +507,7 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner > { const available: EditorId[] = []; @@ -138,7 +138,7 @@ index ab287e4d..20be8a1c 100644 for (const editor of EDITORS) { if (editor.commands === null) { -@@ -427,7 +489,7 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" +@@ -456,7 +518,7 @@ const buildAvailableEditors = Effect.fn("externalLauncher.buildAvailableEditors" } const command = yield* resolveEditorCommand(editor, env); @@ -147,7 +147,7 @@ index ab287e4d..20be8a1c 100644 available.push(editor.id); } } -@@ -529,8 +591,24 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( +@@ -558,8 +620,24 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( } if (editorDef.commands) { diff --git a/patches/0008-flatpak-cli-launcher.patch b/patches/0008-flatpak-cli-launcher.patch new file mode 100644 index 0000000..4eb3830 --- /dev/null +++ b/patches/0008-flatpak-cli-launcher.patch @@ -0,0 +1,31 @@ +diff --git a/apps/desktop/src/app/DesktopCliShim.ts b/apps/desktop/src/app/DesktopCliShim.ts +index 5cd942f66..60c5d3b6e 100644 +--- a/apps/desktop/src/app/DesktopCliShim.ts ++++ b/apps/desktop/src/app/DesktopCliShim.ts +@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect"; + import * as FileSystem from "effect/FileSystem"; + import * as Option from "effect/Option"; + ++import { flatpakAppId } from "./Flatpak.ts"; + import * as DesktopEnvironment from "./DesktopEnvironment.ts"; + import { makeComponentLogger } from "./DesktopObservability.ts"; + +@@ -157,7 +158,17 @@ export const install = Effect.gen(function* () { + // macOS and .deb installs live at a fixed path, so the launcher runs the app directly. + onNone: () => ({ kind: "direct" as const, executable: process.execPath, entry }), + }); +- const content = renderCliShim({ target, shimPath, t3Home: environment.baseDir }); ++ const runtimeDir = process.env.T3CODE_FLATPAK_RUNTIME; ++ // Commands shown by the host server run outside the sandbox, where /app does not exist. ++ const hostTarget: CliShimTarget = ++ flatpakAppId(process.env, environment.platform) !== null && runtimeDir?.startsWith("/") ++ ? { ++ kind: "direct", ++ executable: `${runtimeDir}/t3code`, ++ entry: `${runtimeDir}/resources/app.asar/${SERVER_ENTRY}`, ++ } ++ : target; ++ const content = renderCliShim({ target: hostTarget, shimPath, t3Home: environment.baseDir }); + + return yield* Effect.gen(function* () { + const existing = yield* fs.readFileString(shimPath).pipe(Effect.option); From ac4f5a478429c5e4f1d9807c560f2ad173cb078c Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 15 Oct 2026 21:12:58 -0400 Subject: [PATCH 3/9] Update editor launcher test for upstream helper rename --- patches/0007-flatpak-editor-discovery.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/0007-flatpak-editor-discovery.patch b/patches/0007-flatpak-editor-discovery.patch index e87a054..ae209c4 100644 --- a/patches/0007-flatpak-editor-discovery.patch +++ b/patches/0007-flatpak-editor-discovery.patch @@ -1,5 +1,5 @@ diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts -index 63e89541e..242d8c5ac 100644 +index 63e89541e..4609b1f6d 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -210,6 +210,55 @@ it.effect("launches an installed editor with platform-safe arguments", () => @@ -25,7 +25,7 @@ index 63e89541e..242d8c5ac 100644 + return editors; + }).pipe( + Effect.provide( -+ testLayer({ ++ layerTest({ + platform: "linux", + env: { PATH: binDir }, + onSpawn: (command) => spawned.push(command), From fcb8767a3d7a491f34b3ea4ff7a9b812eb8e9bef Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 00:21:48 -0400 Subject: [PATCH 4/9] Document host requirements for headless browser features --- flatpak/com.t3tools.t3code.metainfo.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flatpak/com.t3tools.t3code.metainfo.xml b/flatpak/com.t3tools.t3code.metainfo.xml index a9c106c..9678671 100644 --- a/flatpak/com.t3tools.t3code.metainfo.xml +++ b/flatpak/com.t3tools.t3code.metainfo.xml @@ -9,6 +9,12 @@

An unofficial Flatpak package for T3 Code. The desktop runs inside Flatpak and starts its matching T3 server on the host, where coding agents can use your projects and development tools.

+

Headless browser features, including HTML preview screenshots and some + recording tasks, download and run Chromium on the host. Your system must + provide Chromium's runtime libraries, including NSS, GLib, accessibility, + graphics, and ALSA libraries. Systems that restrict unprivileged user + namespaces through AppArmor may also require a manually configured + AppArmor profile to allow Chromium's sandbox to run.

com.t3tools.t3code.desktop https://t3.codes From 2ed393c89fa8ad34103e53db86890b287a1ee3f4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 00:23:35 -0400 Subject: [PATCH 5/9] Revert "Document host requirements for headless browser features" This reverts commit fcb8767a3d7a491f34b3ea4ff7a9b812eb8e9bef. --- flatpak/com.t3tools.t3code.metainfo.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flatpak/com.t3tools.t3code.metainfo.xml b/flatpak/com.t3tools.t3code.metainfo.xml index 9678671..a9c106c 100644 --- a/flatpak/com.t3tools.t3code.metainfo.xml +++ b/flatpak/com.t3tools.t3code.metainfo.xml @@ -9,12 +9,6 @@

An unofficial Flatpak package for T3 Code. The desktop runs inside Flatpak and starts its matching T3 server on the host, where coding agents can use your projects and development tools.

-

Headless browser features, including HTML preview screenshots and some - recording tasks, download and run Chromium on the host. Your system must - provide Chromium's runtime libraries, including NSS, GLib, accessibility, - graphics, and ALSA libraries. Systems that restrict unprivileged user - namespaces through AppArmor may also require a manually configured - AppArmor profile to allow Chromium's sandbox to run.

com.t3tools.t3code.desktop https://t3.codes From 09cdc7028d263d2c7f8ae1a27b2f6f5d9508b496 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 00:53:08 -0400 Subject: [PATCH 6/9] Keep Flatpak setup and updates package-managed --- flatpak/com.t3tools.t3code.metainfo.xml | 6 + flatpak/modules/t3code.yml | 7 +- patches/0002-flatpak-host-backend.patch | 12 +- patches/0008-flatpak-cli-launcher.patch | 110 ++++++++++++++---- patches/0009-flatpak-server-updates.patch | 132 ++++++++++++++++++++++ 5 files changed, 239 insertions(+), 28 deletions(-) create mode 100644 patches/0009-flatpak-server-updates.patch diff --git a/flatpak/com.t3tools.t3code.metainfo.xml b/flatpak/com.t3tools.t3code.metainfo.xml index a9c106c..9678671 100644 --- a/flatpak/com.t3tools.t3code.metainfo.xml +++ b/flatpak/com.t3tools.t3code.metainfo.xml @@ -9,6 +9,12 @@

An unofficial Flatpak package for T3 Code. The desktop runs inside Flatpak and starts its matching T3 server on the host, where coding agents can use your projects and development tools.

+

Headless browser features, including HTML preview screenshots and some + recording tasks, download and run Chromium on the host. Your system must + provide Chromium's runtime libraries, including NSS, GLib, accessibility, + graphics, and ALSA libraries. Systems that restrict unprivileged user + namespaces through AppArmor may also require a manually configured + AppArmor profile to allow Chromium's sandbox to run.

com.t3tools.t3code.desktop https://t3.codes diff --git a/flatpak/modules/t3code.yml b/flatpak/modules/t3code.yml index 5154a61..fbe47f2 100644 --- a/flatpak/modules/t3code.yml +++ b/flatpak/modules/t3code.yml @@ -45,7 +45,11 @@ test-commands: pnpm --filter @t3tools/desktop exec vp test run --exclude src/wsl/DesktopWslEnvironment.test.ts - pnpm --filter @t3tools/desktop typecheck - - pnpm --filter t3 exec vp test run src/process/externalLauncher.test.ts + - >- + pnpm --filter t3 exec vp test run + src/process/externalLauncher.test.ts + src/cloud/selfUpdate.test.ts + src/environment/ServerEnvironment.test.ts - pnpm --filter t3 typecheck sources: - type: git @@ -69,6 +73,7 @@ sources: - ../../patches/0006-flatpak-document-portal-paths.patch - ../../patches/0007-flatpak-editor-discovery.patch - ../../patches/0008-flatpak-cli-launcher.patch + - ../../patches/0009-flatpak-server-updates.patch use-git: true - type: dir path: ../package diff --git a/patches/0002-flatpak-host-backend.patch b/patches/0002-flatpak-host-backend.patch index bc10a23..e52c082 100644 --- a/patches/0002-flatpak-host-backend.patch +++ b/patches/0002-flatpak-host-backend.patch @@ -38,10 +38,10 @@ index b306975be..7f70a3cfc 100644 diff --git a/apps/desktop/src/backend/flatpakBackend.test.ts b/apps/desktop/src/backend/flatpakBackend.test.ts new file mode 100644 -index 000000000..0a940012a +index 000000000..cfc536d2e --- /dev/null +++ b/apps/desktop/src/backend/flatpakBackend.test.ts -@@ -0,0 +1,87 @@ +@@ -0,0 +1,86 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Option from "effect/Option"; + @@ -53,7 +53,7 @@ index 000000000..0a940012a + entryPath: "/app/server/bin.mjs", + args: ["/app/server/bin.mjs", "--bootstrap-fd", "3"], + cwd: "/home/alice", -+ env: { ELECTRON_RUN_AS_NODE: "1", T3CODE_CLI_PATH: "/home/alice/.var/app/com.t3tools.t3code/data/t3/bin/t3" }, ++ env: { ELECTRON_RUN_AS_NODE: "1" }, + extendEnv: true, + bootstrap: { + mode: "desktop", @@ -99,7 +99,6 @@ index 000000000..0a940012a + `--env=T3CODE_FLATPAK_SERVER=${runtime}/t3code`, + `--env=T3CODE_FLATPAK_ENTRY=${runtime}/resources/app.asar/apps/server/dist/bin.mjs`, + `--env=T3CODE_FLATPAK_COMPILE_CACHE=${runtime}/resources/app.asar/apps/desktop/dist-electron/compileCache.cjs`, -+ `--env=T3CODE_CLI_PATH=${config.env.T3CODE_CLI_PATH}`, + "--env=PATH=/home/alice/.local/bin:/usr/bin", + "/bin/sh", + `${runtime}/host-server.sh`, @@ -131,10 +130,10 @@ index 000000000..0a940012a +}); diff --git a/apps/desktop/src/backend/flatpakBackend.ts b/apps/desktop/src/backend/flatpakBackend.ts new file mode 100644 -index 000000000..bc8c61a2d +index 000000000..e912b2119 --- /dev/null +++ b/apps/desktop/src/backend/flatpakBackend.ts -@@ -0,0 +1,57 @@ +@@ -0,0 +1,56 @@ +import * as Option from "effect/Option"; + +import type { DesktopBackendStartConfig } from "./DesktopBackendManager.ts"; @@ -178,7 +177,6 @@ index 000000000..bc8c61a2d + `--env=T3CODE_FLATPAK_SERVER=${runtimeDir}/t3code`, + `--env=T3CODE_FLATPAK_ENTRY=${runtimeDir}/resources/app.asar/apps/server/dist/bin.mjs`, + `--env=T3CODE_FLATPAK_COMPILE_CACHE=${runtimeDir}/resources/app.asar/apps/desktop/dist-electron/compileCache.cjs`, -+ ...(config.env.T3CODE_CLI_PATH ? [`--env=T3CODE_CLI_PATH=${config.env.T3CODE_CLI_PATH}`] : []), + ...(hostPath?.trim() ? [`--env=PATH=${hostPath}`] : []), + "/bin/sh", + `${runtimeDir}/host-server.sh`, diff --git a/patches/0008-flatpak-cli-launcher.patch b/patches/0008-flatpak-cli-launcher.patch index 4eb3830..1ed022b 100644 --- a/patches/0008-flatpak-cli-launcher.patch +++ b/patches/0008-flatpak-cli-launcher.patch @@ -1,5 +1,83 @@ +diff --git a/apps/desktop/src/app/DesktopCliCommand.test.ts b/apps/desktop/src/app/DesktopCliCommand.test.ts +index b2305fe98..14d6857e3 100644 +--- a/apps/desktop/src/app/DesktopCliCommand.test.ts ++++ b/apps/desktop/src/app/DesktopCliCommand.test.ts +@@ -1,5 +1,5 @@ + import * as NodeServices from "@effect/platform-node/NodeServices"; +-import { afterEach, beforeEach, expect, it } from "@effect/vitest"; ++import { afterEach, beforeEach, expect, it, vi } from "@effect/vitest"; + import * as Effect from "effect/Effect"; + import * as FileSystem from "effect/FileSystem"; + import * as Option from "effect/Option"; +@@ -95,6 +95,36 @@ afterEach(() => { + }); + + it.layer(NodeServices.layer)("DesktopCliCommand", (it) => { ++ it.effect("hides Flatpak CLI setup and leaves launchers and host command links untouched", () => ++ Effect.gen(function* () { ++ vi.stubEnv("FLATPAK_ID", "com.t3tools.t3code"); ++ const fs = yield* FileSystem.FileSystem; ++ const path = yield* Path.Path; ++ const home = yield* fs.makeTempDirectoryScoped(); ++ const baseDir = path.join(home, "flatpak-data"); ++ const link = path.join(home, ".local", "bin", "t3"); ++ yield* fs.makeDirectory(path.dirname(link), { recursive: true }); ++ yield* fs.writeFileString(link, "existing host command"); ++ const command = yield* commandIn({ home, baseDir }); ++ expect(yield* command.state).toEqual({ ++ supported: false, ++ installedPath: null, ++ onPath: false, ++ }); ++ expect((yield* Effect.flip(command.install)).message).toContain("Flatpak"); ++ expect((yield* Effect.flip(command.uninstall)).message).toContain("Flatpak"); ++ const shim = yield* DesktopCliShim.install.pipe( ++ Effect.provideService( ++ DesktopEnvironment.DesktopEnvironment, ++ environmentFor(path, { home, baseDir }), ++ ), ++ ); ++ expect(Option.isNone(shim)).toBe(true); ++ expect(yield* fs.exists(path.join(baseDir, "bin", "t3"))).toBe(false); ++ expect(yield* fs.readFileString(link)).toBe("existing host command"); ++ }).pipe(Effect.scoped, Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs()))), ++ ); ++ + it.effect("links the launcher onto PATH and removes only that link", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; +diff --git a/apps/desktop/src/app/DesktopCliCommand.ts b/apps/desktop/src/app/DesktopCliCommand.ts +index d79cab92f..d80daf5dc 100644 +--- a/apps/desktop/src/app/DesktopCliCommand.ts ++++ b/apps/desktop/src/app/DesktopCliCommand.ts +@@ -9,6 +9,7 @@ import * as Stream from "effect/Stream"; + import * as ChildProcess from "effect/process/ChildProcess"; + import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + ++import { flatpakAppId } from "./Flatpak.ts"; + import * as DesktopCliShim from "./DesktopCliShim.ts"; + import * as DesktopEnvironment from "./DesktopEnvironment.ts"; + +@@ -76,6 +77,17 @@ export class DesktopCliCommand extends Context.Service< + /** @public Service construction is part of the canonical Effect module API. */ + export const make = Effect.gen(function* () { + const environment = yield* DesktopEnvironment.DesktopEnvironment; ++ if (flatpakAppId(process.env, environment.platform) !== null) { ++ const unsupported = { supported: false, installedPath: null, onPath: false } as const; ++ const unavailable = Effect.fail( ++ new DesktopCliCommandError({ message: "The Flatpak package does not install a t3 command." }), ++ ); ++ return DesktopCliCommand.of({ ++ state: Effect.succeed(unsupported), ++ install: unavailable, ++ uninstall: unavailable, ++ }); ++ } + const fs = yield* FileSystem.FileSystem; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = environment.path; diff --git a/apps/desktop/src/app/DesktopCliShim.ts b/apps/desktop/src/app/DesktopCliShim.ts -index 5cd942f66..60c5d3b6e 100644 +index 5cd942f66..9fa569993 100644 --- a/apps/desktop/src/app/DesktopCliShim.ts +++ b/apps/desktop/src/app/DesktopCliShim.ts @@ -3,6 +3,7 @@ import * as Effect from "effect/Effect"; @@ -10,22 +88,14 @@ index 5cd942f66..60c5d3b6e 100644 import * as DesktopEnvironment from "./DesktopEnvironment.ts"; import { makeComponentLogger } from "./DesktopObservability.ts"; -@@ -157,7 +158,17 @@ export const install = Effect.gen(function* () { - // macOS and .deb installs live at a fixed path, so the launcher runs the app directly. - onNone: () => ({ kind: "direct" as const, executable: process.execPath, entry }), - }); -- const content = renderCliShim({ target, shimPath, t3Home: environment.baseDir }); -+ const runtimeDir = process.env.T3CODE_FLATPAK_RUNTIME; -+ // Commands shown by the host server run outside the sandbox, where /app does not exist. -+ const hostTarget: CliShimTarget = -+ flatpakAppId(process.env, environment.platform) !== null && runtimeDir?.startsWith("/") -+ ? { -+ kind: "direct", -+ executable: `${runtimeDir}/t3code`, -+ entry: `${runtimeDir}/resources/app.asar/${SERVER_ENTRY}`, -+ } -+ : target; -+ const content = renderCliShim({ target: hostTarget, shimPath, t3Home: environment.baseDir }); - - return yield* Effect.gen(function* () { - const existing = yield* fs.readFileString(shimPath).pipe(Effect.option); +@@ -140,7 +141,9 @@ export const launcherPath = (environment: DesktopEnvironment.DesktopEnvironment[ + */ + export const install = Effect.gen(function* () { + const environment = yield* DesktopEnvironment.DesktopEnvironment; +- if (!environment.isPackaged) return Option.none(); ++ if (!environment.isPackaged || flatpakAppId(process.env, environment.platform) !== null) { ++ return Option.none(); ++ } + const fs = yield* FileSystem.FileSystem; + const path = environment.path; + const windows = environment.platform === "win32"; diff --git a/patches/0009-flatpak-server-updates.patch b/patches/0009-flatpak-server-updates.patch new file mode 100644 index 0000000..d09d921 --- /dev/null +++ b/patches/0009-flatpak-server-updates.patch @@ -0,0 +1,132 @@ +diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts +index 443fad607..8a13118a4 100644 +--- a/apps/server/src/cloud/selfUpdate.test.ts ++++ b/apps/server/src/cloud/selfUpdate.test.ts +@@ -128,6 +128,28 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* ( + }); + + it.layer(NodeServices.layer)("server self update", (it) => { ++ it.effect("rejects Flatpak updates and commits before invoking any updater", () => ++ Effect.gen(function* () { ++ const { selfUpdate, order } = yield* makeHarness({ ++ mode: "desktop", ++ desktopAppUpdate: { ++ available: true, ++ run: () => Effect.die("must not start a desktop update"), ++ commit: () => Effect.die("must not commit a desktop update"), ++ }, ++ }).pipe( ++ Effect.provideService(HostProcess.Environment, { ++ T3CODE_FLATPAK_SERVER: "/installed/flatpak/t3code", ++ }), ++ ); ++ const error = yield* Effect.flip(selfUpdate.update({ targetVersion: "1.1.0" })); ++ expect(error.reason).toContain("Flatpak"); ++ const commitError = yield* Effect.flip(selfUpdate.commitDesktopUpdate("request")); ++ expect(commitError.reason).toContain("Flatpak"); ++ expect(order).toEqual([]); ++ }).pipe(Effect.scoped), ++ ); ++ + it.effect("marks running threads at the boot-service handoff", () => + Effect.gen(function* () { + const events: string[] = []; +diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts +index 615351c43..965adca86 100644 +--- a/apps/server/src/cloud/selfUpdate.ts ++++ b/apps/server/src/cloud/selfUpdate.ts +@@ -185,6 +185,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { + yield* Config.String(CLI_RELEASE_BASE_URL_ENV).pipe(Config.option), + ); + const inFlight = yield* Ref.make(false); ++ const flatpakManaged = Boolean((yield* HostProcess.Environment).T3CODE_FLATPAK_SERVER); + + const capability: ServerSelfUpdateCapability | null = + serverConfig.mode === "desktop" ? "desktop-managed" : launcher.managed ? "boot-service" : null; +@@ -196,6 +197,11 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { + const update: ServerSelfUpdate["Service"]["update"] = Effect.fn( + "cloud.server_self_update.update", + )(function* (input, reportProgress = () => Effect.void, onHandoffAccepted = () => Effect.void) { ++ if (flatpakManaged) { ++ return yield* failWith( ++ "Updates are managed by Flatpak. Update the Flatpak package on this machine.", ++ ); ++ } + if (capability === "desktop-managed") { + // input.targetVersion is meaningless here: the desktop app's own + // update feed decides what it downloads, and the result carries what +@@ -337,7 +343,11 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { + return ServerSelfUpdate.of({ + update, + commitDesktopUpdate: (requestId, onHandoffAccepted) => +- desktopAppUpdate.commit(requestId, onHandoffAccepted), ++ flatpakManaged ++ ? Effect.fail( ++ failWith("Updates are managed by Flatpak. Update the Flatpak package on this machine."), ++ ) ++ : desktopAppUpdate.commit(requestId, onHandoffAccepted), + }); + }); + +diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts +index a77802357..43b6f793b 100644 +--- a/apps/server/src/environment/ServerEnvironment.test.ts ++++ b/apps/server/src/environment/ServerEnvironment.test.ts +@@ -96,7 +96,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { + yield* fs.symlink(entry, `${prefix}/bin/t3`); + const config = yield* makeServerConfig(baseDir); + yield* fs.makeDirectory(config.stateDir, { recursive: true }); +- for (const mode of ["web", "desktop"] as const) { ++ for (const { mode, flatpakManaged } of [ ++ { mode: "web", flatpakManaged: false }, ++ { mode: "desktop", flatpakManaged: false }, ++ { mode: "desktop", flatpakManaged: true }, ++ ] as const) { + const descriptor = yield* Effect.gen(function* () { + const environment = yield* ServerEnvironment.ServerEnvironment; + return yield* environment.getDescriptor; +@@ -104,13 +108,15 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { + Effect.provide( + ServerEnvironment.layer.pipe( + Layer.provide(layerEmptySecretStore), +- Layer.provide(ServerConfig.layer({ ...config, mode })), ++ Layer.provide(ServerConfig.layer({ ...config, mode, desktopTelemetryControlFd: 5 })), + ), + ), + Effect.provideService(HostProcess.Arguments, ["node", entry]), + Effect.provideService(HostProcess.IsExecutable, false), + Effect.provideService(HostProcess.Platform, "linux"), +- Effect.provideService(HostProcess.Environment, {}), ++ Effect.provideService(HostProcess.Environment, { ++ ...(flatpakManaged ? { T3CODE_FLATPAK_SERVER: "/installed/flatpak/t3code" } : {}), ++ }), + ); + expect(descriptor.capabilities.serverInstallation).toEqual( + mode === "web" ? { kind: "npm-global", prefix } : undefined, +@@ -118,6 +124,9 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { + expect(descriptor.capabilities.serverSelfUpdate).toBe( + mode === "web" ? undefined : "desktop-managed", + ); ++ expect(descriptor.capabilities.desktopAppUpdate).toBe( ++ mode === "desktop" && !flatpakManaged ? true : undefined, ++ ); + expect(descriptor.capabilities).toMatchObject({ + settledWorktreeCleanup: true, + storageCleanupRun: true, +diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts +index 8e635f981..989a8b63d 100644 +--- a/apps/server/src/environment/ServerEnvironment.ts ++++ b/apps/server/src/environment/ServerEnvironment.ts +@@ -202,8 +202,11 @@ export const make = Effect.gen(function* () { + // app and its bundled server ship in one artifact, so a present fd means + // the app speaks the requestDesktopUpdate protocol. WSL backends never get + // the fd and correctly do not advertise. ++ const flatpakManaged = Boolean((yield* HostProcess.Environment).T3CODE_FLATPAK_SERVER); + const desktopAppUpdate = +- serverSelfUpdate === "desktop-managed" && serverConfig.desktopTelemetryControlFd !== undefined; ++ !flatpakManaged && ++ serverSelfUpdate === "desktop-managed" && ++ serverConfig.desktopTelemetryControlFd !== undefined; + + const descriptor: ExecutionEnvironmentDescriptor = { + environmentId, From 069b5952cd81fc617e8c87d231579834df922710 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 00:54:38 -0400 Subject: [PATCH 7/9] Explain manual browser requirements without exposing CLI setup --- flatpak/modules/t3code.yml | 2 + .../0010-flatpak-browser-requirements.patch | 83 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 patches/0010-flatpak-browser-requirements.patch diff --git a/flatpak/modules/t3code.yml b/flatpak/modules/t3code.yml index fbe47f2..0778f67 100644 --- a/flatpak/modules/t3code.yml +++ b/flatpak/modules/t3code.yml @@ -50,6 +50,7 @@ test-commands: src/process/externalLauncher.test.ts src/cloud/selfUpdate.test.ts src/environment/ServerEnvironment.test.ts + src/preview/PreviewBrowserHost.test.ts - pnpm --filter t3 typecheck sources: - type: git @@ -74,6 +75,7 @@ sources: - ../../patches/0007-flatpak-editor-discovery.patch - ../../patches/0008-flatpak-cli-launcher.patch - ../../patches/0009-flatpak-server-updates.patch + - ../../patches/0010-flatpak-browser-requirements.patch use-git: true - type: dir path: ../package diff --git a/patches/0010-flatpak-browser-requirements.patch b/patches/0010-flatpak-browser-requirements.patch new file mode 100644 index 0000000..a5a6c1b --- /dev/null +++ b/patches/0010-flatpak-browser-requirements.patch @@ -0,0 +1,83 @@ +diff --git a/apps/server/src/preview/PreviewBrowserHost.test.ts b/apps/server/src/preview/PreviewBrowserHost.test.ts +index 5ae0a7075..36351fd9b 100644 +--- a/apps/server/src/preview/PreviewBrowserHost.test.ts ++++ b/apps/server/src/preview/PreviewBrowserHost.test.ts +@@ -73,6 +73,21 @@ const diagnose = (input: { + }; + + describe("diagnoseLaunchFailure", () => { ++ it.effect.each([ ++ { output: SANDBOX_ABORT, ldd: "", instruction: "AppArmor profile" }, ++ { output: LOADER_ERROR, ldd: LDD_IN_BARE_DEBIAN, instruction: "libglib-2.0.so.0" }, ++ ])("gives Flatpak users manual setup instructions for $instruction", (input) => ++ Effect.gen(function* () { ++ const { error } = yield* diagnose({ platform: "linux", ...input }).pipe( ++ Effect.provideService(HostProcess.Environment, { ++ T3CODE_FLATPAK_SERVER: "/installed/flatpak/t3code", ++ }), ++ ); ++ expect(error?.message).toContain(input.instruction); ++ expect(error?.message).not.toContain("t3 browser setup"); ++ }), ++ ); ++ + it.effect("turns Chrome's sandbox abort into the AppArmor steps", () => + Effect.gen(function* () { + const { error, commands } = yield* diagnose({ +diff --git a/apps/server/src/preview/PreviewBrowserHost.ts b/apps/server/src/preview/PreviewBrowserHost.ts +index de136cf41..53ad43441 100644 +--- a/apps/server/src/preview/PreviewBrowserHost.ts ++++ b/apps/server/src/preview/PreviewBrowserHost.ts +@@ -71,18 +71,24 @@ export const DEBIAN_PACKAGES: ReadonlyArray> = [ + + export class PreviewBrowserSandboxError extends Schema.TaggedError()( + "PreviewBrowserSandboxError", +- { setupCommand: Schema.String }, ++ { setupCommand: Schema.NullOr(Schema.String) }, + ) { + override get message(): string { ++ if (this.setupCommand === null) { ++ return "This host blocks the sandbox T3's headless browser needs. Configure an AppArmor profile on the host to allow unprivileged user namespaces for T3's downloaded chrome-headless-shell. The Flatpak package does not change host security configuration."; ++ } + return `This host blocks the sandbox T3's browser runs in (AppArmor on Ubuntu 23.10+). Run \`${this.setupCommand}\` on the host once to allow it, then try again.`; + } + } + + export class PreviewBrowserLibrariesError extends Schema.TaggedError()( + "PreviewBrowserLibrariesError", +- { setupCommand: Schema.String, libraries: Schema.Array(Schema.String) }, ++ { setupCommand: Schema.NullOr(Schema.String), libraries: Schema.Array(Schema.String) }, + ) { + override get message(): string { ++ if (this.setupCommand === null) { ++ return `This host is missing libraries T3's headless browser needs (${this.libraries.join(", ")}). Install them using your host's package management tools, then try again. The Flatpak package does not install host libraries.`; ++ } + return `This host is missing libraries T3's browser needs (${this.libraries.join(", ")}). Run \`${this.setupCommand}\` on the host to install them, then try again.`; + } + } +@@ -104,7 +110,9 @@ export const diagnoseLaunchFailure = Effect.fn("PreviewBrowserHost.diagnoseLaunc + readonly output: string; + readonly setupCommand: string; + }) { +- const { setupCommand } = input; ++ const setupCommand = (yield* HostProcess.Environment).T3CODE_FLATPAK_SERVER ++ ? null ++ : input.setupCommand; + if (input.output.includes(NO_SANDBOX_SIGNATURE)) { + return new PreviewBrowserSandboxError({ setupCommand }); + } +diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts +index 87789692e..259aab1fd 100644 +--- a/apps/server/src/preview/ServerBrowser.ts ++++ b/apps/server/src/preview/ServerBrowser.ts +@@ -2477,7 +2477,9 @@ const make = Effect.gen(function* () { + (yield* PreviewBrowserHost.sandboxBlocked) + ) { + yield* Effect.logWarning( +- `This host blocks the sandbox T3's browser runs in, so browser tabs and HTML previews will not start. Run \`${setupCommand}\` once to allow it.`, ++ new PreviewBrowserHost.PreviewBrowserSandboxError({ ++ setupCommand: (yield* HostProcess.Environment).T3CODE_FLATPAK_SERVER ? null : setupCommand, ++ }).message, + ); + } + // The desktop took its page back (closed, swapped, crashed, or devtools opened). From 00e77d18de31e6f5786c3477266aae3a17d01bb1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 01:01:06 -0400 Subject: [PATCH 8/9] Handle manual browser setup in preview viewers --- flatpak/modules/t3code.yml | 1 + .../0010-flatpak-browser-requirements.patch | 117 ++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/flatpak/modules/t3code.yml b/flatpak/modules/t3code.yml index 0778f67..4a427cb 100644 --- a/flatpak/modules/t3code.yml +++ b/flatpak/modules/t3code.yml @@ -51,6 +51,7 @@ test-commands: src/cloud/selfUpdate.test.ts src/environment/ServerEnvironment.test.ts src/preview/PreviewBrowserHost.test.ts + src/preview/ServerBrowserStream.test.ts - pnpm --filter t3 typecheck sources: - type: git diff --git a/patches/0010-flatpak-browser-requirements.patch b/patches/0010-flatpak-browser-requirements.patch index a5a6c1b..3e71870 100644 --- a/patches/0010-flatpak-browser-requirements.patch +++ b/patches/0010-flatpak-browser-requirements.patch @@ -1,3 +1,16 @@ +diff --git a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx +index 081e81f13..3c997cda7 100644 +--- a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx ++++ b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx +@@ -510,7 +510,7 @@ function PreviewStreamDocumentView({ + {status === "error" ? error : "Connecting to browser..."} + + )} +- {status === "error" && hostSetup && !compact ? ( ++ {status === "error" && hostSetup?.command && !compact ? ( + ++])("reports host setup with a command only when one is available ($need)", ({ error, need }) => + Effect.gen(function* () { + const browser = ServerBrowser.ServerBrowser.of({ + clearProfile: () => Effect.void, +@@ -359,7 +370,7 @@ it.effect.each([ + expect(code).toBe(PREVIEW_STREAM_HOST_SETUP_CLOSE_CODE); + expect(yield* decodeHostSetup(reason)).toEqual({ + need, +- command: "sudo t3 browser setup", ++ command: error.setupCommand ?? "", + }); + }).pipe(Effect.scoped), + ); +diff --git a/apps/server/src/preview/ServerBrowserStream.ts b/apps/server/src/preview/ServerBrowserStream.ts +index b2960c240..87d0e1570 100644 +--- a/apps/server/src/preview/ServerBrowserStream.ts ++++ b/apps/server/src/preview/ServerBrowserStream.ts +@@ -203,9 +203,9 @@ const makeHandler = (browser: ServerBrowser.ServerBrowser["Service"]) => + /** Which host setup a launch failure is missing, and the command that fixes it. */ + const hostSetup = (cause: unknown): PreviewStreamHostSetup | undefined => + isSandboxError(cause) +- ? { need: "sandbox", command: cause.setupCommand } ++ ? { need: "sandbox", command: cause.setupCommand ?? "" } + : isLibrariesError(cause) +- ? { need: "libraries", command: cause.setupCommand } ++ ? { need: "libraries", command: cause.setupCommand ?? "" } + : undefined; + const isSandboxError = Schema.is(PreviewBrowserHost.PreviewBrowserSandboxError); + const isLibrariesError = Schema.is(PreviewBrowserHost.PreviewBrowserLibrariesError); +diff --git a/apps/web/src/browser/ServerBrowserSurface.tsx b/apps/web/src/browser/ServerBrowserSurface.tsx +index b8f403598..a07843935 100644 +--- a/apps/web/src/browser/ServerBrowserSurface.tsx ++++ b/apps/web/src/browser/ServerBrowserSurface.tsx +@@ -892,7 +892,9 @@ export function ServerBrowserSurface(props: { +

+ {previewStreamHostSetupMessage(hostSetup)} +

+- ++ {hostSetup.command ? ( ++ ++ ) : null} + +diff --git a/packages/client-runtime/src/preview/serverBrowserStream.ts b/packages/client-runtime/src/preview/serverBrowserStream.ts +index 34a378697..6b995950b 100644 +--- a/packages/client-runtime/src/preview/serverBrowserStream.ts ++++ b/packages/client-runtime/src/preview/serverBrowserStream.ts +@@ -476,7 +476,13 @@ const decodeHostSetup = (reason: string): PreviewStreamHostSetup => { + }; + + /** What a viewer tells the person; `command` is shown beside it, ready to copy. */ +-export const previewStreamHostSetupMessage = (setup: PreviewStreamHostSetup) => +- setup.need === "sandbox" ++export const previewStreamHostSetupMessage = (setup: PreviewStreamHostSetup) => { ++ if (!setup.command) { ++ return setup.need === "sandbox" ++ ? "This server's headless browser needs an AppArmor profile configured on its host. Configure it manually, then try again." ++ : "This server's headless browser needs additional libraries on its host. Install the missing libraries with the host's package management tools, then try again."; ++ } ++ return setup.need === "sandbox" + ? "This server's host blocks the sandbox its browser runs in. Run this once on the host, then try again:" + : "This server's host is missing libraries its browser needs. Run this once on the host, then try again:"; ++}; +diff --git a/packages/contracts/src/preview.ts b/packages/contracts/src/preview.ts +index 802bbd476..126fa9af8 100644 +--- a/packages/contracts/src/preview.ts ++++ b/packages/contracts/src/preview.ts +@@ -188,7 +188,7 @@ export type PreviewRuntime = typeof PreviewRuntime.Type; + */ + export const PreviewStreamHostSetup = Schema.Struct({ + need: Schema.Literals(["sandbox", "libraries"]), +- /** For example `sudo npx t3 browser setup`, matching how the server was launched. */ ++ /** For example `sudo npx t3 browser setup`. Empty when host setup must be done manually. */ + command: Schema.String, + }); + export type PreviewStreamHostSetup = typeof PreviewStreamHostSetup.Type; From ffebb044bc471a368012cdcb4e6897b617aa3fff Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Oct 2026 01:12:52 -0400 Subject: [PATCH 9/9] Limit browser viewer changes to the Flatpak client --- patches/0010-flatpak-browser-requirements.patch | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/patches/0010-flatpak-browser-requirements.patch b/patches/0010-flatpak-browser-requirements.patch index 3e71870..2e9fdf1 100644 --- a/patches/0010-flatpak-browser-requirements.patch +++ b/patches/0010-flatpak-browser-requirements.patch @@ -1,16 +1,3 @@ -diff --git a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx -index 081e81f13..3c997cda7 100644 ---- a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx -+++ b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx -@@ -510,7 +510,7 @@ function PreviewStreamDocumentView({ - {status === "error" ? error : "Connecting to browser..."} - - )} -- {status === "error" && hostSetup && !compact ? ( -+ {status === "error" && hostSetup?.command && !compact ? ( -