feat: replace force remap with terminal reload control

This commit is contained in:
Harsh 2026-03-06 10:20:01 +05:30
parent 820cbab00c
commit cf2b165b46
2 changed files with 104 additions and 70 deletions

View File

@ -19,6 +19,7 @@ interface DirectTerminalProps {
/** CSS height for the terminal container in normal (non-fullscreen) mode.
* Defaults to "max(440px, calc(100vh - 440px))". */
height?: string;
reloadCommand?: string;
}
/**
@ -36,6 +37,7 @@ export function DirectTerminal({
startFullscreen = false,
variant = "agent",
height = "max(440px, calc(100vh - 440px))",
reloadCommand,
}: DirectTerminalProps) {
const router = useRouter();
const pathname = usePathname();
@ -51,6 +53,7 @@ export function DirectTerminal({
const [fullscreen, setFullscreen] = useState(startFullscreen);
const [status, setStatus] = useState<"connecting" | "connected" | "error">("connecting");
const [error, setError] = useState<string | null>(null);
const [reloading, setReloading] = useState(false);
// Update URL when fullscreen changes
useEffect(() => {
@ -66,6 +69,20 @@ export function DirectTerminal({
router.replace(newUrl, { scroll: false });
}, [fullscreen, pathname, router, searchParams]);
async function handleReload(): Promise<void> {
if (!reloadCommand || reloading) return;
setReloading(true);
try {
await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: reloadCommand }),
});
} finally {
setReloading(false);
}
}
useEffect(() => {
if (!terminalRef.current) return;
@ -93,9 +110,7 @@ export function DirectTerminal({
// agent = blue (#5b7ef8), orchestrator = violet (#a371f7)
const cursorColor = variant === "orchestrator" ? "#a371f7" : "#5b7ef8";
const selectionColor =
variant === "orchestrator"
? "rgba(163, 113, 247, 0.25)"
: "rgba(91, 126, 248, 0.3)";
variant === "orchestrator" ? "rgba(163, 113, 247, 0.25)" : "rgba(91, 126, 248, 0.3)";
// Initialize xterm.js Terminal
const terminal = new Terminal({
@ -109,22 +124,22 @@ export function DirectTerminal({
cursorAccent: "#0a0a0f",
selectionBackground: selectionColor,
// ANSI colors — slightly warmer than pure defaults
black: "#1a1a24",
red: "#ef4444",
green: "#22c55e",
yellow: "#f59e0b",
blue: "#5b7ef8",
magenta: "#a371f7",
cyan: "#22d3ee",
white: "#d4d4d8",
brightBlack: "#50506a",
brightRed: "#f87171",
brightGreen: "#4ade80",
brightYellow: "#fbbf24",
brightBlue: "#7b9cfb",
black: "#1a1a24",
red: "#ef4444",
green: "#22c55e",
yellow: "#f59e0b",
blue: "#5b7ef8",
magenta: "#a371f7",
cyan: "#22d3ee",
white: "#d4d4d8",
brightBlack: "#50506a",
brightRed: "#f87171",
brightGreen: "#4ade80",
brightYellow: "#fbbf24",
brightBlue: "#7b9cfb",
brightMagenta: "#c084fc",
brightCyan: "#67e8f9",
brightWhite: "#eeeef5",
brightCyan: "#67e8f9",
brightWhite: "#eeeef5",
},
scrollback: 10000,
allowProposedApi: true,
@ -201,7 +216,10 @@ export function DirectTerminal({
const MAX_BUFFER_BYTES = 1_048_576; // 1 MB
const flushWriteBuffer = () => {
if (safetyTimer) { clearTimeout(safetyTimer); safetyTimer = null; }
if (safetyTimer) {
clearTimeout(safetyTimer);
safetyTimer = null;
}
if (writeBuffer.length > 0) {
terminal.write(writeBuffer.join(""));
writeBuffer.length = 0;
@ -464,7 +482,8 @@ export function DirectTerminal({
};
}, [fullscreen]);
const accentColor = variant === "orchestrator" ? "var(--color-accent-violet)" : "var(--color-accent)";
const accentColor =
variant === "orchestrator" ? "var(--color-accent-violet)" : "var(--color-accent)";
const statusDotClass =
status === "connected"
@ -474,11 +493,7 @@ export function DirectTerminal({
: "bg-[var(--color-status-attention)] animate-[pulse_1.5s_ease-in-out_infinite]";
const statusText =
status === "connected"
? "Connected"
: status === "error"
? (error ?? "Error")
: "Connecting…";
status === "connected" ? "Connected" : status === "error" ? (error ?? "Error") : "Connecting…";
const statusTextColor =
status === "connected"
@ -498,13 +513,12 @@ export function DirectTerminal({
{/* Terminal chrome bar */}
<div className="flex items-center gap-2 border-b border-[var(--color-border-subtle)] bg-[var(--color-bg-elevated)] px-3 py-2">
<div className={cn("h-2 w-2 shrink-0 rounded-full", statusDotClass)} />
<span
className="font-[var(--font-mono)] text-[11px]"
style={{ color: accentColor }}
>
<span className="font-[var(--font-mono)] text-[11px]" style={{ color: accentColor }}>
{sessionId}
</span>
<span className={cn("text-[10px] font-medium uppercase tracking-[0.06em]", statusTextColor)}>
<span
className={cn("text-[10px] font-medium uppercase tracking-[0.06em]", statusTextColor)}
>
{statusText}
</span>
{/* XDA clipboard badge */}
@ -517,20 +531,68 @@ export function DirectTerminal({
>
XDA
</span>
{reloadCommand ? (
<button
onClick={handleReload}
disabled={reloading}
className="ml-auto flex items-center gap-1 rounded px-2 py-0.5 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-text-primary)] disabled:cursor-not-allowed disabled:opacity-70"
>
{reloading ? (
<>
<svg
className="h-3 w-3 animate-spin"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M12 3a9 9 0 109 9" />
</svg>
reloading
</>
) : (
<>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M21 12a9 9 0 11-2.64-6.36" />
<path d="M21 3v6h-6" />
</svg>
reload
</>
)}
</button>
) : null}
<button
onClick={() => setFullscreen(!fullscreen)}
className="ml-auto flex items-center gap-1 rounded px-2 py-0.5 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-text-primary)]"
className="flex items-center gap-1 rounded px-2 py-0.5 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-text-primary)]"
>
{fullscreen ? (
<>
<svg className="h-3 w-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3" />
</svg>
exit fullscreen
</>
) : (
<>
<svg className="h-3 w-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" />
</svg>
fullscreen

View File

@ -207,28 +207,14 @@ export function SessionDetail({
const terminalHeight = isOrchestrator ? "calc(100vh - 240px)" : "max(440px, calc(100vh - 440px))";
const isOpenCodeSession = session.metadata["agent"] === "opencode";
const [remapState, setRemapState] = useState<"idle" | "running" | "ok" | "error">("idle");
async function handleForceRemap() {
if (remapState === "running") return;
setRemapState("running");
try {
const res = await fetch(`/api/sessions/${encodeURIComponent(session.id)}/remap`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ force: true }),
});
if (!res.ok) {
setRemapState("error");
return;
}
setRemapState("ok");
setTimeout(() => setRemapState("idle"), 3000);
} catch {
setRemapState("error");
setTimeout(() => setRemapState("idle"), 3000);
}
}
const opencodeSessionId =
typeof session.metadata["opencodeSessionId"] === "string" &&
session.metadata["opencodeSessionId"].length > 0
? session.metadata["opencodeSessionId"]
: undefined;
const reloadCommand = opencodeSessionId
? `/exit\nopencode --session ${opencodeSessionId}\n`
: undefined;
return (
<div className="min-h-screen bg-[var(--color-bg-base)]">
@ -266,21 +252,6 @@ export function SessionDetail({
orchestrator
</span>
)}
{isOpenCodeSession && (
<button
onClick={handleForceRemap}
disabled={remapState === "running"}
className="ml-auto rounded border border-[var(--color-border-subtle)] bg-[rgba(255,255,255,0.04)] px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.05em] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-60"
>
{remapState === "running"
? "Remapping..."
: remapState === "ok"
? "Remapped"
: remapState === "error"
? "Remap Failed"
: "Force Remap"}
</button>
)}
</div>
</nav>
@ -424,6 +395,7 @@ export function SessionDetail({
startFullscreen={startFullscreen}
variant={terminalVariant}
height={terminalHeight}
reloadCommand={isOpenCodeSession ? reloadCommand : undefined}
/>
</div>
</div>