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
92 changes: 76 additions & 16 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@
database. legacyUserDataDirName is redirected too, so a fork build never
adopts the legacy directory upstream migrates from.
The fork presents as "no3y Code": bundle id com.t3tools.t3code.fork,
installs as "no3y Code.app", ships no3y-Code-* artifacts, packages
placeholder "N" lettermark art from assets/fork on all three platforms,
and keeps all filesystem state under ~/.t3-fork. The bundle id and the
base directory are the stable identities and must never change across
display renames; display strings are safe to rename precisely because
those two are held stable.
installs as "no3y Code.app", ships no3y-Code-* artifacts, packages the
green-grid mark from assets/fork on all three platforms (plus web splash
and favicon), and keeps all filesystem state under ~/.t3-fork. The bundle
id and the base directory are the stable identities and must never change
across display renames; display strings are safe to rename precisely
because those two are held stable.
Isolation is forked at the base directory, not the "userdata" leaf,
because the packaged app is two processes: the desktop resolves its own
state directory and hands only baseDir (as bootstrap t3Home) to the
Expand All @@ -210,22 +210,41 @@
The rendered app carries the name too: the sidebar brand reads
APP_BASE_NAME and drops the T3 wordmark, and branding.ts's bridge-less
fallback points at the fork's own constant instead of upstream's
"T3 Code". Deliberately shared or deferred: the t3code:// URL scheme stays
shared (it is the app's own internal origin, not just a deep-link
handler — two installed apps contend for it, untidy but not destructive);
splash, favicon and resolveDesktopWebAssetBrand still carry upstream's T3
art (images rather than strings; separate work). Known carve-out: the WSL
backend bootstrap deliberately omits t3Home, so WSL-side state is not
isolated by this customization.
"T3 Code". Splash and favicon also ship from assets/fork (copied into
apps/web/public for local/dev and into the packaged desktop client at
build time via FORK_WEB_ICON_ASSETS + the shared applyWebIconOverrides
path). assets/fork/app-icon.svg is the Figma source of truth for the
green-grid mark (file t3-fork, node 157:4036); raster exports are
derived from that family. Filenames still say n3-* for path stability.
Deliberately shared: the t3code:// URL scheme stays shared (it is the
app's own internal origin, not just a deep-link handler — two installed
apps contend for it, untidy but not destructive).
Known carve-out: the WSL backend bootstrap deliberately omits t3Home, so
WSL-side state is not isolated by this customization.
Known carve-out: hosted web (`apps/web/vercel.ts`) still runs
apply-web-brand-assets.ts with the Vercel channel and stamps upstream
production/nightly icons over the fork public/ set at deploy time.
Known carve-out: `npx t3` publish (`apps/server/scripts/cli.ts`
preparePublishIcons) still resolves upstream icons via
resolveWebAssetBrandForPackageVersion into dist/client.
Known conflict: `pnpm icons:export` writes blueprint development art
into apps/web/public via DEVELOPMENT_PUBLIC_ICON_OVERRIDES; after a
brand export the fork public icons must be re-copied from assets/fork
(or icons:check will report them stale / the byte-equality guard fails).
History — the v0.1.1 leaf-rename incident, the N3→no3y rename, and the
pre-a17cbc3b4 dev-state claim — is in
.fork/notes/FORK-CUSTOMIZATION-DECISIONS.md.
tier: 4
files:
- apps/web/src/custom/forkBranding.ts
- assets/fork/app-icon.svg
- assets/fork/n3-macos-1024.png
- assets/fork/n3-universal-1024.png
- assets/fork/n3-windows.ico
- assets/fork/n3-web-apple-touch-180.png
- assets/fork/n3-web-favicon-16x16.png
- assets/fork/n3-web-favicon-32x32.png
- assets/fork/n3-web-favicon.ico
shadows: []
watch:
- apps/desktop/src/main.ts
Expand All @@ -234,14 +253,33 @@
- apps/desktop/src/app/DesktopEnvironment.ts
- apps/desktop/src/app/DesktopEnvironment.test.ts
- apps/desktop/src/backend/DesktopBackendConfiguration.ts
- apps/desktop/scripts/electron-launcher.mjs
- apps/server/src/config.ts
- apps/web/src/branding.ts
- apps/web/src/branding.test.ts
- apps/web/src/components/sidebar/SidebarChrome.tsx
- apps/web/public/apple-touch-icon.png
- apps/web/public/favicon-16x16.png
- apps/web/public/favicon-32x32.png
- apps/web/public/favicon.ico
- apps/desktop/resources/icon.png
- apps/desktop/resources/icon.ico
- apps/desktop/resources/icon.icns
- packages/shared/src/devHome.ts
- packages/shared/src/hostProcess.ts
- scripts/build-desktop-artifact.ts
- scripts/build-desktop-artifact.test.ts
# Shared web-icon override builder + apply path packaging reuses for
# FORK_WEB_ICON_ASSETS (no twin copy pipeline in the packaging script).
- scripts/lib/brand-assets.ts
- scripts/apply-web-brand-assets.ts
# icons:export can overwrite apps/web/public with blueprint art — see
# Known conflict in intent.
- scripts/export-brand-icons.ts
# Hosted-web / npx-t3 surfaces still stamp upstream channel icons — see
# Known carve-out lines in intent.
- apps/web/vercel.ts
- apps/server/scripts/cli.ts
- scripts/dev-runner.ts
- vite.config.ts
- .vscode/settings.json
Expand Down Expand Up @@ -1262,7 +1300,12 @@
worktree, PR, and branch chips sit above the surface as filled chips
sharing one selected-surface fill with no hairline, 8px apart (matching
the context row's pb-2 under the pills — including the nested PR+branch
pair that upstream keeps at gap-1), on the Auto chip's container at
pair that upstream keeps at gap-1). Upstream's strip children use flex-1
and the branch's justify-end / md:ml-auto, which opens a large empty gap
between checkout and branch at narrower widths while the checkout label
truncates; the fork forces those direct flex children to flex 0 1 auto,
justify-content flex-start, and margin-inline-start 0 so the chips stay a
packed leading cluster. Chips use the Auto chip's container at
4px/6px inline padding (24px tall, 6px radius — extra 2px on the end so
the trailing chevron clears the curve; branch combobox takes 6px start,
workspace select 0 for its roomier folder glyph) at 12px type matching
Expand Down Expand Up @@ -1304,16 +1347,33 @@
mode chip are 12px medium on 16px with 12px glyphs. The prompt wrapper
owns the 12.5rem scroll cap so a one-line Lexical editor never paints a
phantom scrollbar, and the placeholder/spacer overflow containment remains
in place. Attachment and pending-state content may add height above the
in place. Attachment and pending-state content may add height above the
base row, but ordinary composer height changes only when prompt content
wraps. Pill styling stays narrowly scoped so pending-state primary actions
retain their upstream geometry.

