({ kind: "worker" });
const session = workspaces.flatMap((workspace) => workspace.sessions).find((s) => s.id === sessionId);
const isOrchestrator = session ? isOrchestratorSession(session) : false;
+ useEffect(() => {
+ setTerminalTarget({ kind: "worker" });
+ }, [sessionId]);
+
// Orchestrator sessions are terminal-only; only worker sessions have the rail.
const hasInspector = !isOrchestrator;
// Computed when the inspector panel mounts and frozen while it stays
@@ -148,7 +154,13 @@ export function SessionView({ sessionId }: SessionViewProps) {
{/* react-resizable-panels v4: bare numbers are PIXELS; percentages must
be strings. Numeric sizes here once clamped the inspector to 45px. */}
-
+ setTerminalTarget({ kind: "worker" })}
+ session={session}
+ terminalTarget={terminalTarget}
+ theme={theme}
+ />
{hasInspector ? (
<>
@@ -171,7 +183,12 @@ export function SessionView({ sessionId }: SessionViewProps) {
{/* Stable content width while the panel animates (yyork pattern):
the pane clips instead of reflowing the inspector mid-collapse. */}
-
+
+ setTerminalTarget({ kind: "reviewer", handleId, harness })
+ }
+ session={session}
+ />
>
diff --git a/frontend/src/renderer/components/TerminalPane.tsx b/frontend/src/renderer/components/TerminalPane.tsx
index 4ec8330e..59a6b9ff 100644
--- a/frontend/src/renderer/components/TerminalPane.tsx
+++ b/frontend/src/renderer/components/TerminalPane.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
+import type { TerminalTarget } from "../types/terminal";
import type { WorkspaceSession } from "../types/workspace";
import type { Theme } from "../stores/ui-store";
import { useTerminalSession, type AttachableTerminal, type TerminalSessionState } from "../hooks/useTerminalSession";
@@ -8,14 +9,16 @@ type TerminalPaneProps = {
session?: WorkspaceSession;
theme: Theme;
daemonReady: boolean;
+ terminalTarget?: TerminalTarget;
};
-export function TerminalPane({ session, theme, daemonReady }: TerminalPaneProps) {
+export function TerminalPane({ session, theme, daemonReady, terminalTarget }: TerminalPaneProps) {
if (!window.ao) {
+ const provider = terminalTarget?.kind === "reviewer" ? terminalTarget.harness : (session?.provider ?? "claude");
return (
~/{session?.workspaceName ?? "reverbcode"}{" "}
- {session?.branch || "main"} $ {session?.provider ?? "claude"}
+ {session?.branch || "main"} $ {provider}
{"\n"}
✻ Welcome to the agent CLI
{"\n\n"}
@@ -26,7 +29,7 @@ export function TerminalPane({ session, theme, daemonReady }: TerminalPaneProps)
);
}
- return ;
+ return ;
}
function bannerText(state: TerminalSessionState, error?: string): string | undefined {
@@ -35,15 +38,19 @@ function bannerText(state: TerminalSessionState, error?: string): string | undef
return undefined;
}
-function AttachedTerminal({ session, theme, daemonReady }: TerminalPaneProps) {
+function AttachedTerminal({ session, theme, daemonReady, terminalTarget }: TerminalPaneProps) {
+ const attachSession =
+ session && terminalTarget?.kind === "reviewer"
+ ? { ...session, terminalHandleId: terminalTarget.handleId }
+ : session;
// One terminal instance per pane lifetime (yyork's core rule): switching
// sessions never remounts XtermTerminal — the attachment effect re-points
// the mux and clears the screen instead. A keyed remount would tear down the
// renderer mid-switch and lose the warm GPU surface.
const [terminal, setTerminal] = useState(null);
const [initFailed, setInitFailed] = useState(false);
- const { attach, state, error } = useTerminalSession(session, { daemonReady });
- const handleId = session?.terminalHandleId;
+ const { attach, state, error } = useTerminalSession(attachSession, { daemonReady });
+ const handleId = attachSession?.terminalHandleId;
const hadAttachmentRef = useRef(false);
const handleReady = useCallback((handle: AttachableTerminal) => setTerminal(handle), []);
diff --git a/frontend/src/renderer/styles.css b/frontend/src/renderer/styles.css
index 10ad8ba8..5032a43a 100644
--- a/frontend/src/renderer/styles.css
+++ b/frontend/src/renderer/styles.css
@@ -575,6 +575,77 @@ body.is-resizing-x [data-slot="sidebar-container"] {
background: var(--border);
}
+.reviewer-terminal-header {
+ display: flex;
+ height: 36px;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 12px;
+ border-bottom: 1px solid var(--border);
+ background: var(--term-bg);
+ padding: 0 14px;
+ font-family: var(--font-mono);
+}
+
+.reviewer-terminal-header__back {
+ display: inline-flex;
+ height: 24px;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 5px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: var(--bg-1);
+ padding: 0 8px;
+ font-size: 11px;
+ color: var(--fg-muted);
+ transition:
+ background 0.12s ease,
+ color 0.12s ease;
+}
+
+.reviewer-terminal-header__back:hover {
+ background: var(--interactive-hover);
+ color: var(--fg);
+}
+
+.reviewer-terminal-header__back svg,
+.reviewer-terminal-header__role svg {
+ width: 13px;
+ height: 13px;
+ flex-shrink: 0;
+}
+
+.reviewer-terminal-header__role {
+ display: inline-flex;
+ height: 24px;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 6px;
+ border-radius: 6px;
+ border: 1px solid color-mix(in srgb, var(--green) 34%, transparent);
+ background: color-mix(in srgb, var(--green) 9%, transparent);
+ padding: 0 9px;
+ font-size: 11px;
+ font-weight: 700;
+ color: #8bdc75;
+ text-transform: uppercase;
+}
+
+:root[data-theme="light"] .reviewer-terminal-header__role {
+ color: var(--green);
+}
+
+.reviewer-terminal-header__harness {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 13px;
+ font-weight: 650;
+ color: var(--fg);
+}
+
/* Collapse/expand animation for the inspector panel: rrp v4 drives panel
* sizes via flex-grow, which is animatable. Suppress it while the separator
* is actively dragged so resizing tracks the pointer 1:1. */
@@ -620,6 +691,187 @@ body.is-resizing-x [data-slot="sidebar-container"] {
line-height: 1.5;
}
+.reviewer-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.reviewer-error {
+ margin: 0;
+ border-radius: 6px;
+ border: 1px solid color-mix(in srgb, var(--red) 28%, transparent);
+ background: color-mix(in srgb, var(--red) 8%, transparent);
+ padding: 8px 10px;
+ font-size: 11.5px;
+ line-height: 1.45;
+ color: var(--red);
+}
+
+.reviewer-notice {
+ margin: 0;
+ border-radius: 6px;
+ border: 1px solid color-mix(in srgb, var(--green) 28%, transparent);
+ background: color-mix(in srgb, var(--green) 8%, transparent);
+ padding: 8px 10px;
+ font-size: 11.5px;
+ line-height: 1.45;
+ color: var(--green);
+}
+
+.reviewer-card {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: var(--bg-1);
+ padding: 14px;
+}
+
+.reviewer-card--success {
+ border-color: color-mix(in srgb, var(--green) 25%, var(--border));
+}
+
+.reviewer-card--danger {
+ border-color: color-mix(in srgb, var(--red) 22%, var(--border));
+}
+
+.reviewer-card__top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ min-width: 0;
+}
+
+.reviewer-card__name {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 9px;
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--fg);
+}
+
+.reviewer-card__name svg {
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+ color: var(--fg-passive);
+}
+
+.reviewer-card__name span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.reviewer-status {
+ display: inline-flex;
+ height: 24px;
+ max-width: 58%;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 5px;
+ overflow: hidden;
+ border-radius: 7px;
+ padding: 0 9px;
+ font-size: 11.5px;
+ font-weight: 650;
+ line-height: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.reviewer-status svg {
+ width: 13px;
+ height: 13px;
+ flex-shrink: 0;
+}
+
+.reviewer-status--neutral {
+ background: var(--bg-2);
+ color: var(--fg-muted);
+}
+
+.reviewer-status--running {
+ background: color-mix(in srgb, var(--orange) 12%, transparent);
+ color: var(--orange);
+}
+
+.reviewer-status--success {
+ background: color-mix(in srgb, var(--green) 14%, transparent);
+ color: var(--green);
+}
+
+.reviewer-status--danger {
+ background: color-mix(in srgb, var(--red) 14%, transparent);
+ color: var(--red);
+}
+
+.reviewer-card__actions {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.reviewer-card__actions:has(.reviewer-card__action:only-child) {
+ grid-template-columns: 1fr;
+}
+
+.reviewer-card__action {
+ display: inline-flex;
+ height: 38px;
+ min-width: 0;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ overflow: hidden;
+ border-radius: 7px;
+ border: 1px solid var(--border);
+ background: var(--bg-2);
+ padding: 0 10px;
+ font-size: 12px;
+ font-weight: 650;
+ color: var(--fg-muted);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ transition:
+ background 0.12s ease,
+ border-color 0.12s ease,
+ color 0.12s ease;
+}
+
+.reviewer-card__action svg {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+
+.reviewer-card__action:hover:not(:disabled) {
+ background: var(--interactive-hover);
+ color: var(--fg);
+}
+
+.reviewer-card__action:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+}
+
+.reviewer-card__action--primary {
+ border-color: color-mix(in srgb, var(--green) 42%, transparent);
+ background: color-mix(in srgb, var(--green) 10%, transparent);
+ color: #8bdc75;
+}
+
+:root[data-theme="light"] .reviewer-card__action--primary {
+ color: var(--green);
+}
+
.inspector-empty--center {
padding: 24px 8px;
text-align: center;
@@ -734,6 +986,10 @@ body.is-resizing-x [data-slot="sidebar-container"] {
background: var(--amber);
}
+.inspector-timeline__ev--bad .inspector-timeline__node {
+ background: var(--red);
+}
+
.inspector-timeline__et {
font-size: 12px;
color: var(--fg);
diff --git a/frontend/src/renderer/types/terminal.ts b/frontend/src/renderer/types/terminal.ts
new file mode 100644
index 00000000..6c22fc31
--- /dev/null
+++ b/frontend/src/renderer/types/terminal.ts
@@ -0,0 +1,7 @@
+export type TerminalTarget =
+ | { kind: "worker" }
+ | {
+ kind: "reviewer";
+ handleId: string;
+ harness: string;
+ };