Skip to content
Closed
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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
"typescript": "catalog:",
"vitest": "catalog:"
},
"productName": "T3 Code (Alpha)"
"productName": "H3Code"
}
6 changes: 3 additions & 3 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron"
// This file mostly exists because we want dev mode to say "H3Code (Dev)" instead of "electron"

import { spawnSync } from "node:child_process";
import {
Expand All @@ -17,8 +17,8 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";
const APP_DISPLAY_NAME = isDevelopment ? "H3Code (Dev)" : "H3Code";
const APP_BUNDLE_ID = isDevelopment ? "com.h3tools.h3code.dev" : "com.h3tools.h3code";
const LAUNCHER_VERSION = 1;

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down
37 changes: 20 additions & 17 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,26 @@ const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secr
const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret";
const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state";
const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode";
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const BASE_DIR =
process.env.H3CODE_HOME?.trim() ||
process.env.T3CODE_HOME?.trim() ||
Path.join(OS.homedir(), ".h3code");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing legacy fallback for home directory path change

High Severity

BASE_DIR defaults changed from ~/.t3 to ~/.h3code with no fallback check for the old ~/.t3 path. Unlike resolveUserDataPath() which checks if the legacy Electron userdata directory exists before switching to the new name, BASE_DIR unconditionally uses ~/.h3code. Existing users with settings, saved environments, server state (state.sqlite), and logs stored under ~/.t3/userdata/ will silently lose access to all of that data on upgrade.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 752f261. Configure here.

const STATE_DIR = Path.join(BASE_DIR, "userdata");
const DESKTOP_SETTINGS_PATH = Path.join(STATE_DIR, "desktop-settings.json");
const CLIENT_SETTINGS_PATH = Path.join(STATE_DIR, "client-settings.json");
const SAVED_ENVIRONMENT_REGISTRY_PATH = Path.join(STATE_DIR, "saved-environments.json");
const DESKTOP_SCHEME = "t3";
const ROOT_DIR = Path.resolve(__dirname, "../../..");
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const APP_USER_MODEL_ID = "com.t3tools.t3code";
const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop";
const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code";
const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code";
const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const APP_DISPLAY_NAME = isDevelopment ? "H3Code (Dev)" : "H3Code";
const APP_USER_MODEL_ID = "com.h3tools.h3code";
const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "h3code-dev.desktop" : "h3code.desktop";
const LINUX_WM_CLASS = isDevelopment ? "h3code-dev" : "h3code";
const USER_DATA_DIR_NAME = isDevelopment ? "h3code-dev" : "h3code";
const LEGACY_PRE_REBRAND_APP_NAME = ["T3", "Code"].join(" ");
const LEGACY_PRE_REBRAND_USER_DATA_DIR_NAME = isDevelopment
? `${LEGACY_PRE_REBRAND_APP_NAME} (Dev)`
: `${LEGACY_PRE_REBRAND_APP_NAME} (Alpha)`;
const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;
const COMMIT_HASH_DISPLAY_LENGTH = 12;
const LOG_DIR = Path.join(STATE_DIR, "logs");
Expand Down Expand Up @@ -659,7 +665,7 @@ function handleFatalStartupError(stage: string, error: unknown): void {
console.error(`[desktop] fatal startup error (${stage})`, error);
if (!isQuitting) {
isQuitting = true;
dialog.showErrorBox("T3 Code failed to start", `Stage: ${stage}\n${message}${detail}`);
dialog.showErrorBox("H3Code failed to start", `Stage: ${stage}\n${message}${detail}`);
}
stopBackend();
restoreStdIoCapture?.();
Expand Down Expand Up @@ -764,7 +770,7 @@ async function checkForUpdatesFromMenu(): Promise<void> {
void dialog.showMessageBox({
type: "info",
title: "You're up to date!",
message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`,
message: `H3Code ${updateState.currentVersion} is currently the newest version available.`,
buttons: ["OK"],
});
} else if (updateState.status === "error") {
Expand Down Expand Up @@ -881,13 +887,10 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
* Resolve the Electron userData directory path.
*
* Electron derives the default userData path from `productName` in
* package.json, which currently produces directories with spaces and
* parentheses (e.g. `~/.config/T3 Code (Alpha)` on Linux). This is
* unfriendly for shell usage and violates Linux naming conventions.
*
* We override it to a clean lowercase name (`t3code`). If the legacy
* directory already exists we keep using it so existing users don't
* lose their Chromium profile data (localStorage, cookies, sessions).
* package.json. Override it to a clean lowercase name so packaged and local
* builds keep stable paths across platforms. If the pre-rebrand path already
* exists we keep using it so existing users don't lose their Chromium profile
* data (localStorage, cookies, sessions).
*/
function resolveUserDataPath(): string {
const appDataBase =
Expand All @@ -897,7 +900,7 @@ function resolveUserDataPath(): string {
? Path.join(OS.homedir(), "Library", "Application Support")
: process.env.XDG_CONFIG_HOME || Path.join(OS.homedir(), ".config");

const legacyPath = Path.join(appDataBase, LEGACY_USER_DATA_DIR_NAME);
const legacyPath = Path.join(appDataBase, LEGACY_PRE_REBRAND_USER_DATA_DIR_NAME);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Electron userdata path missing intermediate legacy fallback

High Severity

resolveUserDataPath checks for the pre-rebrand legacy directory (T3 Code (Alpha)) then falls back to the new h3code name. But USER_DATA_DIR_NAME changed from t3code to h3code, and there's no check for the intermediate t3code directory. Users whose Electron/Chromium profile data lives in ~/.config/t3code (fresh installs made after the override was added but before this rename) will silently lose access to their session data, localStorage, and cookies.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 752f261. Configure here.

if (FS.existsSync(legacyPath)) {
return legacyPath;
}
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/processRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import { describe, expect, it } from "vitest";
import { runProcess } from "./processRunner";

describe("runProcess", () => {
it("preserves argument boundaries for shell-sensitive values", async () => {
const result = await runProcess(process.execPath, [
"-e",
"process.stdout.write(JSON.stringify(process.argv.slice(1)))",
"Add GHCR Docker publish workflow",
"ampersand & value",
'quoted " value',
]);

expect(JSON.parse(result.stdout)).toEqual([
"Add GHCR Docker publish workflow",
"ampersand & value",
'quoted " value',
]);
});

it("fails when output exceeds max buffer in default mode", async () => {
await expect(
runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], { maxBufferBytes: 128 }),
Expand Down
7 changes: 3 additions & 4 deletions apps/server/src/processRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,8 @@ function normalizeBufferError(
const DEFAULT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;

/**
* On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe`
* wrapper, leaving the actual command running. Use `taskkill /T` to kill the
* entire process tree instead.
* On Windows, terminate the process tree so child processes launched by the
* command do not survive a timeout or output-buffer failure.
*/
function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void {
if (process.platform === "win32" && child.pid !== undefined) {
Expand Down Expand Up @@ -139,7 +138,7 @@ export async function runProcess(
cwd: options.cwd,
env: options.env,
stdio: "pipe",
shell: process.platform === "win32",
shell: false,
});

let stdout = "";
Expand Down
6 changes: 3 additions & 3 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap"
rel="stylesheet"
/>
<title>T3 Code (Alpha)</title>
<title>H3Code</title>
</head>
<body>
<div id="root">
<div id="boot-shell">
<div id="boot-shell-card" aria-label="T3 Code splash screen">
<img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" />
<div id="boot-shell-card" aria-label="%APP_BASE_NAME% splash screen">
<img id="boot-shell-logo" src="/apple-touch-icon.png" alt="%APP_BASE_NAME%" />
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/branding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const APP_BASE_NAME = "T3 Code";
export const APP_STAGE_LABEL = import.meta.env.DEV ? "Dev" : "Alpha";
export const APP_BASE_NAME = "H3Code";
export const APP_STAGE_LABEL = import.meta.env.APP_STAGE_LABEL;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low src/branding.ts:2

APP_STAGE_LABEL reads import.meta.env.APP_STAGE_LABEL without a fallback, so the environment variable renders as undefined in the display name when unset. The previous implementation defaulted to "Dev" or "Alpha" based on import.meta.env.DEV. Consider restoring a fallback value so the display name remains valid without manual environment configuration.

-export const APP_STAGE_LABEL = import.meta.env.APP_STAGE_LABEL;
+export const APP_STAGE_LABEL = import.meta.env.APP_STAGE_LABEL || (import.meta.env.DEV ? "Dev" : "Alpha");
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/branding.ts around line 2:

`APP_STAGE_LABEL` reads `import.meta.env.APP_STAGE_LABEL` without a fallback, so the environment variable renders as `undefined` in the display name when unset. The previous implementation defaulted to `"Dev"` or `"Alpha"` based on `import.meta.env.DEV`. Consider restoring a fallback value so the display name remains valid without manual environment configuration.

Evidence trail:
apps/web/src/branding.ts (lines 1-4) at REVIEWED_COMMIT shows `APP_STAGE_LABEL = import.meta.env.APP_STAGE_LABEL` with no fallback. The git_diff between MERGE_BASE and REVIEWED_COMMIT shows the previous implementation was `APP_STAGE_LABEL = import.meta.env.DEV ? "Dev" : "Alpha"` which always provided a fallback value.

export const APP_DISPLAY_NAME = `${APP_BASE_NAME} (${APP_STAGE_LABEL})`;
export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0";
21 changes: 2 additions & 19 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
} from "@t3tools/contracts/settings";
import { usePrimaryEnvironmentId } from "../environments/primary";
import { isElectron } from "../env";
import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
import { APP_BASE_NAME, APP_STAGE_LABEL, APP_VERSION } from "../branding";
import { isTerminalFocused } from "../lib/terminalFocus";
import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
import {
Expand Down Expand Up @@ -1815,22 +1815,6 @@ const SidebarProjectListRow = memo(function SidebarProjectListRow(props: Sidebar
);
});

function T3Wordmark() {
return (
<svg
aria-label="T3"
className="h-2.5 w-auto shrink-0 text-foreground"
viewBox="15.5309 37 94.3941 56.96"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
fill="currentColor"
/>
</svg>
);
}

type SortableProjectHandleProps = Pick<
ReturnType<typeof useSortable>,
"attributes" | "listeners" | "setActivatorNodeRef"
Expand Down Expand Up @@ -1956,9 +1940,8 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
className="ml-1 flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md outline-hidden ring-ring transition-colors hover:text-foreground focus-visible:ring-2"
to="/"
>
<T3Wordmark />
<span className="truncate text-sm font-medium tracking-tight text-muted-foreground">
Code
{APP_BASE_NAME}
</span>
<span className="rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60">
{APP_STAGE_LABEL}
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/components/SplashScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { APP_BASE_NAME } from "../branding";

export function SplashScreen() {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="flex size-24 items-center justify-center" aria-label="T3 Code splash screen">
<img alt="T3 Code" className="size-16 object-contain" src="/apple-touch-icon.png" />
<div
className="flex size-24 items-center justify-center"
aria-label={`${APP_BASE_NAME} splash screen`}
>
<img alt={APP_BASE_NAME} className="size-16 object-contain" src="/apple-touch-icon.png" />
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { DesktopBridge, LocalApi } from "@t3tools/contracts";

interface ImportMetaEnv {
readonly APP_STAGE_LABEL: string;
readonly APP_VERSION: string;
}

Expand Down
128 changes: 69 additions & 59 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const host = process.env.HOST?.trim() || "localhost";
const configuredHttpUrl = process.env.VITE_HTTP_URL?.trim();
const configuredWsUrl = process.env.VITE_WS_URL?.trim();
const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase();
const APP_BASE_NAME = "H3Code";

const buildSourcemap =
sourcemapEnv === "0" || sourcemapEnv === "false"
Expand Down Expand Up @@ -41,65 +42,74 @@ function resolveDevProxyTarget(wsUrl: string | undefined): string | undefined {

const devProxyTarget = resolveDevProxyTarget(configuredWsUrl);

export default defineConfig({
plugins: [
tanstackRouter(),
react(),
babel({
// We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0
// This is because the babel plugin only automatically parses typescript and jsx based on relative paths (e.g. "**/*.ts")
// whereas the previous version of the plugin parsed all files with a .ts extension.
// This is causing our packages/ directory to fail to parse, as they are not relative to the CWD.
parserOpts: { plugins: ["typescript", "jsx"] },
presets: [reactCompilerPreset()],
}),
tailwindcss(),
],
optimizeDeps: {
include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"],
},
define: {
"import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""),
// In dev mode, tell the web app where the WebSocket server lives
"import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),
"import.meta.env.APP_VERSION": JSON.stringify(pkg.version),
},
resolve: {
tsconfigPaths: true,
},
server: {
host,
port,
strictPort: true,
...(devProxyTarget
? {
proxy: {
"/.well-known": {
target: devProxyTarget,
changeOrigin: true,
},
"/api": {
target: devProxyTarget,
changeOrigin: true,
},
"/attachments": {
target: devProxyTarget,
changeOrigin: true,
},
},
}
: {}),
hmr: {
// Explicit config so Vite's HMR WebSocket connects reliably
// inside Electron's BrowserWindow. Vite 8 uses console.debug for
// connection logs — enable "Verbose" in DevTools to see them.
protocol: "ws",
export default defineConfig(({ command }) => {
const appStageLabel = command === "serve" ? "Dev" : "Alpha";

return {
plugins: [
{
name: "h3code-html-branding",
transformIndexHtml: (html) => html.replaceAll("%APP_BASE_NAME%", APP_BASE_NAME),
},
tanstackRouter(),
react(),
babel({
// We need to be explicit about the parser options after moving to @vitejs/plugin-react v6.0.0
// This is because the babel plugin only automatically parses typescript and jsx based on relative paths (e.g. "**/*.ts")
// whereas the previous version of the plugin parsed all files with a .ts extension.
// This is causing our packages/ directory to fail to parse, as they are not relative to the CWD.
parserOpts: { plugins: ["typescript", "jsx"] },
presets: [reactCompilerPreset()],
}),
tailwindcss(),
],
optimizeDeps: {
include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"],
},
define: {
"import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""),
// In dev mode, tell the web app where the WebSocket server lives
"import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),
"import.meta.env.APP_STAGE_LABEL": JSON.stringify(appStageLabel),
"import.meta.env.APP_VERSION": JSON.stringify(pkg.version),
},
resolve: {
tsconfigPaths: true,
},
server: {
host,
port,
strictPort: true,
...(devProxyTarget
? {
proxy: {
"/.well-known": {
target: devProxyTarget,
changeOrigin: true,
},
"/api": {
target: devProxyTarget,
changeOrigin: true,
},
"/attachments": {
target: devProxyTarget,
changeOrigin: true,
},
},
}
: {}),
hmr: {
// Explicit config so Vite's HMR WebSocket connects reliably
// inside Electron's BrowserWindow. Vite 8 uses console.debug for
// connection logs — enable "Verbose" in DevTools to see them.
protocol: "ws",
host,
},
},
build: {
outDir: "dist",
emptyOutDir: true,
sourcemap: buildSourcemap,
},
},
build: {
outDir: "dist",
emptyOutDir: true,
sourcemap: buildSourcemap,
},
};
});
Loading
Loading