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.desktophttps://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.desktophttps://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.
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.desktophttps://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 ? (
-