diff --git a/favicon.svg b/favicon.svg
deleted file mode 100644
index 61b48db5af1..00000000000
--- a/favicon.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts
index d5ec668deab..fd14e0877b8 100644
--- a/packages/contracts/src/index.ts
+++ b/packages/contracts/src/index.ts
@@ -19,6 +19,7 @@ export * from "./git.ts";
export * from "./vcs.ts";
export * from "./sourceControl.ts";
export * from "./orchestration.ts";
+export * from "./t3ProjectFile.ts";
export * from "./editor.ts";
export * from "./project.ts";
export * from "./filesystem.ts";
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
index b01df310062..e1ddbdbd0cc 100644
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -117,6 +117,7 @@ export type ModelSelection = typeof ModelSelection.Type;
export const RuntimeMode = Schema.Literals([
"approval-required",
"auto-accept-edits",
+ "auto",
"full-access",
]);
export type RuntimeMode = typeof RuntimeMode.Type;
diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts
index 001daf1b239..e8eaf723e17 100644
--- a/packages/contracts/src/settings.test.ts
+++ b/packages/contracts/src/settings.test.ts
@@ -33,6 +33,22 @@ describe("ClientSettings word wrap", () => {
});
});
+describe("ClientSettings glass opacity", () => {
+ it("defaults to a readable translucent surface", () => {
+ expect(decodeClientSettings({}).glassOpacity).toBe(80);
+ });
+
+ it.each([39, 101, 72.5])("rejects an invalid glass opacity: %s", (value) => {
+ expect(() => decodeClientSettings({ glassOpacity: value })).toThrow();
+ expect(() => decodeClientSettingsPatch({ glassOpacity: value })).toThrow();
+ });
+
+ it.each([40, 75, 100])("accepts a glass opacity within the supported range: %s", (value) => {
+ expect(decodeClientSettings({ glassOpacity: value }).glassOpacity).toBe(value);
+ expect(decodeClientSettingsPatch({ glassOpacity: value }).glassOpacity).toBe(value);
+ });
+});
+
describe("ClientSettings sidebar v2", () => {
it("defaults the beta off with a three-day auto-settle threshold", () => {
const settings = decodeClientSettings({});
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 2f3c1aafe69..06f7de3db67 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -48,6 +48,16 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check(
);
export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type;
export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3;
+export const MIN_GLASS_OPACITY = 40;
+export const MAX_GLASS_OPACITY = 100;
+export const GlassOpacity = Schema.Int.check(
+ Schema.isBetween({
+ minimum: MIN_GLASS_OPACITY,
+ maximum: MAX_GLASS_OPACITY,
+ }),
+);
+export type GlassOpacity = typeof GlassOpacity.Type;
+export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80;
export const ClientSettingsSchema = Schema.Struct({
autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
@@ -57,6 +67,9 @@ export const ClientSettingsSchema = Schema.Struct({
Schema.withDecodingDefault(Effect.succeed([])),
),
diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
+ glassOpacity: GlassOpacity.pipe(
+ Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)),
+ ),
// Model favorites. Historically keyed by provider kind, now
// widened to `ProviderInstanceId` so users can favorite a specific model
// on a custom provider instance (e.g. "Codex Personal · gpt-5") without
@@ -560,6 +573,7 @@ export const ClientSettingsPatch = Schema.Struct({
confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
+ glassOpacity: Schema.optionalKey(GlassOpacity),
favorites: Schema.optionalKey(
Schema.Array(
Schema.Struct({
diff --git a/packages/contracts/src/t3ProjectFile.test.ts b/packages/contracts/src/t3ProjectFile.test.ts
new file mode 100644
index 00000000000..aadafa10d99
--- /dev/null
+++ b/packages/contracts/src/t3ProjectFile.test.ts
@@ -0,0 +1,55 @@
+import * as Schema from "effect/Schema";
+import { describe, expect, it } from "vite-plus/test";
+
+import { T3ProjectFile } from "./t3ProjectFile.ts";
+
+const decode = Schema.decodeUnknownSync(T3ProjectFile);
+
+describe("T3ProjectFile", () => {
+ it("decodes a full project file", () => {
+ const decoded = decode({
+ $schema: "https://t3.codes/schema/t3.json",
+ iconPath: "assets/logo.svg",
+ scripts: [
+ {
+ name: "Dev",
+ command: "pnpm dev",
+ icon: "play",
+ runOnWorktreeCreate: false,
+ previewUrl: "http://localhost:3000",
+ autoOpenPreview: true,
+ },
+ { name: "Test", command: "pnpm test" },
+ ],
+ });
+
+ expect(decoded.iconPath).toBe("assets/logo.svg");
+ expect(decoded.scripts).toHaveLength(2);
+ expect(decoded.scripts?.[1]).toEqual({ name: "Test", command: "pnpm test" });
+ });
+
+ it("decodes an empty object and ignores unknown fields", () => {
+ expect(decode({})).toEqual({});
+ expect(decode({ futureField: true })).toEqual({});
+ });
+
+ it("trims icon paths and script fields", () => {
+ const decoded = decode({
+ iconPath: " assets/logo.svg ",
+ scripts: [{ name: " Dev ", command: " pnpm dev " }],
+ });
+
+ expect(decoded.iconPath).toBe("assets/logo.svg");
+ expect(decoded.scripts?.[0]).toEqual({ name: "Dev", command: "pnpm dev" });
+ });
+
+ it("rejects scripts without a command", () => {
+ expect(() => decode({ scripts: [{ name: "Dev" }] })).toThrow();
+ });
+
+ it("rejects unknown script icons", () => {
+ expect(() =>
+ decode({ scripts: [{ name: "Dev", command: "pnpm dev", icon: "rocket" }] }),
+ ).toThrow();
+ });
+});
diff --git a/packages/contracts/src/t3ProjectFile.ts b/packages/contracts/src/t3ProjectFile.ts
new file mode 100644
index 00000000000..755e3f73928
--- /dev/null
+++ b/packages/contracts/src/t3ProjectFile.ts
@@ -0,0 +1,88 @@
+import * as Schema from "effect/Schema";
+import * as SchemaTransformation from "effect/SchemaTransformation";
+
+import { ProjectScriptIcon } from "./orchestration.ts";
+
+/** File name of the checked-in T3 project file, resolved at the workspace root. */
+export const T3_PROJECT_FILE_NAME = "t3.json";
+
+/** Public URL of the published JSON Schema for {@link T3ProjectFile}. */
+export const T3_PROJECT_FILE_SCHEMA_URL = "https://t3.codes/schema/t3.json";
+
+const T3_PROJECT_FILE_PATH_MAX_LENGTH = 512;
+const T3_PROJECT_FILE_MAX_SCRIPTS = 50;
+
+// Annotations go on the encoded (string) side so they survive into the
+// published JSON Schema; decoding still trims and re-validates non-emptiness.
+const trimmedNonEmpty = (annotations: { readonly description: string }, maxLength?: number) => {
+ const annotated = Schema.String.annotate(annotations);
+ const encoded =
+ maxLength === undefined
+ ? annotated.check(Schema.isNonEmpty())
+ : annotated.check(Schema.isNonEmpty(), Schema.isMaxLength(maxLength));
+ return encoded.pipe(Schema.decodeTo(encoded, SchemaTransformation.trim()));
+};
+
+export const T3ProjectFileScript = Schema.Struct({
+ name: trimmedNonEmpty({
+ description: "Display name for the script, shown in the T3 Code scripts menu.",
+ }),
+ command: trimmedNonEmpty({
+ description: "Shell command executed in a T3 Code terminal at the project root.",
+ }),
+ icon: Schema.optionalKey(
+ ProjectScriptIcon.annotate({
+ description: 'Icon shown next to the script in the scripts menu. Defaults to "play".',
+ }),
+ ),
+ runOnWorktreeCreate: Schema.optionalKey(
+ Schema.Boolean.annotate({
+ description:
+ "When true, the script runs automatically after a worktree is created for a new thread.",
+ }),
+ ),
+ previewUrl: Schema.optionalKey(
+ trimmedNonEmpty({
+ description:
+ "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build.",
+ }),
+ ),
+ autoOpenPreview: Schema.optionalKey(
+ Schema.Boolean.annotate({
+ description:
+ "When true, automatically open the preview panel at `previewUrl` the moment the script starts.",
+ }),
+ ),
+}).annotate({
+ description: "A project script that team members can import into T3 Code.",
+});
+export type T3ProjectFileScript = typeof T3ProjectFileScript.Type;
+
+export const T3ProjectFile = Schema.Struct({
+ $schema: Schema.optionalKey(
+ Schema.String.annotate({
+ description: `URL of the JSON Schema for this file, typically "${T3_PROJECT_FILE_SCHEMA_URL}".`,
+ }),
+ ),
+ iconPath: Schema.optionalKey(
+ trimmedNonEmpty(
+ {
+ description:
+ 'Workspace-relative path to the project icon (e.g. "assets/logo.svg"). Checked before T3 Code\'s built-in icon locations.',
+ },
+ T3_PROJECT_FILE_PATH_MAX_LENGTH,
+ ),
+ ),
+ scripts: Schema.optionalKey(
+ Schema.Array(T3ProjectFileScript)
+ .annotate({
+ description: "Project scripts shared with everyone who opens this repository in T3 Code.",
+ })
+ .check(Schema.isMaxLength(T3_PROJECT_FILE_MAX_SCRIPTS)),
+ ),
+}).annotate({
+ title: "T3 project file",
+ description:
+ "Checked-in project configuration for T3 Code (t3.json at the repository root). See https://t3.codes for documentation.",
+});
+export type T3ProjectFile = typeof T3ProjectFile.Type;
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 9c45753cd8e..7d0cc5eb820 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -87,6 +87,10 @@
"types": "./src/projectScripts.ts",
"import": "./src/projectScripts.ts"
},
+ "./t3ProjectFile": {
+ "types": "./src/t3ProjectFile.ts",
+ "import": "./src/t3ProjectFile.ts"
+ },
"./orchestrationTiming": {
"types": "./src/orchestrationTiming.ts",
"import": "./src/orchestrationTiming.ts"
diff --git a/packages/shared/src/t3ProjectFile.test.ts b/packages/shared/src/t3ProjectFile.test.ts
new file mode 100644
index 00000000000..7ca1f72eb09
--- /dev/null
+++ b/packages/shared/src/t3ProjectFile.test.ts
@@ -0,0 +1,69 @@
+import * as Schema from "effect/Schema";
+import { describe, expect, it } from "vite-plus/test";
+
+import { buildT3ProjectFileJsonSchema, T3ProjectFileFromJson } from "./t3ProjectFile.ts";
+
+const decodeJson = Schema.decodeUnknownSync(T3ProjectFileFromJson);
+
+describe("buildT3ProjectFileJsonSchema", () => {
+ it("emits a draft 2020-12 schema with the published $id", () => {
+ const schema = buildT3ProjectFileJsonSchema();
+
+ expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
+ expect(schema.$id).toBe("https://t3.codes/schema/t3.json");
+ expect(schema.type).toBe("object");
+ expect(schema.additionalProperties).toBe(false);
+ });
+
+ it("documents every supported field", () => {
+ const schema = buildT3ProjectFileJsonSchema() as {
+ properties: Record<
+ string,
+ {
+ description?: string;
+ items?: { properties: Record
; required: ReadonlyArray };
+ }
+ >;
+ required?: ReadonlyArray;
+ };
+
+ expect(Object.keys(schema.properties).sort()).toEqual(["$schema", "iconPath", "scripts"]);
+ expect(schema.required).toBeUndefined();
+ expect(schema.properties.iconPath?.description).toContain("Workspace-relative path");
+
+ const script = schema.properties.scripts?.items;
+ expect(script?.required).toEqual(["name", "command"]);
+ expect(Object.keys(script?.properties ?? {}).sort()).toEqual([
+ "autoOpenPreview",
+ "command",
+ "icon",
+ "name",
+ "previewUrl",
+ "runOnWorktreeCreate",
+ ]);
+ });
+
+ it("stays JSON-serializable", () => {
+ const schema = buildT3ProjectFileJsonSchema();
+ expect(JSON.parse(JSON.stringify(schema))).toEqual(schema);
+ });
+});
+
+describe("T3ProjectFileFromJson", () => {
+ it("decodes lenient JSONC with comments and trailing commas", () => {
+ const decoded = decodeJson(`{
+ // team scripts
+ "iconPath": "assets/logo.svg",
+ "scripts": [
+ { "name": "Dev", "command": "pnpm dev", },
+ ],
+ }`);
+
+ expect(decoded.iconPath).toBe("assets/logo.svg");
+ expect(decoded.scripts?.[0]).toEqual({ name: "Dev", command: "pnpm dev" });
+ });
+
+ it("fails on malformed JSON", () => {
+ expect(() => decodeJson("{ not json")).toThrow();
+ });
+});
diff --git a/packages/shared/src/t3ProjectFile.ts b/packages/shared/src/t3ProjectFile.ts
new file mode 100644
index 00000000000..348c3fc4b99
--- /dev/null
+++ b/packages/shared/src/t3ProjectFile.ts
@@ -0,0 +1,30 @@
+import * as Schema from "effect/Schema";
+
+import { T3ProjectFile, T3_PROJECT_FILE_SCHEMA_URL } from "@t3tools/contracts";
+
+import { fromLenientJson } from "./schemaJson.ts";
+
+/**
+ * Codec between the raw `t3.json` file contents (lenient JSONC string) and the
+ * decoded {@link T3ProjectFile}.
+ */
+export const T3ProjectFileFromJson = fromLenientJson(T3ProjectFile);
+
+/**
+ * Build the publishable JSON Schema document for `t3.json` (draft 2020-12).
+ *
+ * Served from the marketing site at {@link T3_PROJECT_FILE_SCHEMA_URL} so
+ * editors get LSP support via a `$schema` reference.
+ */
+export function buildT3ProjectFileJsonSchema(): Record {
+ const document = Schema.toJsonSchemaDocument(T3ProjectFile);
+ const jsonSchema: Record = {
+ $schema: "https://json-schema.org/draft/2020-12/schema",
+ $id: T3_PROJECT_FILE_SCHEMA_URL,
+ ...document.schema,
+ };
+ if (document.definitions && Object.keys(document.definitions).length > 0) {
+ jsonSchema.$defs = document.definitions;
+ }
+ return jsonSchema;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c58ccffc420..a1e116d95bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -172,6 +172,9 @@ importers:
apps/marketing:
dependencies:
+ '@t3tools/shared':
+ specifier: workspace:*
+ version: link:../../packages/shared
astro:
specifier: ^7.0.3
version: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(rollup@4.61.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)
diff --git a/t3.json b/t3.json
new file mode 100644
index 00000000000..3a6956003af
--- /dev/null
+++ b/t3.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://t3.codes/schema/t3.json",
+ "iconPath": "assets/dev/blueprint-web-apple-touch-180.png",
+ "scripts": [
+ {
+ "name": "Setup Worktree",
+ "command": "vp i && ln -sf $T3CODE_PROJECT_ROOT/.env .env && ln -sf $T3CODE_PROJECT_ROOT/infra/relay/.env infra/relay/.env",
+ "icon": "configure",
+ "runOnWorktreeCreate": true
+ }
+ ]
+}