Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ body:
id: version
attributes:
label: T4 Code version
placeholder: "0.1.9"
placeholder: "0.1.10"
validations:
required: true
- type: dropdown
Expand Down
31 changes: 1 addition & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,36 +356,7 @@ jobs:
tag_name: ${{ env.RELEASE_TAG }}
generate_release_notes: true
fail_on_unmatched_files: true
body: |
## Android app

This release includes a signed Android APK for direct installation. It is a thin client: the T4 Code interface is bundled in the APK, while OMP sessions and runtime state remain on the host. On first run, enter the host's HTTPS Tailscale address and complete the normal T4 pairing flow.

Android device credentials are encrypted with an app-specific Android Keystore key. The app does not contain an OMP appserver and does not require a public route; the phone and host should use the same tailnet.

## Browser and iOS

The installable Tailnet web app remains available. iOS TestFlight is coming soon; there is no iOS binary in this release.

## Connection security

The Tailnet gateway accepts only the configured HTTPS web origin and the exact native app origins. It rejects wildcard, opaque, and untrusted origins. Keep the route on Tailscale Serve and do not enable Funnel.

## Install notes

The Android APK is signed and supports Android 7.0 or later. Linux packages are built for x86_64. macOS packages are built for Apple Silicon.

## Runtime compatibility