Compact ⋯ collapse for the below-surface mode row uses fork-lowered
form-width thresholds (400px normal / 520px with wide primary actions),
not upstream's 620/780. Upstream's numbers pack denser in-box footers;
here mode chips sit opposite the model picker on a dedicated row, so
those thresholds hide Full access / Build / Plan behind ⋯ while a large
empty gap remains. The wide-actions threshold must stay under the form's
max-w-3xl (768px) or the expanded row can never appear in that state.
Mode-row collapse and model-picker compaction are deliberately split:
the override drives only ⋯ vs ComposerFooterModeControls; the model
trigger keeps upstream's 620/780 via composerModelSlotCompact.ts so the
right slot does not grow in the newly expanded 400–620 band. Traits stay
gated with the mode row (inline when expanded, CompactComposerControlsMenu
when collapsed) so they are never stranded between the two thresholds.
tier: 4
files:
- apps/web/src/custom/ComposerShell.tsx
- apps/web/src/custom/ComposerShell.css
- apps/web/src/custom/composerModelSlotCompact.ts
- apps/web/src/theme.custom.css
shadows: []
- apps/web/src/overrides/components/composerFooterLayout.ts
shadows:
- apps/web/src/components/composerFooterLayout.ts
watch:
# Carries the base row, the context/control rows, and contextStrip prop.
- apps/web/src/components/chat/ChatComposer.tsx
Expand Down
20 changes: 20 additions & 0 deletions .fork/notes/pr-46/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# PR #46 composer + favicon screenshots

