Skip to content

Commit 132bfce

Browse files
7418claude
andcommitted
fix: disable terminal UI, fix hydration mismatch
- Remove terminal button from UnifiedTopBar and TerminalDrawer rendering Terminal feature disabled until proper node-pty + xterm.js integration - Fix ChatListPanel hydration mismatch: chatListOpenRaw/chatListWidth used window-dependent initializers causing SSR/client divergence - Improve TerminalInstance for future use: stable useEffect deps, ANSI rendering via ansi-to-react, focus management - Clear CLAUDECODE env var in terminal-manager to allow nested sessions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4326269 commit 132bfce

6 files changed

Lines changed: 77 additions & 64 deletions

File tree

electron/terminal-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export class TerminalManager {
5151
COLUMNS: String(opts.cols),
5252
LINES: String(opts.rows),
5353
};
54+
// Allow launching Claude Code inside the terminal
55+
delete env.CLAUDECODE;
5456

5557
const child = spawn(shell, shellArgs, {
5658
cwd: opts.cwd,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.34.1",
3+
"version": "0.34.2",
44
"private": true,
55
"workspaces": [
66
"apps/*",

src/components/layout/AppShell.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { ImageGenContext, useImageGenState } from "@/hooks/useImageGen";
1717
import { BatchImageGenContext, useBatchImageGenState } from "@/hooks/useBatchImageGen";
1818
import { SplitContext, type SplitSession } from "@/hooks/useSplit";
1919
import { SplitChatContainer } from "./SplitChatContainer";
20-
import { TerminalDrawer } from "@/components/terminal/TerminalDrawer";
2120
import { ErrorBoundary } from "./ErrorBoundary";
2221
import { getActiveSessionIds, getSnapshot } from "@/lib/stream-session-manager";
2322
import { useGitStatus } from "@/hooks/useGitStatus";
@@ -69,16 +68,25 @@ export function AppShell({ children }: { children: React.ReactNode }) {
6968
const pathname = usePathname();
7069
const router = useRouter();
7170

72-
const [chatListOpenRaw, setChatListOpenRaw] = useState(() => {
73-
if (typeof window === "undefined") return false;
74-
return window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`).matches;
75-
});
71+
const [chatListOpenRaw, setChatListOpenRaw] = useState(false);
72+
73+
// Sync with viewport after hydration to avoid SSR mismatch
74+
/* eslint-disable react-hooks/set-state-in-effect */
75+
useEffect(() => {
76+
setChatListOpenRaw(window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`).matches);
77+
}, []);
78+
/* eslint-enable react-hooks/set-state-in-effect */
7679

7780
// Panel width state with localStorage persistence
78-
const [chatListWidth, setChatListWidth] = useState(() => {
79-
if (typeof window === "undefined") return 240;
80-
return parseInt(localStorage.getItem("codepilot_chatlist_width") || "240");
81-
});
81+
const [chatListWidth, setChatListWidth] = useState(240);
82+
83+
// Restore persisted width after hydration
84+
/* eslint-disable react-hooks/set-state-in-effect */
85+
useEffect(() => {
86+
const saved = localStorage.getItem("codepilot_chatlist_width");
87+
if (saved) setChatListWidth(parseInt(saved));
88+
}, []);
89+
/* eslint-enable react-hooks/set-state-in-effect */
8290

8391
const handleChatListResize = useCallback((delta: number) => {
8492
setChatListWidth((w) => Math.min(CHATLIST_MAX, Math.max(CHATLIST_MIN, w + delta)));
@@ -305,17 +313,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
305313
return () => mql.removeEventListener("change", handler);
306314
}, []);
307315

308-
// Ctrl+` / Cmd+` toggles terminal drawer
309-
useEffect(() => {
310-
const handler = (e: KeyboardEvent) => {
311-
if (e.key === '`' && (e.ctrlKey || e.metaKey)) {
312-
e.preventDefault();
313-
setTerminalOpen((prev) => !prev);
314-
}
315-
};
316-
window.addEventListener('keydown', handler);
317-
return () => window.removeEventListener('keydown', handler);
318-
}, []);
319316

320317
// --- Skip-permissions indicator ---
321318
const [skipPermissionsActive, setSkipPermissionsActive] = useState(false);
@@ -417,7 +414,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
417414
<ErrorBoundary>{children}</ErrorBoundary>
418415
)}
419416
</main>
420-
{isChatDetailRoute && <TerminalDrawer />}
421417
</div>
422418
{isChatDetailRoute && <PanelZone />}
423419
</div>