This release vendors app-wire 0.5.2 from public integration commit [5d4315ee](https://github.com/lyc-aon/oh-my-pi/commit/5d4315eea317260fec030e2b4726f10fed0cd5f6) and was verified with OMP 16.5.0 built from [d4a0b934](https://github.com/lyc-aon/oh-my-pi/commit/d4a0b9344e1796c0e56041cfeea3431a8a728e61), tagged [t4code-16.5.0-appserver-3](https://github.com/lyc-aon/oh-my-pi/tree/t4code-16.5.0-appserver-3). The integration is based on the official upstream [v16.5.0 tag](https://github.com/can1357/oh-my-pi/tree/v16.5.0), commit [3047c27c](https://github.com/can1357/oh-my-pi/commit/3047c27c332c5629c8e063283d349384c10c9a56). It adds bounded growing-session replay, complete session event projection, catalog-backed session lifecycle management, ordered remote outbound frames, cross-client control-state convergence, terminal streaming-state settlement, and restart-safe session teardown. Official upstream OMP v16.5.0 has no `appserver` command and cannot host T4 Code. The verified runtime is built normally from the public `lyc-aon/oh-my-pi` source; it does not require private home-directory files, an auth broker, or a custom Codex CLI fork.

The macOS build is unsigned and unnotarized. Gatekeeper will block the first launch. After copying T4 Code to Applications, run:

```sh
xattr -dr com.apple.quarantine "/Applications/T4 Code.app"
```

Verify downloads with `SHA256SUMS.txt`.
body_path: docs/CURRENT_RELEASE_NOTES.md
files: |
artifacts/T4-Code-*.apk
artifacts/T4-Code-*.deb
Expand Down
38 changes: 18 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,35 @@ T4 Code is a free, open-source (MIT) desktop app for [Oh My Pi](https://github.c

![T4 Code main window](docs/assets/t4-code-main.png)

[**Download v0.1.9**](https://github.com/LycaonLLC/t4-code/releases/tag/v0.1.9) · [**Docs**](https://t4code.net/docs) · [**Get the source**](#build-from-source)
[**Download v0.1.10**](https://github.com/LycaonLLC/t4-code/releases/tag/v0.1.10) · [**Docs**](https://t4code.net/docs) · [**Get the source**](#build-from-source)

## Requirements

T4 Code needs an OMP build with desktop appserver support. For v0.1.9, use the public integration build below.
T4 Code needs an OMP build with desktop appserver support. For v0.1.10, use the public integration build below.

T4 Code v0.1.9 was verified with OMP 16.5.0 built from [`d4a0b934`](https://github.com/lyc-aon/oh-my-pi/commit/d4a0b9344e1796c0e56041cfeea3431a8a728e61), tagged [`t4code-16.5.0-appserver-3`](https://github.com/lyc-aon/oh-my-pi/tree/t4code-16.5.0-appserver-3). That public integration is based on the official upstream [`v16.5.0`](https://github.com/can1357/oh-my-pi/tree/v16.5.0) tag at [`3047c27c`](https://github.com/can1357/oh-my-pi/commit/3047c27c332c5629c8e063283d349384c10c9a56). It adds bounded large-session replay, complete desktop runtime events, catalog-backed session management, ordered remote delivery, cross-client control-state convergence, terminal streaming-state settlement, and restart-safe session teardown. The official upstream v16.5.0 tag has no `appserver` command, so it cannot host T4 Code. The verified runtime is a normal build from the public `lyc-aon/oh-my-pi` source; T4 Code does not depend on private home-directory files, an auth broker, or a custom Codex CLI fork. T4 Code vendors `@oh-my-pi/app-wire` 0.5.2 from integration commit [`5d4315ee`](https://github.com/lyc-aon/oh-my-pi/commit/5d4315eea317260fec030e2b4726f10fed0cd5f6), source tree `713688e8099d4553a0a30b1bf415a7cffb5963f4`.
T4 Code v0.1.10 was verified with OMP 16.5.0 built from [`1ada5fc2`](https://github.com/lyc-aon/oh-my-pi/commit/1ada5fc2f0d6f9026d373cd25e004b974437651e), tagged [`t4code-16.5.0-appserver-4`](https://github.com/lyc-aon/oh-my-pi/tree/t4code-16.5.0-appserver-4). That public integration is based on the official upstream [`v16.5.0`](https://github.com/can1357/oh-my-pi/tree/v16.5.0) tag at [`3047c27c`](https://github.com/can1357/oh-my-pi/commit/3047c27c332c5629c8e063283d349384c10c9a56). It adds bounded large-session replay and terminal events, complete desktop runtime projection, catalog-backed session management, deterministic failed-worker reaping, recoverable crash state, settled close state, ordered remote delivery, cross-client control convergence, and restart-safe session teardown. The official upstream v16.5.0 tag has no `appserver` command, so it cannot host T4 Code. The verified runtime is a normal build from the public `lyc-aon/oh-my-pi` source; T4 Code does not depend on private home-directory files, an auth broker, or a custom Codex CLI fork. T4 Code vendors `@oh-my-pi/app-wire` 0.5.3 from integration commit [`1ada5fc2`](https://github.com/lyc-aon/oh-my-pi/commit/1ada5fc2f0d6f9026d373cd25e004b974437651e), source tree `4961ea9c522a3bbf9a9900424dd475a48148c729`.

| Platform | Arch | Package |
| --- | --- | --- |
| Android | arm64, armv7, x86_64 | `.apk` (**signed**) |
| Linux | x86_64 | `.deb`, AppImage |
| macOS | Apple Silicon (arm64) | `.dmg`, `.zip` (**unsigned, see below**) |

No Windows build and no Intel Mac build in v0.1.9. The iOS TestFlight build is coming soon.
No Windows build and no Intel Mac build in v0.1.10. The iOS TestFlight build is coming soon.

## What changed in v0.1.9
## What changed in v0.1.10

- Android now has a signed, installable APK. It is a thin client: the UI ships inside the app, while sessions and runtime state stay on your T4 Code host.
- First run accepts an HTTPS Tailscale hostname, checks the secure WebSocket route, and then uses the normal T4 pairing flow.
- Android device credentials are encrypted with an app-specific Android Keystore key. Only the non-secret Tailnet hostname is kept in web storage.
- The Tailnet gateway accepts the exact Capacitor Android and iOS origins while continuing to reject wildcard, opaque, and untrusted origins.
- The connection screen and titlebar controls are sized for small phone displays. A user can clear the saved host and pair again without reinstalling.
- The browser-installable PWA remains available, and the iOS download slot now reports that TestFlight is coming soon without advertising a build that is not published.
- Host command errors now reach the composer with their reason and redacted details. T4 Code keeps the draft in place so you can respond to the specific problem.
- When prompt delivery has an unknown outcome, the composer asks you to check the transcript before retrying. The retained draft stays ready after that check.
- A new **Terminate runtime** action closes a stuck session worker. T4 Code waits for the host to report the session closed and idle, then refreshes the session list so archive and delete can proceed.
- The matching OMP integration bounds terminal events, reaps failed workers, makes crash-only sessions recoverable, and clears pending work during an explicit close.

## Install

### Android

1. On the Android phone, sign in to Tailscale with an account that can reach the T4 Code host.
2. Download [`T4-Code-0.1.9-android.apk`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.9/T4-Code-0.1.9-android.apk).
2. Download [`T4-Code-0.1.10-android.apk`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.10/T4-Code-0.1.10-android.apk).
3. If Android asks, allow your browser or file manager to install unknown apps, then install the APK.
4. Open T4 Code and enter the host's HTTPS Tailscale address, including its port.

Expand All @@ -43,26 +41,26 @@ The APK does not contain an appserver or expose one to the public internet. It c
### Linux (Debian/Ubuntu)

```sh
wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.9/T4-Code-0.1.9-linux-amd64.deb
sudo apt install ./T4-Code-0.1.9-linux-amd64.deb
wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.10/T4-Code-0.1.10-linux-amd64.deb
sudo apt install ./T4-Code-0.1.10-linux-amd64.deb
```

Use `apt install` rather than `dpkg -i` so system dependencies resolve automatically.

### Linux (AppImage)

```sh
wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.9/T4-Code-0.1.9-linux-x86_64.AppImage
chmod +x T4-Code-0.1.9-linux-x86_64.AppImage
./T4-Code-0.1.9-linux-x86_64.AppImage
wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.10/T4-Code-0.1.10-linux-x86_64.AppImage
chmod +x T4-Code-0.1.10-linux-x86_64.AppImage
./T4-Code-0.1.10-linux-x86_64.AppImage
```

### macOS (Apple Silicon)

> [!WARNING]
> **The macOS v0.1.9 build is unsigned and unnotarized.** Apple has not signed or notarized it, so Gatekeeper can report a "damaged" app or an unidentified developer. Only continue if you trust the release from this repository. You can always build from source instead.
> **The macOS v0.1.10 build is unsigned and unnotarized.** Apple has not signed or notarized it, so Gatekeeper can report a "damaged" app or an unidentified developer. Only continue if you trust the release from this repository. You can always build from source instead.

1. Download [`T4-Code-0.1.9-mac-arm64.dmg`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.9/T4-Code-0.1.9-mac-arm64.dmg) (or [`T4-Code-0.1.9-mac-arm64.zip`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.9/T4-Code-0.1.9-mac-arm64.zip)).
1. Download [`T4-Code-0.1.10-mac-arm64.dmg`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.10/T4-Code-0.1.10-mac-arm64.dmg) (or [`T4-Code-0.1.10-mac-arm64.zip`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.10/T4-Code-0.1.10-mac-arm64.zip)).
2. Drag `T4 Code.app` into `/Applications`.
3. If Gatekeeper blocks the app and you choose to proceed, remove the quarantine attributes from the copied app bundle:

Expand All @@ -74,7 +72,7 @@ chmod +x T4-Code-0.1.9-linux-x86_64.AppImage

## What the app does

- **Sessions.** Browse sessions grouped by their working folder, create new ones, and switch between them. Rename, archive, restore, or permanently delete a session from its menu. Recently used sessions stay warm, so switching back is instant and nothing is replayed twice.
- **Sessions.** Browse sessions grouped by their working folder, create new ones, and switch between them. Rename, terminate a stuck runtime, archive, restore, or permanently delete a session from its menu. Recently used sessions stay warm, so switching back is instant and nothing is replayed twice.
- **Composer.** Send prompts, use slash commands (`/model`, `/compact`, `/retry`, `/review`, `/terminal`, and more), and change the session's model, thinking level, or fast mode inline.
- **Panes.** Watch subagents (and cancel them), apply reviews, browse and preview files on the host, and attach to live terminals with real keyboard input and resize.
- **Settings.** Edit host settings over the wire. Drafts stage locally and only apply when the host confirms; a dropped connection never silently writes anything.
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ We read every report and will reply to tell you what happens next. This is a sma

- T4 Code is a desktop client. The OMP runtime is a separate project; runtime vulnerabilities belong at <https://github.com/can1357/oh-my-pi>.
- Pairing credentials are encrypted with the OS keychain via Electron `safeStorage`. Reports about credential handling, the pairing flow, or the `t4-code://` deep-link handler are especially welcome.
- The macOS v0.1.9 build is unsigned and unnotarized; that is a known, disclosed limitation, not a vulnerability report. Removing `com.apple.quarantine` changes Gatekeeper handling but does not sign, notarize, or verify the app.
- The macOS v0.1.10 build is unsigned and unnotarized; that is a known, disclosed limitation, not a vulnerability report. Removing `com.apple.quarantine` changes Gatekeeper handling but does not sign, notarize, or verify the app.
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ T3 Code is selectively referenced for future ports from https://github.com/pingd

## Oh My Pi

Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.5.2` package is packed from the public `lyc-aon/oh-my-pi` integration commit `5d4315eea317260fec030e2b4726f10fed0cd5f6`, source tree `713688e8099d4553a0a30b1bf415a7cffb5963f4`; tarball SHA-256 `fb9b608d7a2245001c334808475fabd9e05603729f48cff88b21a5165b4fb63a`; golden corpus SHA-256 `36811f39241c6c491c967a8f969f14c43431366289750538a40893d0dc267324`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.
Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.5.3` package is packed from the public `lyc-aon/oh-my-pi` integration commit `1ada5fc2f0d6f9026d373cd25e004b974437651e`, source tree `4961ea9c522a3bbf9a9900424dd475a48148c729`; tarball SHA-256 `5ae8833b963a6a56b65a9e2184358906a8717a6c8f57617875a91d4771791050`; golden corpus SHA-256 `36811f39241c6c491c967a8f969f14c43431366289750538a40893d0dc267324`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.

## Oh My Pi icon

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t4-code/desktop",
"version": "0.1.9",
"version": "0.1.10",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src/target-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createOmpClient, isConfirmationDecisionConsumed, OmpClientError, type CommandIntent, type CursorStore, type OmpClient, type PublicServerFrame } from "@t4-code/client";
import type { CommandResult, ConnectionStateEvent, RuntimeErrorEvent } from "@t4-code/protocol/desktop-ipc";
import { commandResultError, type CommandResult, type ConnectionStateEvent, type RuntimeErrorEvent } from "@t4-code/protocol/desktop-ipc";
import type { ConfirmRequest, ConfirmResult, TerminalCloseRequest, TerminalInputRequest, TerminalResizeRequest, TerminalResult } from "@t4-code/protocol/desktop-ipc";
import { ADDITIVE_FEATURES, DEVICE_CAPABILITIES, type DeviceCapability } from "@t4-code/protocol";
import type { PairOkFrame, WelcomeFrame } from "@t4-code/protocol";
Expand Down Expand Up @@ -65,6 +65,12 @@ function safeError(error: unknown): { readonly code: RuntimeErrorEvent["code"];
return { code: "transport", message: "target operation failed" };
}

function safePublicFrame(frame: PublicServerFrame): PublicServerFrame {
if (frame.type !== "response" || frame.error === undefined) return frame;
const error = commandResultError(frame.error) ?? { code: "internal", message: "command failed" };
return { ...frame, error };
}

export class DesktopTargetManager {
private readonly cursorStore: CursorStore;
private readonly cursorStoreFactory: (targetId: string) => CursorStore;
Expand Down Expand Up @@ -175,12 +181,14 @@ export class DesktopTargetManager {
const generation = runtime.generation;
try {
const result = await runtime.client.command(intent);
const error = commandResultError(result.error);
return {
targetId,
requestId: String(result.requestId),
commandId: String(result.commandId),
accepted: result.ok,
...(result.result === undefined ? {} : { result: result.result }),
...(error === undefined ? {} : { error }),
};
} catch (error) {
if (this.generations.get(targetId) !== generation && error instanceof OmpClientError && error.code === "closed")
Expand Down Expand Up @@ -324,7 +332,7 @@ export class DesktopTargetManager {
cursorStore: this.cursorStoreFactory(targetId),
capabilities: requestedCapabilities,
requestedFeatures: ADDITIVE_FEATURES,
client: { name: "T4 Code", version: "0.1.9", build: "desktop", platform: process.platform },
client: { name: "T4 Code", version: "0.1.10", build: "desktop", platform: process.platform },
reconnect: { attemptCap: 12, baseMs: 250, maxMs: 10_000 },
};
const client = createOmpClient(clientOptions);
Expand All @@ -333,7 +341,7 @@ export class DesktopTargetManager {
client.onFrame((frame) => {
if (this.generations.get(targetId) !== generation) return;
if (frame.type === "welcome") this.latestWelcomes.set(targetId, frame);
this.events.onFrame(targetId, frame);
this.events.onFrame(targetId, safePublicFrame(frame));
});
client.onState((snapshot) => {
if (this.generations.get(targetId) !== generation) return;
Expand Down
Loading
Loading