Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
60 changes: 45 additions & 15 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,29 @@
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
"T3 Code". Splash and favicon also ship from assets/fork (copied into
apps/web/public for local/dev and into the packaged client at build
time); resolveDesktopWebAssetBrand keeps upstream's nightly/production
mapping for call-shape parity but packaging no longer applies those
channel icons. 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);
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.
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.
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,10 +241,18 @@
- 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
Expand Down Expand Up @@ -1262,7 +1277,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 +1324,26 @@
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.
tier: 4
files:
- apps/web/src/custom/ComposerShell.tsx
- apps/web/src/custom/ComposerShell.css
- 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
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.
24 changes: 23 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,35 @@ 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("applyForkWebBrandAssets");
expect(script).not.toContain("BRAND_ASSET_PATHS.productionMacIconPng");
expect(script).not.toContain("BRAND_ASSET_PATHS.nightlyMacIconPng");
expect(script).not.toContain("applyWebBrandAssets(");

const launcher = read("apps/desktop/scripts/electron-launcher.mjs");
expect(launcher).toContain('"fork"');
expect(launcher).toContain('"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
30 changes: 29 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,11 @@ 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 {
ComposerPromptRow,
ComposerShell,
Expand All @@ -16,6 +21,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 +315,22 @@ 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.
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);
expect(shouldUseCompactComposerFooter(399)).toBe(true);
});

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 +366,17 @@ 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);
});
});
34 changes: 34 additions & 0 deletions apps/web/src/overrides/components/composerFooterLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Fork shadow of upstream composerFooterLayout — see
// `.fork/customizations.yaml#fork-composer-shell`.
//
// Upstream's 620/780 thresholds were calibrated for packing mode + model into
// one in-box footer. The fork's denser below-surface control row (20px chips,
// text-only mode toggles, model on the opposite side) still has room well
// below those widths, so the ⋯ menu was firing with a large empty gap. Keep
// the wide-actions threshold under the form's max-w-3xl (768px) so the
// expanded row remains reachable at the form's natural ceiling.

export const COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX = 400;
export const COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX = 520;
export const COMPOSER_PRIMARY_ACTIONS_COMPACT_BREAKPOINT_PX =
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX;

export function shouldUseCompactComposerFooter(
width: number | null,
options?: { hasWideActions?: boolean },
): boolean {
const breakpoint = options?.hasWideActions
? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
: COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX;
return width !== null && width < breakpoint;
}

export function shouldUseCompactComposerPrimaryActions(
width: number | null,
options?: { hasWideActions?: boolean },
): boolean {
if (!options?.hasWideActions) {
return false;
}
return width !== null && width < COMPOSER_PRIMARY_ACTIONS_COMPACT_BREAKPOINT_PX;
}
12 changes: 11 additions & 1 deletion apps/web/src/theme.custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,21 @@
/* Upstream nests the PR pill with the branch trigger at gap-1 (4px), while the
strip itself is 8px between checkout and that pair — so checkout→PR reads
loose and PR→branch reads tight. Same 8px on every direct flex child keeps
the three chips (and env→checkout when both show) evenly spaced. */
the three chips (and env→checkout when both show) evenly spaced.

Upstream also gives the checkout group and branch wrapper `flex-1`, and the
branch `justify-end` below `md` (plus `md:ml-auto`). That splits the strip
into halves and parks the branch at the end of its half — a large empty gap
while "Current checkout" truncates. The fork's chips sit in one packed
leading cluster; neutralize the grow/end alignment so gap stays 8px. */
:root[data-fork="noahhendrickson-t3code"]
[data-fork-composer-context-row]
.chat-composer-context-strip
> .flex {
flex: 0 1 auto;
justify-content: flex-start;
margin-inline-start: 0;
max-width: 100%;
gap: 8px;
}

Expand Down
15 changes: 15 additions & 0 deletions assets/fork/app-icon.svg
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 assets/fork/n3-macos-1024.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 assets/fork/n3-universal-1024.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 assets/fork/n3-web-apple-touch-180.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 assets/fork/n3-web-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 added assets/fork/n3-web-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 added assets/fork/n3-web-favicon.ico
Binary file not shown.
Binary file modified assets/fork/n3-windows.ico
Binary file not shown.
27 changes: 25 additions & 2 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
resolveDesktopProductName,
resolveDesktopUpdateChannel,
resolveDesktopWebAssetBrand,
resolveForkWebIconOverrides,
resolveResourceMonitorRustTargets,
resourceMonitorExecutableName,
resolveGitHubPublishConfig,
Expand Down Expand Up @@ -96,7 +97,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
// fork:end fork-app-identity
});

it("uses the fork's placeholder artwork for every channel", () => {
it("uses the fork's green-grid artwork for every channel", () => {
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
const forkIconAssets = {
macIconPng: "assets/fork/n3-macos-1024.png",
Expand All @@ -111,9 +112,31 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
// fork:end fork-app-identity
});

it("switches the bundled splash and favicon branding for nightly versions", () => {
it("bundles the fork splash and favicon instead of upstream channel art", () => {
// fork:begin fork-app-identity — see .fork/customizations.yaml#fork-app-identity
// resolveDesktopWebAssetBrand keeps upstream's nightly/production mapping
// for call-shape parity, but packaging copies FORK_WEB_ICON_ASSETS.
assert.equal(resolveDesktopWebAssetBrand("0.0.17"), "production");
assert.equal(resolveDesktopWebAssetBrand("0.0.17-nightly.20260413.42"), "nightly");
Comment thread
NoahHendrickson marked this conversation as resolved.
Outdated
assert.deepStrictEqual(resolveForkWebIconOverrides("apps/server/dist/client"), [
{
sourceRelativePath: "assets/fork/n3-web-favicon.ico",
targetRelativePath: "apps/server/dist/client/favicon.ico",
},
{
sourceRelativePath: "assets/fork/n3-web-favicon-16x16.png",
targetRelativePath: "apps/server/dist/client/favicon-16x16.png",
},
{
sourceRelativePath: "assets/fork/n3-web-favicon-32x32.png",
targetRelativePath: "apps/server/dist/client/favicon-32x32.png",
},
{
sourceRelativePath: "assets/fork/n3-web-apple-touch-180.png",
targetRelativePath: "apps/server/dist/client/apple-touch-icon.png",
},
]);
// fork:end fork-app-identity
});

it.effect("resolves GitHub desktop publish config from Effect config", () =>
Expand Down
Loading
Loading