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>
This commit is contained in:
1 parent
4326269817
commit
132bfcebc0
6 files changed
+77
-64
No files matched your search
@@ -51,6 +51,8 @@ export class TerminalManager {
|
||||
COLUMNS: String(opts.cols),
|
||||
LINES: String(opts.rows),
|
||||
};
|
||||
// Allow launching Claude Code inside the terminal
|
||||
delete env.CLAUDECODE;
|
||||
|
||||
const child = spawn(shell, shellArgs, {
|
||||
cwd: opts.cwd,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.34.1",
|
||||
"version": "0.34.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codepilot",
|
||||
"version": "0.34.1",
|
||||
"version": "0.34.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.34.1",
|
||||
"version": "0.34.2",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ImageGenContext, useImageGenState } from "@/hooks/useImageGen";
|
||||
import { BatchImageGenContext, useBatchImageGenState } from "@/hooks/useBatchImageGen";
|
||||
import { SplitContext, type SplitSession } from "@/hooks/useSplit";
|
||||
import { SplitChatContainer } from "./SplitChatContainer";
|
||||
import { TerminalDrawer } from "@/components/terminal/TerminalDrawer";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { getActiveSessionIds, getSnapshot } from "@/lib/stream-session-manager";
|
||||
import { useGitStatus } from "@/hooks/useGitStatus";
|
||||
@@ -69,16 +68,25 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const [chatListOpenRaw, setChatListOpenRaw] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`).matches;
|
||||
});
|
||||
const [chatListOpenRaw, setChatListOpenRaw] = useState(false);
|
||||
|
||||
// Sync with viewport after hydration to avoid SSR mismatch
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
setChatListOpenRaw(window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`).matches);
|
||||
}, []);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
// Panel width state with localStorage persistence
|
||||
const [chatListWidth, setChatListWidth] = useState(() => {
|
||||
if (typeof window === "undefined") return 240;
|
||||
return parseInt(localStorage.getItem("codepilot_chatlist_width") || "240");
|
||||
});
|
||||
const [chatListWidth, setChatListWidth] = useState(240);
|
||||
|
||||
// Restore persisted width after hydration
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("codepilot_chatlist_width");
|
||||
if (saved) setChatListWidth(parseInt(saved));
|
||||
}, []);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
const handleChatListResize = useCallback((delta: number) => {
|
||||
setChatListWidth((w) => Math.min(CHATLIST_MAX, Math.max(CHATLIST_MIN, w + delta)));
|
||||
@@ -305,17 +313,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
// Ctrl+` / Cmd+` toggles terminal drawer
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === '`' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
setTerminalOpen((prev) => !prev);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
|
||||
// --- Skip-permissions indicator ---
|
||||
const [skipPermissionsActive, setSkipPermissionsActive] = useState(false);
|
||||
@@ -417,7 +414,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
)}
|
||||
</main>
|
||||
{isChatDetailRoute && <TerminalDrawer />}
|
||||
</div>
|
||||
{isChatDetailRoute && <PanelZone />}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
GitCommit,
|
||||
CloudArrowUp,
|
||||
TreeStructure,
|
||||
Terminal,
|
||||
PencilSimple,
|
||||
DotOutline,
|
||||
CaretDown,
|
||||
@@ -39,8 +38,6 @@ export function UnifiedTopBar() {
|
||||
setFileTreeOpen,
|
||||
gitPanelOpen,
|
||||
setGitPanelOpen,
|
||||
terminalOpen,
|
||||
setTerminalOpen,
|
||||
currentBranch,
|
||||
gitDirtyCount,
|
||||
} = usePanel();
|
||||
@@ -291,21 +288,6 @@ export function UnifiedTopBar() {
|
||||
<TooltipContent side="bottom">{t('topBar.git')}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={terminalOpen ? "secondary" : "ghost"}
|
||||
size="icon-sm"
|
||||
className={terminalOpen ? "" : "text-muted-foreground hover:text-foreground"}
|
||||
onClick={() => setTerminalOpen(!terminalOpen)}
|
||||
>
|
||||
<Terminal size={16} />
|
||||
<span className="sr-only">{t('topBar.terminal')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{t('topBar.terminal')}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import Ansi from "ansi-to-react";
|
||||
import type { useTerminal } from "@/hooks/useTerminal";
|
||||
|
||||
interface TerminalInstanceProps {
|
||||
@@ -8,54 +9,86 @@ interface TerminalInstanceProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* TerminalInstance — renders terminal output as a simple text view.
|
||||
* TerminalInstance — renders terminal output with ANSI color support.
|
||||
*
|
||||
* Phase 4 v1: Uses a simple <pre> element for output display.
|
||||
* xterm.js integration can be added later for full terminal emulation
|
||||
* (requires npm install xterm @xterm/addon-fit).
|
||||
* Uses ansi-to-react for ANSI escape code rendering.
|
||||
* xterm.js integration can be added later for full terminal emulation.
|
||||
*/
|
||||
export function TerminalInstance({ terminal }: TerminalInstanceProps) {
|
||||
const outputRef = useRef<HTMLPreElement>(null);
|
||||
const { isElectron, connected, exited, create, write, setOnData } = terminal;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const outputTextRef = useRef("");
|
||||
const [output, setOutput] = useState("");
|
||||
const bufferRef = useRef("");
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
// Flush buffered output via rAF to avoid excessive re-renders
|
||||
const flush = useCallback(() => {
|
||||
rafRef.current = null;
|
||||
setOutput(bufferRef.current);
|
||||
}, []);
|
||||
|
||||
// Subscribe to PTY output
|
||||
useEffect(() => {
|
||||
terminal.setOnData((data: string) => {
|
||||
outputTextRef.current += data;
|
||||
if (outputRef.current) {
|
||||
outputRef.current.textContent = outputTextRef.current;
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
setOnData((data: string) => {
|
||||
bufferRef.current += data;
|
||||
if (rafRef.current === null) {
|
||||
rafRef.current = requestAnimationFrame(flush);
|
||||
}
|
||||
});
|
||||
}, [terminal]);
|
||||
return () => {
|
||||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [setOnData, flush]);
|
||||
|
||||
// Auto-scroll on new output
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
// Create terminal on mount
|
||||
useEffect(() => {
|
||||
if (terminal.isElectron && !terminal.connected && !terminal.exited) {
|
||||
terminal.create(120, 30);
|
||||
if (isElectron && !connected && !exited) {
|
||||
create(120, 30);
|
||||
}
|
||||
}, [terminal]);
|
||||
}, [isElectron, connected, exited, create]);
|
||||
|
||||
// Focus input when terminal connects
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [connected]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const value = inputRef.current?.value || '';
|
||||
terminal.write(value + '\n');
|
||||
write(value + '\n');
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleContainerClick = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-[#1a1a1a] text-[#d4d4d4] font-mono text-xs">
|
||||
<pre
|
||||
ref={outputRef}
|
||||
<div
|
||||
className="flex flex-col h-full bg-[#1a1a1a] text-[#d4d4d4] font-mono text-xs"
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-auto p-2 whitespace-pre-wrap break-all"
|
||||
/>
|
||||
>
|
||||
<Ansi>{output}</Ansi>
|
||||
</div>
|
||||
<div className="flex items-center border-t border-[#333] px-2">
|
||||
<span className="text-green-400 mr-1">$</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="flex-1 bg-transparent border-none outline-none text-xs py-1.5 text-[#d4d4d4]"
|
||||
className="flex-1 bg-transparent border-none outline-none text-xs py-1.5 text-[#d4d4d4] caret-[#d4d4d4]"
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
|
||||
Reference in new issue
Block a user