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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ chmod +x T4-Code-0.1.32-linux-x86_64.AppImage
3. Open T4 Code normally. The release workflow verifies the pinned publisher, hardened runtime, secure timestamp, Apple notarization, stapled ticket, and Gatekeeper acceptance before publication.
4. To start terminal or CMUX sessions that can hand off to T4 Code, open **Settings → Hosts** and choose **Install t4-omp**. This adds `~/.local/bin/t4-omp` and leaves any existing `omp` command unchanged.

Upgrade note for v0.1.32 only: its DMG named the bundle `t4-code.app`. Before installing a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the affected-release range

The lowercase bundle was not unique to v0.1.32: the top-level executableName: "t4-code" was already present in the first signed v0.1.24 configuration and remained through v0.1.31 with the same electron-builder version. Users upgrading from any of those releases will have /Applications/t4-code.app, but this v0.1.32-only instruction tells them the cleanup does not apply, leaving the old app alongside the corrected one. Broaden the migration note in the README, site docs, and release notes to cover all affected releases.

Useful? React with 👍 / 👎.

newer release, quit T4 Code and move `/Applications/t4-code.app` to the Trash, then copy
`T4 Code.app` into `/Applications`. Removing the old application bundle does not remove
your T4 Code settings or sessions.

## What the app does

- **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.
Expand Down
4 changes: 4 additions & 0 deletions apps/site/src/docs/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ const install: DocTopic = {
kind: "note",
text: `The v${RELEASE_VERSION} macOS build is signed with the project's pinned Developer ID identity and notarized by Apple. The release workflow verifies both downloadable formats with Gatekeeper before publication.`,
},
{
kind: "note",
text: "Upgrade note for v0.1.32 only: its DMG named the bundle `t4-code.app`. Before installing a newer release, quit T4 Code and move `/Applications/t4-code.app` to the Trash, then copy `T4 Code.app` into Applications. Removing the old application bundle does not remove your T4 Code settings or sessions.",
},
{ kind: "h3", id: "install-gatekeeper", text: "First launch on macOS" },
{ kind: "p", text: "After copying the app into Applications, open T4 Code normally." },
{
Expand Down
7 changes: 7 additions & 0 deletions docs/CURRENT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ the session lock.
Pull-request CI now treats client runtime changes as bridge-boundary changes and runs both the
current-runtime and pinned-legacy continuity gates.

## macOS application identity

The v0.1.32 DMG accidentally named its bundle `t4-code.app` instead of the documented
`T4 Code.app`. Before installing a newer release, quit T4 Code and move
`/Applications/t4-code.app` to the Trash, then copy the new `T4 Code.app` into Applications.
Removing the old application bundle does not remove T4 Code settings or sessions.

## A session rail built for large libraries

T4 Code v0.1.32 makes a large session library easier to navigate. The rail now supports text search, activity filters, newest/oldest sorting, grouped and flat layouts, collapsible project folders, and saved display preferences. Those controls follow the Codex desktop organization model while keeping OMP as the source of truth.
Expand Down
2 changes: 1 addition & 1 deletion electron-builder.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const linuxUpdatePublish = {
const config = {
appId: "com.lycaonsolutions.t4code",
productName: "T4 Code",
executableName: "t4-code",
electronVersion: "41.5.0",
artifactName: "T4-Code-${version}-${os}-${arch}.${ext}",
directories: {
Expand All @@ -37,6 +36,7 @@ const config = {
],
protocols: [{ name: "T4 Code", schemes: ["t4-code"] }],
linux: {
executableName: "t4-code",
category: "Development",
icon: "apps/desktop/build/icons",
publish: [linuxUpdatePublish],
Expand Down
15 changes: 13 additions & 2 deletions scripts/inspect-macos-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { extname, join, resolve } from "node:path";
import { basename, extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u;
const TAG_PATTERN = /^v\d+\.\d+\.\d+$/u;
const MACOS_APP_BUNDLE_NAME = "T4 Code.app";

function requireString(value, label) {
if (typeof value !== "string" || value.length === 0) {
Expand Down Expand Up @@ -132,13 +133,23 @@ function run(command, args, options = {}) {
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
}

export function validateMacosAppBundleName(appPath) {
const actualName = basename(appPath);
if (actualName !== MACOS_APP_BUNDLE_NAME) {
throw new Error(
`macOS release app must be named ${MACOS_APP_BUNDLE_NAME}; found ${actualName}`,
);
}
return appPath;
}

function findSingleApp(directory) {
const apps = readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && extname(entry.name).toLowerCase() === ".app")
.map((entry) => join(directory, entry.name));
if (apps.length !== 1)
throw new Error(`expected exactly one top-level macOS app; found ${apps.length}`);
return apps[0];
return validateMacosAppBundleName(apps[0]);
}

function inspectApp(appPath, contract, certificatePrefix) {
Expand Down
14 changes: 13 additions & 1 deletion scripts/inspect-macos-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { test } from "node:test";
import {
parseCodesignDisplay,
validateMacosLibraryValidationBoundary,
validateMacosAppBundleName,
validateMacosIdentityContract,
validateMacosSignatureReport,
} from "./inspect-macos-release.mjs";
Expand All @@ -14,7 +15,7 @@ const identity = JSON.parse(
);

const displayFixture = `
Executable=/Applications/T4 Code.app/Contents/MacOS/t4-code
Executable=/Applications/T4 Code.app/Contents/MacOS/T4 Code
Identifier=com.lycaonsolutions.t4code
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20500 size=640 flags=0x10000(runtime) hashes=10+7 location=embedded
Expand All @@ -25,6 +26,17 @@ Timestamp=Jul 18, 2026 at 8:30:00 PM
TeamIdentifier=WJLM3D3DK6
`;

test("macOS release archives preserve the canonical application bundle name", () => {
assert.equal(
validateMacosAppBundleName("/Volumes/T4 Code/T4 Code.app"),
"/Volumes/T4 Code/T4 Code.app",
);
assert.throws(
() => validateMacosAppBundleName("/Volumes/T4 Code/t4-code.app"),
/must be named T4 Code\.app; found t4-code\.app/u,
);
});

test("macOS release identity pins the public Developer ID contract", () => {
assert.doesNotThrow(() => validateMacosIdentityContract(identity));
assert.equal(identity.firstSignedReleaseTag, "v0.1.24");
Expand Down
3 changes: 3 additions & 0 deletions scripts/packaging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ function androidReleaseFixture(overrides = {}) {
test("builder config keeps release contract", () => {
assert.equal(config.appId, "com.lycaonsolutions.t4code");
assert.equal(config.productName, "T4 Code");
assert.equal(config.executableName, undefined);
assert.equal(config.linux.executableName, "t4-code");
assert.equal(config.mac.executableName, undefined);
assert.equal(config.asar, true);
assert.deepEqual(config.protocols[0].schemes, ["t4-code"]);
assert.equal(config.linux.category, "Development");
Expand Down
Loading