Before/after captures for the visual checklist on
[`NoahHendrickson/t3code#46`](https://github.com/NoahHendrickson/t3code/pull/46).
Captured from the t3code draft thread (`Current checkout` + `#46` +
`fork/composer-chrome-and-favicon` context strip) with the composer form's
measured `clientWidth` pinned to each target, dark mode, 2x scale.

- `composer-before-*.png` — `custom@a3c19aa70`: mode row collapses to `⋯` below
620, checkout chip stretches on upstream `flex-1`, branch chip truncates while
empty space remains.
- `composer-after-*.png` — this branch: Full access / Build stay expanded down
to 400, chips pack left, the 32-char branch label fits at 460+.
- `composer-after-380.png` — below the new 400 threshold the `⋯` compact menu
and model-slot compaction still engage.
- The long-label context-strip case is the same captures: the branch chip
carries the 32-char `fork/composer-chrome-and-favicon` label, truncated in
before-460/620 and fully visible in after-460+.
- `apple-touch-icon-before/after.png` — dev-served web icon: upstream T3
blueprint vs the Figma green-grid mark (`t3-fork` node `157:4036`).
Binary file added .fork/notes/pr-46/apple-touch-icon-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/apple-touch-icon-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-380.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-460.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-520.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-620.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-after-768.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-before-400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-before-460.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-before-520.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-before-620.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .fork/notes/pr-46/composer-before-768.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/desktop/resources/icon.icns
Binary file not shown.
Binary file modified apps/desktop/resources/icon.ico
Binary file not shown.
Binary file modified apps/desktop/resources/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 3 additions & 6 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@ export const APP_BUNDLE_ID = isDevelopment
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 14;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
const developmentMacIconPngPath = NodePath.join(
repoRoot,
"assets",
"dev",
"blueprint-macos-1024.png",
);
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
const developmentMacIconPngPath = NodePath.join(repoRoot, "assets", "fork", "n3-macos-1024.png");
// fork:end fork-app-identity
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
const hostPlatform = NodeOS.platform();

Expand Down
Binary file modified apps/web/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon.ico
Binary file not shown.
27 changes: 26 additions & 1 deletion apps/web/src/__fork_guards__/forkAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,38 @@ describe("fork guard: fork-app-identity", () => {
it("packages with the fork's own artwork", () => {
// Upstream art would make a fork build indistinguishable from the real
// app in the Dock and /Applications, exactly where the two must be
// tell-apart-able. All three platform icons come from assets/fork.
// tell-apart-able. Desktop icons and bundled splash/favicon come from
// assets/fork; packaging must not fall back to upstream channel art.
const script = read(BUILD_SCRIPT);
expect(script).toContain('"assets/fork/n3-macos-1024.png"');
expect(script).toContain('"assets/fork/n3-universal-1024.png"');
expect(script).toContain('"assets/fork/n3-windows.ico"');
expect(script).toContain('"assets/fork/n3-web-favicon.ico"');
expect(script).toContain("FORK_WEB_ICON_ASSETS");
expect(script).toContain("applyWebIconOverrides");
expect(script).toContain("resolveWebIconOverridesFromSources");
expect(script).not.toContain("BRAND_ASSET_PATHS.productionMacIconPng");
expect(script).not.toContain("BRAND_ASSET_PATHS.nightlyMacIconPng");
expect(script).not.toContain("applyWebBrandAssets(");
expect(script).not.toContain("applyForkWebBrandAssets");
expect(script).not.toContain("resolveForkWebIconOverrides");

const launcher = read("apps/desktop/scripts/electron-launcher.mjs");
expect(launcher).toContain('"assets", "fork", "n3-macos-1024.png"');
expect(launcher).not.toContain("blueprint-macos-1024.png");

// Dev (`vp run dev`) serves apps/web/public — keep it byte-identical to
// the fork sources so local tabs don't silently fall back to T3 blueprint.
for (const [source, target] of [
["assets/fork/n3-web-favicon.ico", "apps/web/public/favicon.ico"],
["assets/fork/n3-web-favicon-16x16.png", "apps/web/public/favicon-16x16.png"],
["assets/fork/n3-web-favicon-32x32.png", "apps/web/public/favicon-32x32.png"],
["assets/fork/n3-web-apple-touch-180.png", "apps/web/public/apple-touch-icon.png"],
] as const) {
expect(NodeFS.readFileSync(NodePath.join(repoRoot, target))).toEqual(
NodeFS.readFileSync(NodePath.join(repoRoot, source)),
);
}
});

it("keeps packaged state out of the shared ~/.t3 base directory", () => {
Expand Down
57 changes: 56 additions & 1 deletion apps/web/src/__fork_guards__/forkComposerShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import {
COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
shouldUseCompactComposerFooter,
} from "../components/composerFooterLayout";
import {
COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX,
COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
shouldUseCompactComposerModelSlot,
} from "../custom/composerModelSlotCompact";
import {
ComposerPromptRow,
ComposerShell,
Expand All @@ -16,6 +26,9 @@ import {
import { FORK_MARKER_ATTRIBUTE, FORK_MARKER_VALUE } from "../custom/forkMarker";
import { cssRules } from "./cssRules";

/** Form ceiling from ChatComposer's `max-w-3xl` — keep compact thresholds below it. */
const COMPOSER_FORM_MAX_WIDTH_PX = 768;

function readSibling(relativePath: string): string {
return NodeFS.readFileSync(NodeURL.fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
}
Expand Down Expand Up @@ -307,6 +320,43 @@ describe("fork guard: fork-composer-shell", () => {
}
});

it("keeps mode-row ⋯ collapse below the denser fork control-row widths", () => {
// Upstream is 620/780; those fire with a large empty gap on the fork row.
// Mode-row only — model-picker compaction stays at upstream 620/780.
expect(COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX).toBe(400);
expect(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBe(520);
expect(COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBeLessThan(
COMPOSER_FORM_MAX_WIDTH_PX,
);
expect(shouldUseCompactComposerFooter(560)).toBe(false);
expect(
shouldUseCompactComposerFooter(560, {
hasWideActions: true,
}),
).toBe(false);
// Wide-actions branch: expanded at/above 520, compact below.
expect(
shouldUseCompactComposerFooter(519, {
hasWideActions: true,
}),
).toBe(true);
expect(
shouldUseCompactComposerFooter(520, {
hasWideActions: true,
}),
).toBe(false);
expect(shouldUseCompactComposerFooter(399)).toBe(true);
expect(shouldUseCompactComposerFooter(400)).toBe(false);

// Model trigger stays on upstream widths; ChatComposer must wire both.
expect(COMPOSER_MODEL_SLOT_COMPACT_BREAKPOINT_PX).toBe(620);
expect(COMPOSER_MODEL_SLOT_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX).toBe(780);
expect(shouldUseCompactComposerModelSlot(560)).toBe(true);
expect(shouldUseCompactComposerModelSlot(620)).toBe(false);
expect(chatComposer).toContain("shouldUseCompactComposerModelSlot");
expect(chatComposer).toContain("compact={isComposerModelSlotCompact}");
});

it("keeps primary actions out of ghost sizing", () => {
expect(primaryActions).toContain('data-fork-composer-action="send"');
expect(primaryActions).toContain('data-fork-composer-action="stop"');
Expand Down Expand Up @@ -342,13 +392,18 @@ describe("fork guard: fork-composer-shell", () => {
expect(strip?.body).toMatch(/margin:\s*0/u);
expect(strip?.body).toMatch(/gap:\s*8px/u);
// Nested PR+branch (and env+checkout) wrappers keep upstream gap-1; the
// fork re-gaps them to 8px so checkout→PR→branch reads evenly.
// fork re-gaps them to 8px so checkout→PR→branch reads evenly, and drops
// flex-1 / justify-end / ml-auto so the cluster stays packed when narrow.
const nested = rules.find(
(rule) =>
rule.selector.includes("[data-fork-composer-context-row]") &&
rule.selector.includes(".chat-composer-context-strip") &&
(rule.selector.includes(">.flex") || rule.selector.includes("> .flex")),
);
expect(nested?.body).toMatch(/gap:\s*8px/u);
expect(nested?.body).toMatch(/flex:\s*0 1 auto/u);
expect(nested?.body).toMatch(/justify-content:\s*flex-start/u);
expect(nested?.body).toMatch(/margin-inline-start:\s*0/u);
expect(nested?.body).toMatch(/max-width:\s*100%/u);
});
});
Loading
Loading