From 90fed7fb4ff8ed7042c3d97a0195ccc9a1d419b4 Mon Sep 17 00:00:00 2001 From: hshum Date: Thu, 16 Jul 2026 22:55:31 -0700 Subject: [PATCH] refactor(chat): clarify the single action surface --- .../ai-ui/PrimitiveMigration.guard.test.ts | 1 - src/features/redesign/README.md | 10 +- src/features/redesign/RedesignShell.tsx | 19 +- src/features/redesign/agent-workspace.css | 56 +- .../redesign/components/ChatEmptyState.tsx | 2 - .../redesign/components/RightInspector.tsx | 871 ------------------ .../redesign/components/TopNav.test.tsx | 6 +- src/features/redesign/components/TopNav.tsx | 16 +- .../components/UniversalComposer.test.tsx | 4 +- .../redesign/components/UniversalComposer.tsx | 18 +- .../AgentWorkspaceHonesty.guard.test.ts | 18 +- .../redesign/surfaces/ChatSurface.tsx | 466 +--------- .../surfaces/OneSurface.guard.test.ts | 15 +- .../redesign/surfaces/WorkspaceSurface.tsx | 2 +- 14 files changed, 129 insertions(+), 1375 deletions(-) delete mode 100644 src/features/redesign/components/RightInspector.tsx diff --git a/src/components/ai-ui/PrimitiveMigration.guard.test.ts b/src/components/ai-ui/PrimitiveMigration.guard.test.ts index 47945bb72..c91dbfcdd 100644 --- a/src/components/ai-ui/PrimitiveMigration.guard.test.ts +++ b/src/components/ai-ui/PrimitiveMigration.guard.test.ts @@ -26,7 +26,6 @@ const migratedSurfaces = [ "src/features/redesign/components/CommandPalette.tsx", "src/layouts/chrome/CommandPalette.tsx", "src/features/redesign/components/MessageActions.tsx", - "src/features/redesign/components/RightInspector.tsx", "src/shared/ui/SurfacePrimitives.tsx", "src/features/redesign/surfaces/ReportsSurface.tsx", "src/layouts/settings/SettingsModal.tsx", diff --git a/src/features/redesign/README.md b/src/features/redesign/README.md index 112ab2e74..73b51df3a 100644 --- a/src/features/redesign/README.md +++ b/src/features/redesign/README.md @@ -21,8 +21,9 @@ All redesign code is scoped to `[data-redesign]`. The active shell shares the li ## File map -The active runtime tree is `RedesignShell` → `TopNav` + `ChatSurface` + the conditional -`RightInspector`. The remaining historical surface modules are retained only as +The active runtime tree is `RedesignShell` → `TopNav` + `ChatSurface`. Runtime scope, +evidence, receipts, and source review live progressively inside the conversation instead +of a parallel inspector. The remaining historical surface modules are retained only as unmounted migration references while their reusable runtime fragments are folded into the conversation; adding them back to `RedesignShell` violates the one-surface guard. @@ -39,7 +40,6 @@ src/features/redesign/ │ ├── UniversalComposer.tsx Claude/ChatGPT-style composer · tier dropdown · + menu · send circle │ ├── Pill.tsx tone-mapped status pill (accent / green / blue / amber / red) │ ├── CardStack.tsx 3-column max graph traversal · breadcrumb + Back · drill / promote -│ ├── RightInspector.tsx chat right rail · Report status · Active entity · Graph · Sources · Threads │ └── MobileShell.tsx capture-first phone shell · bottom 5-tab nav · bottom sheets │ └── surfaces/ @@ -57,7 +57,7 @@ src/features/redesign/ | Path | Renders | |---|---| -| `/redesign/chat` | The canonical `ChatSurface`, with a contextual runtime inspector after a run starts | +| `/redesign/chat` | The canonical `ChatSurface`, with progressive runtime and evidence detail inside each run | | `/redesign/chat/r/:hash` | The same conversation with an immutable receipt loaded as context and the composer still available | | `/redesign`, `/redesign/reports/*`, `/redesign/inbox`, `/redesign/me`, `/redesign/workspace` | Context-preserving replace into `/redesign/chat` | | Main-site `/reports/:id` and its brief/cards/notebook/sources/graph/map tabs | Report and artifact context in `/redesign/chat`; recursive editing stays on the Workspace hostname | @@ -165,7 +165,7 @@ import { } from "@/features/redesign/components/UniversalComposer"; {/* open picker */}} // chip click onSubmit={(text, tier) => {/* dispatch */}} // Enter or send-button click tier="auto" // controlled tier (omit for uncontrolled) diff --git a/src/features/redesign/RedesignShell.tsx b/src/features/redesign/RedesignShell.tsx index 84dab4c9d..3d79c0d64 100644 --- a/src/features/redesign/RedesignShell.tsx +++ b/src/features/redesign/RedesignShell.tsx @@ -13,10 +13,8 @@ import "./tokens.css"; import "./primitives.css"; import "./agent-workspace.css"; -import { RightInspector, type AgentRailSnapshot } from "./components/RightInspector"; import { TopNav } from "./components/TopNav"; import { ChatSurface } from "./surfaces/ChatSurface"; -import type { LiveArtifactDetail } from "./hooks/useLiveArtifacts"; import { parseChatLaunchParams } from "./lib/chatContinuation"; import { canonicalRedesignChatTarget, pathToChatHash } from "./lib/oneSurfaceRouting"; @@ -29,8 +27,6 @@ export default function RedesignShell() { const location = useLocation(); const navigate = useNavigate(); const [theme, setTheme] = useState<"light" | "dark">(readInitialTheme); - const [activeChatDetail, setActiveChatDetail] = useState(null); - const [activeChatAgentRail, setActiveChatAgentRail] = useState(null); const canonicalTarget = useMemo( () => canonicalRedesignChatTarget(location.pathname, location.search), @@ -39,7 +35,6 @@ export default function RedesignShell() { const chatLaunch = useMemo(() => parseChatLaunchParams(location.search), [location.search]); const routeChatHash = useMemo(() => pathToChatHash(location.pathname), [location.pathname]); const continuationHash = chatLaunch.continuationHash ?? routeChatHash ?? undefined; - const showChatInspector = Boolean(activeChatAgentRail?.hasIntent); useEffect(() => { if (canonicalTarget) navigate(canonicalTarget, { replace: true }); @@ -62,6 +57,10 @@ export default function RedesignShell() { data-redesign data-redesign-theme={theme} data-product-surface="decision-workspace" + data-screen-id="decision-workspace" + data-screen-title="NodeBench" + data-screen-path="/redesign/chat" + data-screen-state="ready" style={{ height: "100dvh", overflow: "hidden", @@ -75,7 +74,7 @@ export default function RedesignShell() { />
@@ -84,18 +83,10 @@ export default function RedesignShell() {
- {showChatInspector && ( - - )}
diff --git a/src/features/redesign/agent-workspace.css b/src/features/redesign/agent-workspace.css index e2ef54fb7..3a56b4210 100644 --- a/src/features/redesign/agent-workspace.css +++ b/src/features/redesign/agent-workspace.css @@ -6,17 +6,9 @@ } [data-redesign] .rd-shell--chat-v3 .rd-shell__main { - grid-template-columns: minmax(0, 1fr) 320px; -} - -[data-redesign] .rd-shell--chat-v3.rd-shell--chat-no-inspector .rd-shell__main { grid-template-columns: minmax(0, 1fr); } -[data-redesign] .rd-shell--chat-v3 .rd-pane--right.chat-context { - width: 320px; -} - [data-redesign] .rd-shell--chat-v3 .rd-shell__main > .rd-pane:first-child { align-items: stretch; padding: 0; @@ -157,13 +149,30 @@ } .rd-account-menu { - min-width: 170px; + width: min(340px, calc(100vw - 24px)); border-color: var(--rd-line); background: var(--rd-panel); color: var(--rd-ink); font-family: var(--rd-font-sans); } +.rd-account-menu .rd-account-voice { + margin: 4px; + border-radius: 12px; + padding: 12px; + box-shadow: none; +} + +[data-redesign] .rd-composer-submit { + width: 36px; + height: 36px; + min-height: 36px; + padding: 0; + border-radius: 50%; + display: inline-grid; + place-items: center; +} + [data-redesign] .rd-launch-context { display: grid; gap: 4px; @@ -204,13 +213,26 @@ list-style-position: inside; } -[data-redesign] .rd-run-scope > summary span:nth-of-type(2) { +[data-redesign] .rd-run-scope__icon { + flex: 0 0 auto; + color: var(--rd-green); +} + +[data-redesign] .rd-run-scope > summary span:first-of-type { + min-width: 0; + flex: 1; max-width: 36ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +[data-redesign] .rd-run-scope > summary span:last-of-type { + flex: 0 0 auto; + color: var(--rd-ink-soft); + white-space: nowrap; +} + [data-redesign] .rd-run-scope dl { display: grid; gap: 6px; @@ -259,12 +281,6 @@ line-height: 1.2; } -[data-redesign] .rd-chat-empty__sub { - max-width: 58ch; - font-size: 14px; - line-height: 1.5; -} - [data-redesign] .rd-chat-empty__chips { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; @@ -296,18 +312,14 @@ gap: 8px; } - [data-redesign] .rd-run-scope > summary span:nth-of-type(2) { + [data-redesign] .rd-run-scope > summary span:first-of-type { display: block; min-width: 0; flex: 1; max-width: none; } - [data-redesign] .rd-run-scope > summary span:nth-of-type(3) { - display: none; - } - - [data-redesign] .rd-run-scope > summary span:nth-of-type(4) { + [data-redesign] .rd-run-scope > summary span:last-of-type { flex: 0 0 auto; } diff --git a/src/features/redesign/components/ChatEmptyState.tsx b/src/features/redesign/components/ChatEmptyState.tsx index ae541f6d5..d1d3d6d38 100644 --- a/src/features/redesign/components/ChatEmptyState.tsx +++ b/src/features/redesign/components/ChatEmptyState.tsx @@ -20,9 +20,7 @@ export function ChatEmptyState({ onPick, starters = STARTERS }: ChatEmptyStatePr return (
-

What do you need to know?

-

Ask from saved memory or start a sourced research run.

diff --git a/src/features/redesign/components/RightInspector.tsx b/src/features/redesign/components/RightInspector.tsx deleted file mode 100644 index 4bf98c18f..000000000 --- a/src/features/redesign/components/RightInspector.tsx +++ /dev/null @@ -1,871 +0,0 @@ -/** - * RightInspector - chat-page right rail. - * - * The rail can be driven by an explicit active live artifact from Chat. When - * no prop is passed, it falls back to the saved live artifact list. - */ - -import { useState, type ReactNode } from "react"; -import { Pill } from "./Pill"; -import { useLiveArtifacts, type LiveArtifactDetail } from "../hooks/useLiveArtifacts"; -import { VoiceCostBadge } from "@/features/voice"; -import { useCurrentUserId } from "@/hooks/useCurrentUser"; -import { buildGraphContextBridgePacket } from "../lib/graphContextBridge"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ai-ui/tabs"; - -export type AgentRailStatus = "done" | "running" | "queued" | "blocked" | "idle"; - -export interface AgentRailItem { - id: string; - label: string; - status?: AgentRailStatus; - detail?: string; - meta?: string; - href?: string; -} - -export interface AgentRailMetric { - label: string; - value: string; - tone?: "green" | "amber" | "accent"; -} - -export interface AgentWorkspaceContract { - objective: string; - reads: string; - writes: string; - verification: string; - reviewHref?: string; -} - -export interface AgentRailSnapshot { - title: string; - subtitle?: string; - /** True only after a user prompt or runtime run exists. */ - hasIntent?: boolean; - status: "idle" | "thinking" | "ok" | "error"; - runId?: string; - progress: AgentRailItem[]; - runDetails: AgentRailItem[]; - artifacts: AgentRailItem[]; - backgroundAgents: AgentRailItem[]; - sources: AgentRailItem[]; - metrics?: AgentRailMetric[]; - contract?: AgentWorkspaceContract; -} - -const chatContextTabs = [ - { id: "session", label: "Run" }, - { id: "trace", label: "Evidence" }, -] as const; - -type ChatContextTab = typeof chatContextTabs[number]["id"]; - -interface RightInspectorProps { - activeLiveArtifactDetail?: LiveArtifactDetail | null; - agentSnapshot?: AgentRailSnapshot | null; -} - -export function RightInspector({ activeLiveArtifactDetail, agentSnapshot }: RightInspectorProps = {}) { - const [activeTab, setActiveTab] = useState("session"); - const liveArtifacts = useLiveArtifacts(12); - const detail = activeLiveArtifactDetail === undefined - ? liveArtifacts.details[0] - : activeLiveArtifactDetail ?? undefined; - const resolvedAgentSnapshot = agentSnapshot ?? buildFallbackAgentSnapshot(detail, liveArtifacts.isLoading); - const graphPacket = detail ? buildGraphContextBridgePacket({ detail, mode: "agent" }) : null; - // HONEST_STATUS: only mount VoiceCostBadge for signed-in viewers. - // `null` = signed out (skip), `undefined` = loading auth (skip until known). - const userId = useCurrentUserId(); - - return ( - - ); -} - -function SessionContextPanel({ snapshot }: { snapshot: AgentRailSnapshot }) { - const contract = snapshot.contract ?? { - objective: snapshot.subtitle ?? "Waiting for a request.", - reads: "No runtime context recorded.", - writes: "No automatic writes.", - verification: "Not started.", - }; - return ( - <> -
-
-
-
Current run
- {snapshot.title} -
- -
-

{contract.objective}

- {snapshot.runId && run {snapshot.runId}} -
- - -
-
Reads
{contract.reads}
-
Writes
{contract.writes}
-
Checks
{contract.verification}
-
-
- - - {snapshot.progress.map((item) => ( -
- - - {item.label} - {item.detail && {item.detail}} - -
- ))} -
- - - {snapshot.artifacts.slice(0, 4).map((item) => ( - - ))} - {snapshot.artifacts.length > 4 && ( -
Show {snapshot.artifacts.length - 4} more
- )} -
- - {contract.reviewHref && ( -
- Review sources here - Inspect sources and proposed changes before any shared write. -
- )} - - ); -} - -function TraceContextPanel({ - snapshot, - graphPacket, -}: { - snapshot: AgentRailSnapshot; - graphPacket: ReturnType | null; -}) { - const traceItems = traceRowsForSnapshot(snapshot); - const toolCalls = metricValue(snapshot, "tools", String(traceItems.length)); - const latency = metricValue(snapshot, "latency", traceLatencyFallback(snapshot)); - const contextSize = metricValue(snapshot, "context", graphPacket ? graphPacket.packedNodes.toLocaleString() : "0"); - const cost = metricValue(snapshot, "cost", metricValue(snapshot, "paid", "$0.00")); - - return ( - <> -
- - - - -
- -
- {traceItems.map((item, index) => ( -
-
{traceIconGlyph(item)}
-
-
{traceName(item)}
-
- {item.status ?? "not recorded"} - {item.detail ?? item.meta ?? "No runtime detail recorded."} -
-
-
- ))} -
- - - {snapshot.sources.map((item) => ( - - ))} - - -
-
Model usage
-
- Router tier - {runDetailValue(snapshot, "routing")} -
-
- Graph packet - {graphPacket ? `${graphPacket.packedNodes} nodes` : "not packed"} -
-
- Topology - {graphPacket?.topology ? `${graphPacket.topology.view} ${graphPacket.topology.densityScore}` : "not attached"} -
-
- Write boundary - {snapshot.contract?.writes ?? "No automatic writes"} -
-
- - ); -} - -function ChatContextSection({ title, children }: { title: string; children: ReactNode }) { - return ( -
-
{title}
- {children} -
- ); -} - -function ContextArtifactRow({ item }: { item: AgentRailItem }) { - const content = ( - <> -
@@ -744,17 +746,15 @@ export function UniversalComposer({ disabled={!text.trim()} aria-label="Run research" title="Run research (Enter)" - className="rd-btn rd-btn--primary rd-btn--sm" + className="rd-btn rd-btn--primary rd-composer-submit" style={{ opacity: text.trim() ? 1 : 0.55, cursor: text.trim() ? "pointer" : "not-allowed", - gap: 6, }} > -
- + - Review mode - {contract?.reads ?? "Prompt only"} - - No automatic shared writes + + {snapshot.reads} + Review · no shared writes
-
Reads
{contract?.reads ?? "No runtime context recorded."}
-
Writes
{contract?.writes ?? "Review mode · no automatic shared writes"}
-
Checks
{contract?.verification ?? "Not started"}
+
Reads
{snapshot.reads}
+
Writes
{snapshot.writes}
+
Checks
{snapshot.verification}
); diff --git a/src/features/redesign/surfaces/OneSurface.guard.test.ts b/src/features/redesign/surfaces/OneSurface.guard.test.ts index c80446e55..d3086611f 100644 --- a/src/features/redesign/surfaces/OneSurface.guard.test.ts +++ b/src/features/redesign/surfaces/OneSurface.guard.test.ts @@ -10,7 +10,6 @@ describe("NodeBench one-surface product contract", () => { const shell = read("src/features/redesign/RedesignShell.tsx"); const header = read("src/features/redesign/components/TopNav.tsx"); const chat = read("src/features/redesign/surfaces/ChatSurface.tsx"); - const inspector = read("src/features/redesign/components/RightInspector.tsx"); const workspaceCss = read("src/features/redesign/agent-workspace.css"); it("mounts only the canonical decision workspace in the primary shell", () => { @@ -23,6 +22,7 @@ describe("NodeBench one-surface product contract", () => { expect(shell).not.toContain(" { @@ -38,22 +38,19 @@ describe("NodeBench one-surface product contract", () => { }); it("keeps contextual chat actions inside the same surface", () => { - for (const source of [chat, inspector]) { - expect(source).not.toMatch(/\/redesign\/(?:reports|inbox|me|workspace)/); - } + expect(chat).not.toMatch(/\/redesign\/(?:reports|inbox|me|workspace)/); expect(chat).toContain("/redesign/chat?report="); expect(chat).toContain("requestedIntent"); expect(chat).toContain(" { - expect(workspaceCss).toMatch(/span:nth-of-type\(2\)[^{]*\{[^}]*display:\s*block/s); - expect(workspaceCss).not.toMatch(/span:nth-of-type\(2\)[^{]*,[^{]*span:nth-of-type\(3\)[^{]*\{[^}]*display:\s*none/s); + expect(workspaceCss).toMatch(/span:first-of-type[^{]*\{[^}]*display:\s*block/s); + expect(workspaceCss).not.toMatch(/rd-run-scope[^}]*display:\s*none/s); }); it("routes the root application to chat on desktop and mobile", () => { diff --git a/src/features/redesign/surfaces/WorkspaceSurface.tsx b/src/features/redesign/surfaces/WorkspaceSurface.tsx index 1c17c5571..d99a24acc 100644 --- a/src/features/redesign/surfaces/WorkspaceSurface.tsx +++ b/src/features/redesign/surfaces/WorkspaceSurface.tsx @@ -463,7 +463,7 @@ export function WorkspaceSurface({ reportId, initialTab = "brief", buildRoute }: ? : )} - {tab === "chat" && } + {tab === "chat" && } {tab === "map" && ( workspaceDetail ? (