mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
topology compact terminal preview popover
This commit is contained in:
1 parent
3151a5d9eb
commit
9512ab0502
4 files changed
+117
-36
No files matched your search
@@ -11,23 +11,32 @@
|
||||
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { useSessionPreview, isNodePreviewUnavailable } from "../../hooks/useNodePreview.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
interface SessionPreviewPaneProps {
|
||||
sessionName: string;
|
||||
lines?: number;
|
||||
paused?: boolean;
|
||||
testIdPrefix?: string;
|
||||
variant?: "default" | "compact-terminal";
|
||||
}
|
||||
|
||||
function isNearBottom(el: HTMLElement): boolean {
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight <= 4;
|
||||
}
|
||||
|
||||
export function SessionPreviewPane({ sessionName, lines, paused, testIdPrefix = "session-preview" }: SessionPreviewPaneProps) {
|
||||
export function SessionPreviewPane({
|
||||
sessionName,
|
||||
lines,
|
||||
paused,
|
||||
testIdPrefix = "session-preview",
|
||||
variant = "default",
|
||||
}: SessionPreviewPaneProps) {
|
||||
const preview = useSessionPreview({ sessionName, lines, paused });
|
||||
const contentRef = useRef<HTMLPreElement | null>(null);
|
||||
const shouldFollowTailRef = useRef(true);
|
||||
const content = !isNodePreviewUnavailable(preview.data) ? preview.data?.content : undefined;
|
||||
const compactTerminal = variant === "compact-terminal";
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = contentRef.current;
|
||||
@@ -47,24 +56,45 @@ export function SessionPreviewPane({ sessionName, lines, paused, testIdPrefix =
|
||||
<div
|
||||
data-testid={`${testIdPrefix}-pane`}
|
||||
data-session-name={sessionName}
|
||||
className="border border-stone-300/40 bg-white/8 px-3 py-2 space-y-1"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"space-y-1",
|
||||
compactTerminal
|
||||
? "border-0 bg-transparent p-0 text-stone-100"
|
||||
: "border border-stone-300/40 bg-white/8 px-3 py-2",
|
||||
)}
|
||||
>
|
||||
<div className="font-mono text-[9px] uppercase tracking-[0.10em] text-stone-500 truncate">
|
||||
live preview · {sessionName}
|
||||
</div>
|
||||
{!compactTerminal && (
|
||||
<div className="font-mono text-[9px] uppercase tracking-[0.10em] text-stone-500 truncate">
|
||||
live preview · {sessionName}
|
||||
</div>
|
||||
)}
|
||||
{preview.isLoading && (
|
||||
<div data-testid={`${testIdPrefix}-loading`} className="font-mono text-[9px] text-stone-400">Loading…</div>
|
||||
)}
|
||||
{preview.isError && (
|
||||
<div data-testid={`${testIdPrefix}-error`} className="font-mono text-[9px] text-red-600">
|
||||
<div
|
||||
data-testid={`${testIdPrefix}-error`}
|
||||
className={cn("font-mono text-[9px]", compactTerminal ? "text-red-300" : "text-red-600")}
|
||||
>
|
||||
{(preview.error as Error)?.message ?? "Preview failed."}
|
||||
</div>
|
||||
)}
|
||||
{isNodePreviewUnavailable(preview.data) && (
|
||||
<div data-testid={`${testIdPrefix}-unavailable`} className="font-mono text-[9px] text-stone-500 space-y-0.5">
|
||||
<div
|
||||
data-testid={`${testIdPrefix}-unavailable`}
|
||||
className={cn(
|
||||
"font-mono space-y-0.5",
|
||||
compactTerminal ? "text-[10px] text-stone-300" : "text-[9px] text-stone-500",
|
||||
)}
|
||||
>
|
||||
<div>Preview unavailable: {preview.data.reason}.</div>
|
||||
{preview.data.hint && <div className="text-stone-400">{preview.data.hint}</div>}
|
||||
<div className="text-stone-400">Use <code>rig capture {sessionName}</code> from terminal as a fallback.</div>
|
||||
{preview.data.hint && (
|
||||
<div className={compactTerminal ? "text-stone-500" : "text-stone-400"}>{preview.data.hint}</div>
|
||||
)}
|
||||
<div className={compactTerminal ? "text-stone-500" : "text-stone-400"}>
|
||||
Use <code>rig capture {sessionName}</code> from terminal as a fallback.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isNodePreviewUnavailable(preview.data) && preview.data && (
|
||||
@@ -73,14 +103,28 @@ export function SessionPreviewPane({ sessionName, lines, paused, testIdPrefix =
|
||||
ref={contentRef}
|
||||
data-testid={`${testIdPrefix}-content`}
|
||||
onScroll={handleScroll}
|
||||
className="font-mono text-[9px] text-stone-800 bg-stone-50 px-2 py-1 max-h-32 overflow-y-auto whitespace-pre-wrap break-all"
|
||||
className={cn(
|
||||
"font-mono overflow-y-auto whitespace-pre-wrap break-all",
|
||||
compactTerminal
|
||||
? "max-h-72 bg-transparent px-1 py-0.5 text-[10px] leading-[1.25] text-stone-100"
|
||||
: "max-h-32 bg-stone-50 px-2 py-1 text-[9px] text-stone-800",
|
||||
)}
|
||||
>
|
||||
{preview.data.content || "(empty pane)"}
|
||||
</pre>
|
||||
<div className="font-mono text-[8px] text-stone-400 flex justify-between">
|
||||
<span>captured {new Date(preview.data.capturedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}</span>
|
||||
<span>{preview.data.lines} lines</span>
|
||||
</div>
|
||||
{!compactTerminal && (
|
||||
<div className="font-mono text-[8px] text-stone-400 flex justify-between">
|
||||
<span>
|
||||
captured{" "}
|
||||
{new Date(preview.data.capturedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span>{preview.data.lines} lines</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type React from "react";
|
||||
import { Terminal, X } from "lucide-react";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { SessionPreviewPane } from "../preview/SessionPreviewPane.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
@@ -79,32 +79,19 @@ export function TerminalPreviewPopover({
|
||||
data-testid={`${testIdPrefix}-terminal-popover`}
|
||||
data-reduced-motion={reducedMotion ? "true" : "false"}
|
||||
className={cn(
|
||||
"nodrag nopan absolute left-full top-0 z-[80] ml-2 w-80 border border-stone-900 bg-white p-2 hard-shadow",
|
||||
"cursor-default select-text",
|
||||
"nodrag nopan absolute left-full top-0 z-[80] ml-2 w-80 border border-outline-variant bg-stone-950 p-1.5 hard-shadow",
|
||||
"cursor-default select-text text-stone-100",
|
||||
popoverClassName,
|
||||
)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="truncate font-mono text-[8px] uppercase tracking-[0.14em] text-stone-500">
|
||||
terminal preview
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`${testIdPrefix}-terminal-close`}
|
||||
aria-label="Close terminal preview"
|
||||
title="Close"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setOpen(false);
|
||||
}}
|
||||
className="inline-flex h-5 w-5 items-center justify-center border border-outline-variant bg-white text-stone-600 hover:bg-stone-100 hover:text-stone-950 focus:outline-none focus:ring-2 focus:ring-stone-900/20"
|
||||
>
|
||||
<X className="h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<SessionPreviewPane sessionName={sessionName} lines={80} testIdPrefix={`${testIdPrefix}-terminal-preview`} />
|
||||
<SessionPreviewPane
|
||||
sessionName={sessionName}
|
||||
lines={80}
|
||||
testIdPrefix={`${testIdPrefix}-terminal-preview`}
|
||||
variant="compact-terminal"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -195,6 +195,19 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
|
||||
await waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledWith("/api/sessions/velocity-driver%40openrig-velocity/preview?lines=80");
|
||||
});
|
||||
await screen.findByText("latest terminal line");
|
||||
expect(screen.queryByText(/terminal preview/i)).toBeNull();
|
||||
expect(screen.queryByTestId("hybrid-driver-terminal-close")).toBeNull();
|
||||
expect(screen.queryByText(/live preview/i)).toBeNull();
|
||||
expect(screen.queryByText(/captured/i)).toBeNull();
|
||||
expect(screen.queryByText(/1 lines/i)).toBeNull();
|
||||
expect(screen.getByTestId("hybrid-driver-terminal-preview-pane").getAttribute("data-variant")).toBe("compact-terminal");
|
||||
|
||||
fireEvent.pointerDown(document.body);
|
||||
expect(screen.queryByTestId("hybrid-driver-terminal-popover")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId("hybrid-driver-terminal-open"));
|
||||
expect(screen.getByTestId("hybrid-driver-terminal-popover")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByTestId("hybrid-guard-terminal-open"));
|
||||
expect(screen.getByTestId("hybrid-guard-terminal-popover")).toBeDefined();
|
||||
@@ -228,6 +241,10 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
|
||||
expect(rigNode).toMatch(/getActivityCardClasses/);
|
||||
expect(rigNode).toMatch(/TerminalPreviewPopover/);
|
||||
expect(terminalPopover).toMatch(/SessionPreviewPane/);
|
||||
expect(terminalPopover).toMatch(/variant="compact-terminal"/);
|
||||
expect(terminalPopover).toMatch(/bg-stone-950/);
|
||||
expect(terminalPopover).not.toMatch(/terminal preview/);
|
||||
expect(terminalPopover).not.toMatch(/terminal-close/);
|
||||
expect(activityCards).toMatch(/activity-card-active/);
|
||||
expect(rigGraph).toMatch(/activityRing/);
|
||||
expect(table).toMatch(/ActivityRing/);
|
||||
|
||||
@@ -62,4 +62,37 @@ describe("SessionPreviewPane", () => {
|
||||
expect(content.scrollTop).toBe(240);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders compact terminal variant without metadata chrome", async () => {
|
||||
mockFetch.mockImplementation(async (url: string) => {
|
||||
if (url === "/api/config") return new Response(JSON.stringify(settingsResponse()));
|
||||
if (url.includes("/api/sessions/")) {
|
||||
return new Response(JSON.stringify({
|
||||
sessionName: "driver@test-rig",
|
||||
content: "tail line",
|
||||
lines: 1,
|
||||
capturedAt: "2026-05-07T08:00:00Z",
|
||||
}));
|
||||
}
|
||||
return new Response(JSON.stringify({}), { status: 404 });
|
||||
});
|
||||
|
||||
withQueryClient(
|
||||
<SessionPreviewPane
|
||||
sessionName="driver@test-rig"
|
||||
testIdPrefix="compact-terminal-test"
|
||||
variant="compact-terminal"
|
||||
/>,
|
||||
);
|
||||
|
||||
const pane = await screen.findByTestId("compact-terminal-test-pane");
|
||||
const content = await screen.findByTestId("compact-terminal-test-content");
|
||||
|
||||
expect(pane.getAttribute("data-variant")).toBe("compact-terminal");
|
||||
expect(content.className).toContain("text-[10px]");
|
||||
expect(content.className).toContain("text-stone-100");
|
||||
expect(screen.queryByText(/live preview/i)).toBeNull();
|
||||
expect(screen.queryByText(/captured/i)).toBeNull();
|
||||
expect(screen.queryByText(/1 lines/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user