Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c5de7e6
Add WSL tmux restore support for Windows terminals
BearHuddleston Mar 30, 2026
136a5e1
Harden WSL tmux test coverage
BearHuddleston Mar 30, 2026
d05e519
Add in-process terminal rendering to eliminate per-terminal webviews
lmvdz Mar 31, 2026
e485ba5
Replace xterm.js with ghostty-web for in-process terminal rendering
lmvdz Mar 31, 2026
c6a49ca
Add native GPU terminal rendering plan and design docs
lmvdz Mar 31, 2026
3146212
remove plans
lmvdz Apr 1, 2026
7c806ea
refactor(pty): async tmux calls, resize debouncing & extended timeout
lmvdz Apr 2, 2026
7cbb0cf
feat(shell): add native GPU terminal rendering engine
lmvdz Apr 2, 2026
c06dfc6
feat(settings): add Performance pane with renderer selection
lmvdz Apr 2, 2026
ef34d6c
chore(security): enforce min release age of 7d supply chain risk
lmvdz Apr 2, 2026
1cd0332
test(config, font-atlas): add unit tests for GPU renderer prefs and a…
lmvdz Apr 2, 2026
f914702
chore(pty): remove debug instrumentation
lmvdz Apr 2, 2026
5fc0dd0
fix(test): update positionTile tests for translate3d positioning
lmvdz Apr 2, 2026
584d1fc
chore(deps): remove unused ghostty-web dependency
lmvdz Apr 2, 2026
7cd7294
feat(shell): add game-style performance overlay (F3)
lmvdz Apr 2, 2026
02866e1
fix(perf-overlay): fix bottom cutoff and track right panel position
lmvdz Apr 2, 2026
f35aaf9
fix(perf-overlay): wire up CPU timing and GPU context
lmvdz Apr 2, 2026
91d2e69
fix(perf-overlay): address review findings
lmvdz Apr 2, 2026
5a66e15
fix(security): harden IPC and input validation
lmvdz Apr 2, 2026
5328361
fix(canvas): serialize concurrent state saves on Windows
lmvdz Apr 2, 2026
2ce4699
chore: remove dead code and add renderer mode awareness
lmvdz Apr 2, 2026
abf2cd4
fix(pty): async tmux in createSession, per-session sender verification
lmvdz Apr 2, 2026
ec7f45a
feat(perf-overlay): wire GPU timer queries into render path
lmvdz Apr 2, 2026
6c4245c
feat(font-atlas): add generational eviction for non-ASCII glyphs
lmvdz Apr 2, 2026
1e4519a
test: add getTerminalMode/Backend and perf overlay math coverage
lmvdz Apr 2, 2026
77663b5
fix: address second-pass review findings
lmvdz Apr 2, 2026
029e7bc
fix: address third-pass review findings
lmvdz Apr 2, 2026
06d222b
style: fix indentation and trailing whitespace
lmvdz Apr 2, 2026
7b9db1f
refactor(terminal-embed): phased async init with immediate handle return
lmvdz Apr 2, 2026
325d706
fix(perf-overlay): reference-count GPU timer for multi-terminal frames
lmvdz Apr 2, 2026
55b9358
feat(startup): typewriter splash animation, show window before heavy …
lmvdz Apr 2, 2026
5ada4ba
fix(ipc): simplify app:restart handler for webview guests
lmvdz Apr 2, 2026
0b91325
chore: add startup timing instrumentation to tile-manager
lmvdz Apr 2, 2026
7c0e41f
test: migrate panel-manager tests to bun:test with happy-dom
lmvdz Apr 2, 2026
15a72af
test(sidecar): rename tests to .node-test.ts and exclude from bun
lmvdz Apr 2, 2026
ffd5242
fix(pty): tighten type narrowing and guard sendToMainWindow in tests
lmvdz Apr 2, 2026
8a941ea
chore: gitignore plans/, tsconfig ignoreDeprecations, canvas-viewport…
lmvdz Apr 2, 2026
ff9eb39
chore(bunfig): drop redundant sidecar test exclude
lmvdz Apr 2, 2026
cda14df
feat(gpu-terminal): shared WebGL2 pipeline with xterm.js adapter
lmvdz Apr 2, 2026
929c4be
fix(security): session ID validation, path containment, ownership har…
lmvdz Apr 2, 2026
1fe7fb0
fix(canvas-persistence): replace saveInFlight with latest-wins queue
lmvdz Apr 2, 2026
b3712a4
chore(types): add ImportMeta.env type augmentation
lmvdz Apr 2, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ docs/
notes/
Screenshots/
.worktrees
opencode.json
opencode.json
plans/
3 changes: 3 additions & 0 deletions collab-electron/bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ root = "."
"@collab/shared" = "packages/shared/src"
"@collab/components" = "packages/components/src"
"@collab/theme" = "packages/theme/src"

