feat: replace force remap with terminal reload control
This commit is contained in:
parent
820cbab00c
commit
cf2b165b46
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue