fix: use script+pipes for terminal PTY and fix frontend sizing

Bun's terminal option data callback doesn't fire inside Bun.serve
context (Bun 1.3.9 bug). Switch to script(1) for PTY allocation with
piped stdin/stdout. Fix terminal not taking full width/height by adding
min-h-0, min-w-0 and width: 100% for proper flex layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-21 00:04:50 +00:00
parent d502ef5029
commit c1f7cb5d42
4 changed files with 36 additions and 18 deletions
+33 -16
View File
@@ -29,10 +29,12 @@ export async function attach(
const gName = groupedName();
const windowTarget = `wm-${worktreeName}`;
// Create a grouped tmux session (independent sizing) and attach to the worktree's window
// Use `script` to create a proper PTY (Bun's terminal option data callback
// doesn't fire inside Bun.serve). Pipes carry data, script provides the PTY.
const cmd = [
`tmux new-session -d -s "${gName}" -t "${tmuxSession}"`,
`tmux select-window -t "${gName}:${windowTarget}"`,
`stty rows ${rows} cols ${cols}`,
`exec tmux attach-session -t "${gName}"`,
].join(" && ");
@@ -46,24 +48,33 @@ export async function attach(
sessions.set(worktreeName, session);
const proc = Bun.spawn(["bash", "-c", cmd], {
terminal: {
cols,
rows,
name: "xterm-256color",
data(_terminal, data) {
const str = typeof data === "string" ? data : new TextDecoder().decode(data);
const proc = Bun.spawn(["script", "-q", "-c", cmd, "/dev/null"], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, TERM: "xterm-256color" },
});
session.proc = proc;
// Read stdout → push to scrollback + callback
(async () => {
const reader = proc.stdout.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const str = new TextDecoder().decode(value);
session.scrollback.push(str);
if (session.scrollback.length > MAX_SCROLLBACK) {
session.scrollback.shift();
}
session.onData?.(str);
},
exit() {},
},
});
session.proc = proc;
}
} catch {
// Stream closed
}
})();
proc.exited.then((exitCode) => {
session.onExit?.(exitCode);
@@ -93,11 +104,17 @@ export async function detach(worktreeName: string): Promise<void> {
}
export function write(worktreeName: string, data: string): void {
sessions.get(worktreeName)?.proc.terminal?.write(data);
const session = sessions.get(worktreeName);
if (session) {
session.proc.stdin.write(new TextEncoder().encode(data));
}
}
export function resize(worktreeName: string, cols: number, rows: number): void {
sessions.get(worktreeName)?.proc.terminal?.resize(cols, rows);
const session = sessions.get(worktreeName);
if (!session) return;
// Resize via tmux directly (we don't have access to script's internal PTY)
Bun.spawnSync(["tmux", "resize-window", "-t", session.groupedSessionName, "-x", String(cols), "-y", String(rows)]);
}
export function getScrollback(worktreeName: string): string {
+1 -1
View File
@@ -109,7 +109,7 @@
<WorktreeList {worktrees} selected={selectedBranch} onselect={(b) => (selectedBranch = b)} />
</aside>
<main class="flex-1 flex flex-col overflow-hidden">
<main class="flex-1 min-w-0 flex flex-col overflow-hidden">
<TopBar
name={selectedBranch}
worktree={selectedWorktree}
+1
View File
@@ -37,4 +37,5 @@ dialog textarea:focus {
/* xterm overrides */
.xterm {
height: 100%;
width: 100%;
}
@@ -88,4 +88,4 @@
});
</script>
<div class="flex-1 p-1 overflow-hidden" bind:this={containerEl}></div>
<div class="flex-1 min-h-0 w-full p-1 overflow-hidden" bind:this={containerEl}></div>