[install]
minimumReleaseAge = 604800 # 7 days in seconds
1 change: 1 addition & 0 deletions collab-electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"electron": "40.6.0",
"electron-builder": "26.8.1",
"electron-vite": "5.0.0",
"happy-dom": "^20.8.8",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwindcss": "4.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface TerminalTabProps {
visible: boolean;
restored?: boolean;
scrollbackData?: string | null;
mode?: "tmux" | "sidecar";
mode?: "tmux" | "sidecar" | "direct";
}

function TerminalTab({ sessionId, visible, restored, scrollbackData, mode }: TerminalTabProps) {
Expand Down Expand Up @@ -180,10 +180,11 @@ function TerminalTab({ sessionId, visible, restored, scrollbackData, mode }: Ter
flushTimer = undefined;
if (firstData) {
firstData = false;
if (restored && mode !== "sidecar") {
// Clear the "Starting..." placeholder or stale tmux frame.
// Use escape sequences instead of term.reset() to avoid
// destroying the WebGL texture atlas.
if (!restored || (restored && mode === "tmux")) {
term.write("\x1b[2J\x1b[H");
} else if (!restored) {
term.reset();
}
}
for (const chunk of chunks) {
Expand Down
40 changes: 31 additions & 9 deletions collab-electron/src/main/canvas-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,37 @@ export async function loadState(): Promise<CanvasState | null> {
}
}

let saving = false;
let pendingState: CanvasState | null = null;

export async function saveState(state: CanvasState): Promise<void> {
if (!existsSync(STATE_DIR)) {
await mkdir(STATE_DIR, { recursive: true });
pendingState = state; // always keep latest

if (saving) return; // drainer will pick up pendingState

saving = true;
try {
while (pendingState !== null) {
const toSave = pendingState;
pendingState = null; // clear before async work

if (!existsSync(STATE_DIR)) {
await mkdir(STATE_DIR, { recursive: true });
}
const tmp = join(
tmpdir(),
`canvas-state-${crypto.randomUUID()}.json`,
);
const json = JSON.stringify(toSave, null, 2);
await writeFile(tmp, json, "utf-8");
try {
await rename(tmp, STATE_FILE);
} catch {
// rename can fail on Windows when another process holds the target.
await writeFile(STATE_FILE, json, "utf-8");
}
}
} finally {
saving = false;
}
const tmp = join(
tmpdir(),
`canvas-state-${crypto.randomUUID()}.json`,
);
const json = JSON.stringify(state, null, 2);
await writeFile(tmp, json, "utf-8");
await rename(tmp, STATE_FILE);
}
244 changes: 244 additions & 0 deletions collab-electron/src/main/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { describe, test, expect, beforeEach, mock } from "bun:test";
import {
loadConfig,
setPref,
getPref,
getInProcessTerminals,
getGpuRenderer,
getUncapFrameRate,
getTerminalMode,
getTerminalBackend,
getTerminalTarget,
isTerminalTarget,
} from "./config";

// ---------------------------------------------------------------------------
// getBoolPref-backed accessors (getGpuRenderer, getUncapFrameRate, etc.)
// ---------------------------------------------------------------------------

describe("getGpuRenderer", () => {
test("defaults to true when pref is not set", () => {
const config = loadConfig();
// Ensure the pref is absent
delete (config.ui as Record<string, unknown>).gpuRenderer;
// We can't easily save-then-read without touching disk, so test the
// underlying getPref path: getPref returns null when absent.
const pref = getPref(config, "gpuRenderer");
// When pref is null, getGpuRenderer should return default (true).
expect(pref === true || pref === false || pref === null).toBe(true);
// The exported function reads from disk, so just verify it returns boolean.
expect(typeof getGpuRenderer()).toBe("boolean");
});

test("returns true when pref is explicitly true", () => {
const config = loadConfig();
setPref(config, "gpuRenderer", true);
expect(getGpuRenderer()).toBe(true);
});

test("returns false when pref is explicitly false", () => {
const config = loadConfig();
setPref(config, "gpuRenderer", false);
expect(getGpuRenderer()).toBe(false);
});

test("returns default (true) when pref is a non-boolean value", () => {
const config = loadConfig();
setPref(config, "gpuRenderer", "yes");
expect(getGpuRenderer()).toBe(true);
});
});

describe("getUncapFrameRate", () => {
test("defaults to false when pref is not set", () => {
const config = loadConfig();
delete (config.ui as Record<string, unknown>).uncapFrameRate;
expect(typeof getUncapFrameRate()).toBe("boolean");
});

test("returns true when pref is explicitly true", () => {
const config = loadConfig();
setPref(config, "uncapFrameRate", true);
expect(getUncapFrameRate()).toBe(true);
});

test("returns false when pref is explicitly false", () => {
const config = loadConfig();
setPref(config, "uncapFrameRate", false);
expect(getUncapFrameRate()).toBe(false);
});

test("returns default (false) when pref is a non-boolean value", () => {
const config = loadConfig();
setPref(config, "uncapFrameRate", 120);
expect(getUncapFrameRate()).toBe(false);
});
});

describe("getInProcessTerminals", () => {
test("returns boolean", () => {
expect(typeof getInProcessTerminals()).toBe("boolean");
});

test("returns true when pref is explicitly true", () => {
const config = loadConfig();
setPref(config, "inProcessTerminals", true);
expect(getInProcessTerminals()).toBe(true);
});

test("returns false when pref is explicitly false", () => {
const config = loadConfig();
setPref(config, "inProcessTerminals", false);
expect(getInProcessTerminals()).toBe(false);
});
});

// ---------------------------------------------------------------------------
// isTerminalTarget
// ---------------------------------------------------------------------------

describe("isTerminalTarget", () => {
test("accepts 'auto'", () => {
expect(isTerminalTarget("auto")).toBe(true);
});

test("accepts 'powershell'", () => {
expect(isTerminalTarget("powershell")).toBe(true);
});

test("accepts 'shell'", () => {
expect(isTerminalTarget("shell")).toBe(true);
});

test("accepts wsl: prefixed strings", () => {
expect(isTerminalTarget("wsl:Ubuntu")).toBe(true);
expect(isTerminalTarget("wsl:Debian")).toBe(true);
});

test("rejects invalid strings", () => {
expect(isTerminalTarget("invalid")).toBe(false);
expect(isTerminalTarget("")).toBe(false);
expect(isTerminalTarget("WSL:Ubuntu")).toBe(false);
});

test("rejects non-string values", () => {
expect(isTerminalTarget(null)).toBe(false);
expect(isTerminalTarget(undefined)).toBe(false);
expect(isTerminalTarget(42)).toBe(false);
expect(isTerminalTarget(true)).toBe(false);
});
});

// ---------------------------------------------------------------------------
// getPref / setPref round-trip
// ---------------------------------------------------------------------------

describe("getPref / setPref", () => {
test("returns null for unset pref", () => {
const config = loadConfig();
expect(getPref(config, "nonExistentPref_" + Date.now())).toBe(null);
});

test("round-trips a boolean pref via an allowed key", () => {
const config = loadConfig();
setPref(config, "gpuRenderer", true);
const fresh = loadConfig();
expect(getPref(fresh, "gpuRenderer")).toBe(true);
});

test("round-trips a string pref via an allowed key", () => {
const config = loadConfig();
setPref(config, "terminalTarget", "auto");
const fresh = loadConfig();
expect(getPref(fresh, "terminalTarget")).toBe("auto");
});

test("rejects unknown pref keys", () => {
const config = loadConfig();
setPref(config, "evil_key", "payload");
expect(getPref(config, "evil_key")).toBe(null);
});

test("rejects prototype pollution keys", () => {
const config = loadConfig();
setPref(config, "__proto__", { polluted: true });
// __proto__ should not have been written with our payload
expect((config.ui as any).polluted).toBeUndefined();
});

test("allows panel-width-* prefix keys", () => {
const config = loadConfig();
setPref(config, "panel-width-nav", 300);
expect(getPref(config, "panel-width-nav")).toBe(300);
});
});

// ---------------------------------------------------------------------------
// getTerminalTarget
// ---------------------------------------------------------------------------

describe("getTerminalTarget", () => {
test("returns a valid TerminalTarget", () => {
const target = getTerminalTarget();
expect(isTerminalTarget(target)).toBe(true);
});

test("returns 'auto' when pref is invalid", () => {
const config = loadConfig();
setPref(config, "terminalTarget", "bogus");
expect(getTerminalTarget()).toBe("auto");
});
});

// ---------------------------------------------------------------------------
// getTerminalMode
// ---------------------------------------------------------------------------

describe("getTerminalMode", () => {
test("returns 'tmux' or 'sidecar'", () => {
const mode = getTerminalMode();
expect(mode === "tmux" || mode === "sidecar").toBe(true);
});

test("returns 'sidecar' on non-darwin platforms", () => {
// On Windows (which CI and this dev env run), the default is always sidecar
if (process.platform !== "darwin") {
expect(getTerminalMode()).toBe("sidecar");
}
});

test("respects explicit pref value", () => {
const config = loadConfig();
setPref(config, "terminalMode", "tmux");
// On non-darwin, getTerminalMode always returns sidecar regardless of pref
if (process.platform === "darwin") {
expect(getTerminalMode()).toBe("tmux");
} else {
expect(getTerminalMode()).toBe("sidecar");
}
});
});

// ---------------------------------------------------------------------------
// getTerminalBackend
// ---------------------------------------------------------------------------

describe("getTerminalBackend", () => {
test("returns 'direct' or 'sidecar'", () => {
const backend = getTerminalBackend();
expect(backend === "direct" || backend === "sidecar").toBe(true);
});

test("returns 'direct' on win32", () => {
if (process.platform === "win32") {
expect(getTerminalBackend()).toBe("direct");
}
});

test("returns valid backend when pref is invalid", () => {
const config = loadConfig();
setPref(config, "terminalBackend", "bogus");
const backend = getTerminalBackend();
expect(backend === "direct" || backend === "sidecar").toBe(true);
});
});
Loading
Loading