src/components/layout/UnifiedTopBar.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
GitCommit,
88
CloudArrowUp,
99
TreeStructure,
10-
Terminal,
1110
PencilSimple,
1211
DotOutline,
1312
CaretDown,
@@ -39,8 +38,6 @@ export function UnifiedTopBar() {
3938
setFileTreeOpen,
4039
gitPanelOpen,
4140
setGitPanelOpen,
42-
terminalOpen,
43-
setTerminalOpen,
4441
currentBranch,
4542
gitDirtyCount,
4643
} = usePanel();
@@ -291,21 +288,6 @@ export function UnifiedTopBar() {
291288
<TooltipContent side="bottom">{t('topBar.git')}</TooltipContent>
292289
</Tooltip>
293290

294-
<Tooltip>
295-
<TooltipTrigger asChild>
296-
<Button
297-
variant={terminalOpen ? "secondary" : "ghost"}
298-
size="icon-sm"
299-
className={terminalOpen ? "" : "text-muted-foreground hover:text-foreground"}
300-
onClick={() => setTerminalOpen(!terminalOpen)}
301-
>
302-
<Terminal size={16} />
303-
<span className="sr-only">{t('topBar.terminal')}</span>
304-
</Button>
305-
</TooltipTrigger>
306-
<TooltipContent side="bottom">{t('topBar.terminal')}</TooltipContent>
307-
</Tooltip>
308-
309291
<Tooltip>
310292
<TooltipTrigger asChild>
311293
<Button

src/components/terminal/TerminalInstance.tsx

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,94 @@
11
"use client";
22

3-
import { useEffect, useRef } from "react";
3+
import { useEffect, useRef, useState, useCallback } from "react";
4+
import Ansi from "ansi-to-react";
45
import type { useTerminal } from "@/hooks/useTerminal";
56

67
interface TerminalInstanceProps {
78
terminal: ReturnType<typeof useTerminal>;
89
}
910

1011
/**
11-
* TerminalInstance — renders terminal output as a simple text view.
12+
* TerminalInstance — renders terminal output with ANSI color support.
1213
*
13-
* Phase 4 v1: Uses a simple <pre> element for output display.
14-
* xterm.js integration can be added later for full terminal emulation
15-
* (requires npm install xterm @xterm/addon-fit).
14+
* Uses ansi-to-react for ANSI escape code rendering.
15+
* xterm.js integration can be added later for full terminal emulation.
1616
*/
1717
export function TerminalInstance({ terminal }: TerminalInstanceProps) {
18-
const outputRef = useRef<HTMLPreElement>(null);
18+
const { isElectron, connected, exited, create, write, setOnData } = terminal;
19+
const scrollRef = useRef<HTMLDivElement>(null);
1920
const inputRef = useRef<HTMLInputElement>(null);
20-
const outputTextRef = useRef("");
21+
const [output, setOutput] = useState("");
22+
const bufferRef = useRef("");
23+
const rafRef = useRef<number | null>(null);
2124

25+
// Flush buffered output via rAF to avoid excessive re-renders
26+
const flush = useCallback(() => {
27+
rafRef.current = null;
28+
setOutput(bufferRef.current);
29+
}, []);
30+
31+
// Subscribe to PTY output
2232
useEffect(() => {
23-
terminal.setOnData((data: string) => {
24-
outputTextRef.current += data;
25-
if (outputRef.current) {
26-
outputRef.current.textContent = outputTextRef.current;
27-
outputRef.current.scrollTop = outputRef.current.scrollHeight;
33+
setOnData((data: string) => {
34+
bufferRef.current += data;
35+
if (rafRef.current === null) {
36+
rafRef.current = requestAnimationFrame(flush);
2837
}
2938
});
30-
}, [terminal]);
39+
return () => {
40+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
41+
};
42+
}, [setOnData, flush]);
43+
44+
// Auto-scroll on new output
45+
useEffect(() => {
46+
if (scrollRef.current) {
47+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
48+
}
49+
}, [output]);
3150

3251
// Create terminal on mount
3352
useEffect(() => {
34-
if (terminal.isElectron && !terminal.connected && !terminal.exited) {
35-
terminal.create(120, 30);
53+
if (isElectron && !connected && !exited) {
54+
create(120, 30);
3655
}
37-
}, [terminal]);
56+
}, [isElectron, connected, exited, create]);
57+
58+
// Focus input when terminal connects
59+
useEffect(() => {
60+
inputRef.current?.focus();
61+
}, [connected]);
3862

3963
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
4064
if (e.key === 'Enter') {
4165
const value = inputRef.current?.value || '';
42-
terminal.write(value + '\n');
66+
write(value + '\n');
4367
if (inputRef.current) inputRef.current.value = '';
4468
}
4569
};
4670

71+
const handleContainerClick = () => {
72+
inputRef.current?.focus();
73+
};
74+
4775
return (
48-
<div className="flex flex-col h-full bg-[#1a1a1a] text-[#d4d4d4] font-mono text-xs">
49-
<pre
50-
ref={outputRef}
76+
<div
77+
className="flex flex-col h-full bg-[#1a1a1a] text-[#d4d4d4] font-mono text-xs"
78+
onClick={handleContainerClick}
79+
>
80+
<div
81+
ref={scrollRef}
5182
className="flex-1 overflow-auto p-2 whitespace-pre-wrap break-all"
52-
/>
83+
>
84+
<Ansi>{output}</Ansi>
85+
</div>
5386
<div className="flex items-center border-t border-[#333] px-2">
5487
<span className="text-green-400 mr-1">$</span>
5588
<input
5689
ref={inputRef}
5790
type="text"
58-
className="flex-1 bg-transparent border-none outline-none text-xs py-1.5 text-[#d4d4d4]"
91+
className="flex-1 bg-transparent border-none outline-none text-xs py-1.5 text-[#d4d4d4] caret-[#d4d4d4]"
5992
onKeyDown={handleKeyDown}
6093
autoFocus
6194
spellCheck={false}

0 commit comments

Comments
 (0)