Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ You can also just install the desktop app. It's cooler.

Install the [desktop app from the Releases page](https://github.com/pingdotgg/t3code/releases)

## Shared history (web + desktop)

You can point T3 Code Desktop at the same server-backed history that the web UI is already using and keep one shared chat timeline between web and desktop.

Set desktop remote mode before launch:

```bash
export T3CODE_DESKTOP_REMOTE_URL="<the current web UI server endpoint>"
export T3CODE_DESKTOP_REMOTE_AUTH_TOKEN="<the auth token used by that server>"
```

Desktop will attach to that server as the source of truth (no silent fallback local history backend).
You can also configure the same shared-history mode from the desktop app Settings screen by copying the current server endpoint shown in the web UI.
See [REMOTE.md](./REMOTE.md) for full LAN/Tailscale setup.

## Some notes

We are very very early in this project. Expect bugs.
Expand Down
31 changes: 31 additions & 0 deletions REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,34 @@ Open from any device in your tailnet:
`http://<tailnet-ip>:3773`

You can also bind `--host 0.0.0.0` and connect through the Tailnet IP, but binding directly to the Tailnet IP limits exposure.

## 3) Desktop app in remote/shared-history mode

Use this when the web UI is already using the history you want and desktop should attach to that same backend.

1. Start one server (same as above) with a persistent `--state-dir` and `--auth-token`.
2. Launch the desktop app with these environment variables:

```bash
export T3CODE_DESKTOP_REMOTE_URL="<the current web UI server endpoint>"
export T3CODE_DESKTOP_REMOTE_AUTH_TOKEN="$TOKEN"
```

Then start T3 Code Desktop normally.

You can also open `Settings -> Shared History` inside the desktop app, copy the current server endpoint from the web UI, save the same URL/token there, and let the app restart itself into remote mode.

Behavior in remote mode:

- Desktop connects directly to the configured remote WebSocket/backend.
- Desktop does **not** start its own persistent local chat backend.
- History shown in desktop comes from that shared remote server state.
- On auth or connectivity failure, desktop surfaces the error and does not silently fall back to local mode.

### Protocol expectations

- `http://...` remote URL -> desktop WebSocket uses `ws://...`
- `https://...` remote URL -> desktop WebSocket uses `wss://...`
- If your remote URL already uses `ws://` or `wss://`, it is used as-is.

Set `T3CODE_DESKTOP_REMOTE_URL` to the same server endpoint the web UI is already using.
73 changes: 73 additions & 0 deletions apps/desktop/src/desktopConnectionSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";

import {
DEFAULT_DESKTOP_CONNECTION_SETTINGS,
normalizeDesktopConnectionSettings,
resolveDesktopConnectionSettingsSnapshot,
} from "./desktopConnectionSettings";

describe("normalizeDesktopConnectionSettings", () => {
it("defaults unknown values to local mode with empty strings", () => {
expect(normalizeDesktopConnectionSettings(null)).toEqual(DEFAULT_DESKTOP_CONNECTION_SETTINGS);
});

it("trims persisted values", () => {
expect(
normalizeDesktopConnectionSettings({
mode: "remote",
remoteUrl: " https://chat.example.com ",
authToken: " secret ",
}),
).toEqual({
mode: "remote",
remoteUrl: "https://chat.example.com",
authToken: "secret",
});
});
});

describe("resolveDesktopConnectionSettingsSnapshot", () => {
it("treats the default local config as an implicit default", () => {
expect(
resolveDesktopConnectionSettingsSnapshot({
saved: DEFAULT_DESKTOP_CONNECTION_SETTINGS,
savedExists: false,
environmentOverride: null,
}),
).toEqual({
source: "default",
effective: DEFAULT_DESKTOP_CONNECTION_SETTINGS,
saved: DEFAULT_DESKTOP_CONNECTION_SETTINGS,
});
});

it("prefers an environment override over saved settings", () => {
expect(
resolveDesktopConnectionSettingsSnapshot({
saved: {
mode: "local",
remoteUrl: "",
authToken: "",
},
savedExists: true,
environmentOverride: {
mode: "remote",
remoteUrl: "https://chat.example.com",
authToken: "secret",
},
}),
).toEqual({
source: "environment",
effective: {
mode: "remote",
remoteUrl: "https://chat.example.com",
authToken: "secret",
},
saved: {
mode: "local",
remoteUrl: "",
authToken: "",
},
});
});
});
104 changes: 104 additions & 0 deletions apps/desktop/src/desktopConnectionSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as FS from "node:fs";
import * as Path from "node:path";

import type {
DesktopConnectionMode,
DesktopConnectionSettings,
DesktopConnectionSettingsSnapshot,
DesktopConnectionSettingsSource,
} from "@t3tools/contracts";

const DESKTOP_CONNECTION_SETTINGS_FILENAME = "desktop-connection.json";

export const DEFAULT_DESKTOP_CONNECTION_SETTINGS: DesktopConnectionSettings = {
mode: "local",
remoteUrl: "",
authToken: "",
};

interface ReadDesktopConnectionSettingsResult {
exists: boolean;
settings: DesktopConnectionSettings;
}

function normalizeMode(value: unknown): DesktopConnectionMode {
return value === "remote" ? "remote" : "local";
}

function normalizeString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}

export function normalizeDesktopConnectionSettings(
value: Partial<DesktopConnectionSettings> | null | undefined,
): DesktopConnectionSettings {
return {
mode: normalizeMode(value?.mode),
remoteUrl: normalizeString(value?.remoteUrl),
authToken: normalizeString(value?.authToken),
};
}

export function resolveDesktopConnectionSettingsPath(stateDir: string): string {
return Path.join(stateDir, DESKTOP_CONNECTION_SETTINGS_FILENAME);
}

export function readDesktopConnectionSettings(path: string): ReadDesktopConnectionSettingsResult {
if (!FS.existsSync(path)) {
return {
exists: false,
settings: DEFAULT_DESKTOP_CONNECTION_SETTINGS,
};
}

try {
const raw = FS.readFileSync(path, "utf8");
const parsed = JSON.parse(raw) as Partial<DesktopConnectionSettings>;
return {
exists: true,
settings: normalizeDesktopConnectionSettings(parsed),
};
} catch {
return {
exists: true,
settings: DEFAULT_DESKTOP_CONNECTION_SETTINGS,
};
}
}

export function writeDesktopConnectionSettings(
path: string,
settings: DesktopConnectionSettings,
): DesktopConnectionSettings {
const normalized = normalizeDesktopConnectionSettings(settings);
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
FS.mkdirSync(Path.dirname(path), { recursive: true });
FS.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
FS.renameSync(tempPath, path);
return normalized;
}

export function resolveDesktopConnectionSettingsSnapshot(input: {
saved: DesktopConnectionSettings;
savedExists: boolean;
environmentOverride: DesktopConnectionSettings | null;
}): DesktopConnectionSettingsSnapshot {
let source: DesktopConnectionSettingsSource = "default";
let effective = input.saved;

if (input.environmentOverride) {
source = "environment";
effective = input.environmentOverride;
} else if (input.savedExists || input.saved.mode === "remote") {
source = "settings";
}

return {
source,
effective,
saved: input.saved,
};
}
Loading