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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
7 changes: 1 addition & 6 deletions .coderabbit.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defineConfig, type CodeRabbitContext } from "@coderabbitai/config";

// Org members and collaborators can approve their own pull requests.
const UNGATED_AUTHORS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const isGated = ({ pr }: CodeRabbitContext) => !UNGATED_AUTHORS.has(pr?.authorAssociation ?? "");

Expand All @@ -17,11 +16,7 @@ export default defineConfig((ctx) => ({
docstrings: { mode: "off" },
override_requested_reviewers_only: isGated(ctx),
},
path_filters: [
// Vendored read-only reference checkouts of upstream Effect and Alchemy
// (see scripts/lib/reference-repos.ts). Nothing imports from them.
"!.repos/**",
],
path_filters: ["!.repos/**"],
path_instructions: [
{
path: "{apps,packages,infra}/**/*.ts",
Expand Down
6 changes: 0 additions & 6 deletions .github/scripts/check-nightly-release.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ async function assertReleaseSource({ github, context, releaseChannel }) {

const isNightlyTag = (tag) => /^v.*-nightly\./.test(tag) || tag.startsWith("nightly-v");

// Newest published nightly by publication time, or undefined when none exists.
async function findLatestNightly({ github, context }) {
const releases = await github.paginate(github.rest.repos.listReleases, {
...context.repo,
Expand All @@ -51,7 +50,6 @@ async function findLatestNightly({ github, context }) {
.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0];
}

// Runs after the workflow acquires the nightly concurrency lock.
async function shouldReleaseNightly({ github, context, core, now = Date.now() }) {
const lastNightly = await findLatestNightly({ github, context });

Expand Down Expand Up @@ -81,17 +79,13 @@ async function shouldReleaseNightly({ github, context, core, now = Date.now() })
return true;
}

// Stable releases build the commit the latest nightly shipped, so the stable
// build is one nightly users already ran. Returns the nightly tag, its commit,
// and the stable version that nightly was a preview of.
async function resolveLatestNightlyCommit({ github, context, core }) {
const lastNightly = await findLatestNightly({ github, context });
if (!lastNightly) {
throw new Error("No published nightly found. Stable releases build the latest nightly commit.");
}

const tag = lastNightly.tag_name;
// repos.getCommit dereferences annotated tags, so this is the commit either way.
const { data: commit } = await github.rest.repos.getCommit({ ...context.repo, ref: tag });
const version = /^(?:nightly-)?v(\d+\.\d+\.\d+)-nightly\./.exec(tag)?.[1];
if (!version) {
Expand Down
37 changes: 9 additions & 28 deletions .github/scripts/mirror-play-apk.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ function requireEnv(name) {
return value;
}

// Service account key -> OAuth access token via a self-signed JWT grant.
async function playAccessToken(key) {
const now = Math.floor(Date.now() / 1000);
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
Expand Down Expand Up @@ -165,25 +164,15 @@ async function main() {
}
const safeName = build.versionName.replace(/[^0-9A-Za-z.]+/g, "-").replace(/^-|-$/g, "");
const apkName = `t3code-android-${channel}-${safeName}-${build.versionCode}.apk`;
const tag = outputDirectory ? undefined : requireEnv("RELEASE_TAG");
const snapshotTag = `android-beta-${build.versionCode}`;
const release = tag ? readRelease(tag) : undefined;
const snapshot = tag ? readRelease(snapshotTag) : undefined;
const hasApk = (value) =>
value &&
!value.isDraft &&
value.assets.some(
const tag = `android-beta-${build.versionCode}`;
const release = outputDirectory ? undefined : readRelease(tag);
if (
release &&
!release.isDraft &&
release.assets.some(
(asset) => asset.name === apkName && asset.state === "uploaded" && asset.size > 0,
);
const pruneBetaAssets = () => {
for (const stale of release?.assets ?? []) {
if (stale.name.endsWith(".apk") && stale.name !== apkName) {
gh("release", "delete-asset", tag, stale.name, "--yes");
}
}
};
if (hasApk(release) && hasApk(snapshot)) {
pruneBetaAssets();
)
) {
console.log(`${apkName} is already published.`);
return;
}
Expand All @@ -210,16 +199,8 @@ async function main() {
"",
"These builds have been promoted for wider testing, but may still contain bugs. The beta is optional; the store app also works with Nightly servers.",
].join("\n");
// Publish the versioned beta first, then advance the stable download page.
// The build number makes retries and unchanged hourly checks idempotent.
publishBeta(
snapshotTag,
apkPath,
notes,
`Android beta ${build.versionName} (${build.versionCode})`,
);
publishBeta(tag, apkPath, notes, "T3 Code for Android (beta)");
pruneBetaAssets();
publishBeta(tag, apkPath, notes, `Android beta ${build.versionName} (${build.versionCode})`);
} finally {
fs.rmSync(path.dirname(apkPath), { recursive: true, force: true });
}
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/mobile-android-apk.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Mobile Android APK

# Checks closed testing hourly. New beta builds get a versioned prerelease and
# update the android-beta download page. Unchanged checks publish nothing.
# Checks closed testing hourly. Each new beta gets one versioned prerelease.
# Unchanged checks publish nothing.
on:
schedule:
# Avoid minute zero, when GitHub scheduled jobs are busiest.
Expand All @@ -21,7 +21,6 @@ jobs:
contents: write
env:
PACKAGE_NAME: com.t3tools.t3code
RELEASE_TAG: android-beta
steps:
- id: credentials
name: Check for Play credentials
Expand Down
117 changes: 48 additions & 69 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -689,11 +689,8 @@ jobs:
resource_key: win32-arm64
cli_archive: true

# npm gets the same bytes as the GitHub Release: the launcher plus one
# package per CLI archive. Preview publishes too, under the `preview`
# dist-tag, which nothing resolves unless asked for by name.
publish_cli:
name: Publish CLI to npm
release:
name: Publish GitHub Release
needs:
[
preflight,
Expand All @@ -702,74 +699,13 @@ jobs:
test,
test_server,
desktop_mac_arm64,
desktop_linux_x64,
desktop_linux_arm64,
desktop_win_x64,
desktop_win_arm64,
]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=t3...
- --filter=@t3tools/scripts...

- name: Download all CLI archives
uses: actions/download-artifact@v8
with:
pattern: cli-*
merge-multiple: true
path: release-cli

- name: Build npm packages from CLI archives
run: node scripts/build-npm-platform-packages.ts --archives-dir release-cli --version "${{ needs.preflight.outputs.version }}" --output-dir npm-packages

# A dry run of every package first: an auth or scope error here (the
# @t3code org missing, a package without a trusted publisher) fails
# before anything is live, instead of after some platforms already are.
- name: Check npm publish access (dry run)
run: |
if ! node apps/server/scripts/cli.ts publish --packages-dir npm-packages --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --provenance --dry-run --verbose; then
echo "::error::npm publish --dry-run failed. Make sure the @t3code npm org exists and that t3 and every @t3code/t3-<platform> package has a trusted publisher registered for .github/workflows/release.yml (see docs/operations/release.md)." >&2
exit 1
fi

- name: Publish CLI packages
run: node apps/server/scripts/cli.ts publish --packages-dir npm-packages --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --provenance --verbose

release:
name: Publish GitHub Release
needs:
[
preflight,
desktop_mac_arm64,
desktop_mac_x64,
desktop_linux_x64,
desktop_linux_arm64,
desktop_win_x64,
desktop_win_arm64,
publish_cli,
]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' && needs.publish_cli.result == 'success' }}
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
Expand Down Expand Up @@ -957,6 +893,49 @@ jobs:
fail_on_unmatched_files: true
token: ${{ github.token }}

# The `t3` npm package is a launcher that downloads this version's CLI
# archive from the GitHub Release, so it publishes only once that release
# exists. Preview publishes too, under the `preview` dist-tag, which
# nothing resolves unless asked for by name.
publish_cli:
name: Publish CLI to npm
needs: [preflight, release]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=t3...
- --filter=@t3tools/scripts...

- name: Build the npm launcher
run: node scripts/build-npm-launcher.ts --version "${{ needs.preflight.outputs.version }}" --output-dir npm-packages

- name: Publish the npm launcher
run: |
if ! node apps/server/scripts/cli.ts publish --packages-dir npm-packages --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --provenance --verbose; then
echo "::error::npm publish failed. Make sure package t3 has a trusted publisher registered for .github/workflows/release.yml (see docs/operations/release.md)." >&2
exit 1
fi

publish_aur:
name: Publish AUR package
needs: [preflight, release]
Expand Down Expand Up @@ -1074,8 +1053,8 @@ jobs:

deploy_web:
name: Deploy hosted web app
needs: [preflight, build_web, release]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build_web.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }}
needs: [preflight, build_web, release, publish_cli]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build_web.result == 'success' && needs.release.result == 'success' && needs.publish_cli.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
env:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Architecture and its constraints: `docs/internals/overview.md`. Glossary: `docs/
- Server features are services; transports stay thin. A `ws.ts` RPC handler, HTTP route, or MCP tool decodes input, calls one service method, and maps errors. See [Effect services](docs/internals/effect-services.md).
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in `components/ui`. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` fails lint on violations. See [Web UI](docs/internals/web-ui.md).
- Inferred types over annotations. `any` is the enemy.
- Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior.
- Comments say why: a constraint, an external quirk, or an invariant the code can't show. Don't restate names or signatures, narrate steps, or record history. `t3code/no-low-value-comments` catches the mechanical cases.
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.
- If a rule here fights the task in front of you, say so loudly and get a human sign-off before breaking it.

Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/gnome-extension/captureFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import St from "gi://St";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import { captureDestinationFrame, findCaptureDestination } from "./feedbackGeometry.js";

/** A single, bounded, sender-owned flight. No live window clone or per-frame JS work. */
export class CaptureFeedback {
begin(sender, pid, snapshot, options) {
this.dispose();
Expand Down Expand Up @@ -54,7 +53,6 @@ export class CaptureFeedback {
});
}
} catch (error) {
// Optional decoration must not prevent attaching a successfully captured image.
this._clearActors();
console.warn(`T3 capture effects unavailable: ${error.message}`);
}
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/gnome-extension/captureService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// This policy is independent of Shell so lifecycle and authorization can be tested without a desktop.
export const CLIENT_NAMES = [
"com.t3tools.T3Code.SnapShot",
"com.t3tools.T3Code.Development.SnapShot",
Expand Down
4 changes: 1 addition & 3 deletions apps/desktop/scripts/build-browser-secret.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ if (hostPlatform === "linux") {
NodeFS.statSync(source).mtimeMs,
NodeFS.statSync(NodeURL.fileURLToPath(import.meta.url)).mtimeMs,
) && matchesArchitecture(output);
} catch {
/* The first build has no output yet. */
}
} catch {}
if (!current) {
let flags;
try {
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ function killChildTree(signal) {
return;
}

// Kill direct children as a final fallback in case normal shutdown leaves stragglers.
NodeChildProcess.spawnSync("pkill", [`-${signal}`, "-P", String(process.pid)], {
stdio: "ignore",
});
Expand Down
11 changes: 4 additions & 7 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron"

import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeModule from "node:module";
Expand Down Expand Up @@ -402,11 +400,10 @@ function buildMacLauncher(electronBinaryPath) {
);
patchHelperBundleInfoPlists(targetAppBundlePath);
if (isDevelopment) {
// Keep Electron's native executable inside the branded bundle. Launching the
// node_modules copy makes macOS associate the process (and Dock label) with
// Electron.app even though this bundle's Info.plist has the T3 Code name.
// Its conventional executable name also keeps Electron's default-app runtime
// in development mode instead of making app.isPackaged report true.
// Launching the node_modules copy makes macOS associate the process (and Dock label) with
// Electron.app even though this bundle's Info.plist has the T3 Code name. Its conventional
// executable name also keeps Electron's default-app runtime in development mode instead of
// making app.isPackaged report true.
writeDevelopmentEnvironmentScript();
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ describe("electron development launcher", () => {
const development = resolveMacLauncherIconPaths("/runtime", true);
const production = resolveMacLauncherIconPaths("/runtime", false);

// The source icons are real repo paths, joined for the host.
assert.match(development.sourceIconPath, /assets[\\/]dev[\\/]blueprint-macos-1024\.png$/);
assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns");
assert.match(production.sourceIconPath, /assets[\\/]prod[\\/]black-macos-1024\.png$/);
Expand Down
Loading