From a4ff83424df1d8349262ff359888585df151bc35 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:46:42 +0000 Subject: [PATCH] =?UTF-8?q?feat(tui):=20add=20cockpit=20panels=20=E2=80=94?= =?UTF-8?q?=20Mission,=20Safety=20Shield,=20Pulse,=20Reach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the flat status dump with a 2×2 operator command center layout: - Mission: mode, halt status, tier, provider/model - Safety Shield: review queue, DLQ, health bar with run totals - Pulse: reasoner ticks, candidates, tool calls, cost with ASCII bars - Reach: followers, delta, X usage/health (awaiting telemetry) Preserves TaskGraphsPane, InvocationsPane, Footer below the cockpit. Uses green/yellow/red/cyan for severity. ASCII bars via [####──────]. 80-column readability maintained with two 40-col bordered panels per row. Dashboard remains read-only; no live X calls from TUI. Co-Authored-By: Terrence Schonleber --- src/cli/tui/components.tsx | 205 ++++++++++++++++++++++++++++++++++++- src/cli/tui/dashboard.tsx | 58 ++++++++--- tests/cli/tui.test.ts | 74 +++++++++++-- 3 files changed, 310 insertions(+), 27 deletions(-) diff --git a/src/cli/tui/components.tsx b/src/cli/tui/components.tsx index a273859..418c729 100644 --- a/src/cli/tui/components.tsx +++ b/src/cli/tui/components.tsx @@ -78,7 +78,210 @@ function fmtUsdFromTicks(ticks: number): string { return `$${usd.toFixed(2)}`; } -// ─── Header ───────────────────────────────────────────────────────────────── +// ─── ASCII bar helper ──────────────────────────────────────────────────────── + +function asciiBar(value: number, max: number, width = 10): string { + if (max <= 0) return `[${"─".repeat(width)}]`; + const ratio = Math.min(1, Math.max(0, value / max)); + const filled = Math.round(ratio * width); + return `[${"#".repeat(filled)}${"─".repeat(width - filled)}]`; +} + +// ─── Cockpit banner ───────────────────────────────────────────────────────── + +export function CockpitBanner(): ReactElement { + return ( + + + Strand + + — operator cockpit + + ); +} + +// ─── Mission panel ────────────────────────────────────────────────────────── + +export interface MissionPanelProps { + mode: string; + halt: string; + tier: string; + provider: string; + model: string; +} + +export function MissionPanel(props: MissionPanelProps): ReactElement { + const modeColor = props.mode === "live" ? "red" : props.mode === "gated" ? "yellow" : "green"; + const haltOn = props.halt === "true"; + return ( + + + {" "} + MISSION + + + MODE + + {props.mode.padEnd(9)} + + HALT + + {haltOn ? "\u25CF ON" : "\u25CF off"} + + + + TIER + {props.tier} + + + + + {props.provider}/{props.model.slice(0, 28)} + + + + ); +} + +// ─── Safety Shield panel ──────────────────────────────────────────────────── + +export interface SafetyShieldPanelProps { + reviewQueued: number; + reviewActive: number; + dlqFailed: number; + totalRuns: number; + completedRuns: number; +} + +export function SafetyShieldPanel(props: SafetyShieldPanelProps): ReactElement { + const queueColor = props.reviewQueued > 0 ? "yellow" : "green"; + const dlqColor = props.dlqFailed > 0 ? "red" : "green"; + const healthMax = Math.max(props.totalRuns, 1); + const healthBar = asciiBar(props.completedRuns, healthMax, 10); + return ( + + + {" "} + SAFETY SHIELD + + + Review + {String(props.reviewQueued).padStart(3)} queued + + {" "} + {String(props.reviewActive).padStart(2)} active + + + + DLQ + {String(props.dlqFailed).padStart(3)} failed + + + Health + {healthBar} + + {" "} + {props.totalRuns} runs + + + + ); +} + +// ─── Pulse panel ──────────────────────────────────────────────────────────── + +export interface PulsePanelProps { + ticks: number; + candidates: number; + toolCalls: number; + costUsdTicks: number; +} + +export function PulsePanel(props: PulsePanelProps): ReactElement { + const tickMax = Math.max(props.ticks, 100); + const toolMax = Math.max(props.toolCalls, 50); + return ( + + + {" "} + PULSE + + + Ticks + {String(props.ticks).padStart(5)} ticks + {asciiBar(props.ticks, tickMax, 12)} + + + Cands + {String(props.candidates).padStart(5)} candidates + + + Tools + {String(props.toolCalls).padStart(5)} calls + {asciiBar(props.toolCalls, toolMax, 12)} + + + Cost + + {fmtUsdFromTicks(props.costUsdTicks)} + + + + ); +} + +// ─── Reach panel ──────────────────────────────────────────────────────────── + +export interface ReachPanelProps { + followers?: number | null; + delta24h?: number | null; + xUsage?: string | null; + xHealth?: string | null; +} + +export function ReachPanel(props: ReachPanelProps): ReactElement { + const val = (v: number | string | null | undefined, suffix = ""): string => + v != null ? `${v}${suffix}` : "\u2014"; + const healthColor = + props.xHealth === "ok" ? "green" : props.xHealth === "degraded" ? "yellow" : "gray"; + return ( + + + {" "} + REACH + + + Followers + {val(props.followers)} + + + 24h delta + 0 + ? "green" + : props.delta24h != null && props.delta24h < 0 + ? "red" + : "gray" + } + > + {props.delta24h != null && props.delta24h > 0 ? "+" : ""} + {val(props.delta24h)} + + + + X usage + {val(props.xUsage)} + + + X health + {val(props.xHealth)} + + + ); +} + +// ─── Header (legacy) ──────────────────────────────────────────────────────── export interface HeaderProps { provider: string; diff --git a/src/cli/tui/dashboard.tsx b/src/cli/tui/dashboard.tsx index 43a255c..1e3f8ea 100644 --- a/src/cli/tui/dashboard.tsx +++ b/src/cli/tui/dashboard.tsx @@ -1,5 +1,5 @@ /** - * Strand dashboard — the live view behind `strand tui --dashboard`. + * Strand cockpit — the operator command center behind `strand tui --dashboard`. * * Single-screen read-only tree over the local SQLite ops DB: * - polls `agent_task_graphs` / `_steps` for active graphs @@ -13,8 +13,17 @@ import { env } from "@/config"; import { Box, Text, useApp, useInput, useStdin } from "ink"; import type { ReactElement } from "react"; -import { useCallback, useMemo, useState } from "react"; -import { Footer, Header, InvocationsPane, RunSummaryPane, TaskGraphsPane } from "./components"; +import { useCallback, useState } from "react"; +import { + CockpitBanner, + Footer, + InvocationsPane, + MissionPanel, + PulsePanel, + ReachPanel, + SafetyShieldPanel, + TaskGraphsPane, +} from "./components"; import { useRecentInvocations, useRunSummary, useTaskGraphs } from "./hooks"; export interface DashboardProps { @@ -92,20 +101,12 @@ export function Dashboard({ pollMs = 2000, onWelcome }: DashboardProps): ReactEl { isActive: Boolean(isRawModeSupported) }, ); - const header = useMemo( - () => ({ - provider: env.LLM_PROVIDER, - model: env.LLM_MODEL_REASONER, - mode: env.STRAND_MODE, - credentialStore: process.env["STRAND_CREDENTIAL_STORE"] ?? "env", - tenant: process.env["STRAND_TENANT"] ?? null, - }), - [], - ); + const r = summary.data.reasoner; + const c = summary.data.consolidator; return ( -
+ {!isRawModeSupported ? ( @@ -118,6 +119,34 @@ export function Dashboard({ pollMs = 2000, onWelcome }: DashboardProps): ReactEl {"[paused] — press p to resume, r to refresh once"} ) : null} + {/* ── Cockpit top row ── */} + + + + + {/* ── Cockpit bottom row ── */} + + + + + {/* ── Live data panes ── */} - { - it("renders a non-empty frame with mocked data", () => { + it("renders cockpit panels with mocked data", () => { const source = makeStubSource(); const tree = createElement( DataSourceContext.Provider, @@ -106,20 +106,37 @@ describe("strand tui dashboard", () => { const frame = lastFrame() ?? ""; expect(frame.length).toBeGreaterThan(0); - // Header renders provider + mode - expect(frame).toContain("Strand TUI"); + + // Cockpit banner + expect(frame).toContain("Strand"); + expect(frame).toContain("operator cockpit"); + + // Mission panel + expect(frame).toContain("MISSION"); expect(frame).toContain("shadow"); - // A fake graph appears + expect(frame).toContain("HALT"); + + // Safety Shield panel + expect(frame).toContain("SAFETY SHIELD"); + expect(frame).toContain("queued"); + expect(frame).toContain("7 runs"); + + // Pulse panel + expect(frame).toContain("PULSE"); + expect(frame).toContain("42 ticks"); + expect(frame).toContain("$0.18"); + + // Reach panel + expect(frame).toContain("REACH"); + + // Task graph still renders expect(frame).toContain("crawl site X and summarize"); expect(frame).toContain("fetch home page"); - // Invocations pane shows a tool line + + // Invocations pane shows tool lines expect(frame).toContain("http_fetch"); expect(frame).toContain("fs_read"); - // Run summary pane shows reasoner ticks + cost - expect(frame).toContain("42 ticks"); - expect(frame).toContain("$0.18"); - // Consolidator counts - expect(frame).toContain("7 runs"); + // Footer hint expect(frame).toContain("[q] quit"); @@ -148,9 +165,44 @@ describe("strand tui dashboard", () => { ); const { lastFrame, unmount } = render(tree); const frame = lastFrame() ?? ""; - expect(frame).toContain("Strand TUI"); + // Cockpit panels still render + expect(frame).toContain("MISSION"); + expect(frame).toContain("SAFETY SHIELD"); + expect(frame).toContain("PULSE"); + expect(frame).toContain("REACH"); + // Empty state for data panes expect(frame).toContain("(no active graphs)"); expect(frame).toContain("(no invocations yet)"); unmount(); }); + + it("renders ASCII bars in pulse panel", () => { + const source = makeStubSource(); + const tree = createElement( + DataSourceContext.Provider, + { value: source }, + createElement(Dashboard, { pollMs: 10_000 }), + ); + const { lastFrame, unmount } = render(tree); + const frame = lastFrame() ?? ""; + // ASCII bars use # and ─ characters + expect(frame).toMatch(/\[#+─*\]/); + unmount(); + }); + + it("shows safety indicators with correct severity", () => { + const source = makeStubSource(); + const tree = createElement( + DataSourceContext.Provider, + { value: source }, + createElement(Dashboard, { pollMs: 10_000 }), + ); + const { lastFrame, unmount } = render(tree); + const frame = lastFrame() ?? ""; + // DLQ count appears + expect(frame).toContain("1 failed"); + // Queued count appears + expect(frame).toContain("4 queued"); + unmount(); + }); });