feat: add prepared authoring assistant pane

This commit is contained in:
lda
2026-07-12 06:21:33 +07:00 Verified
parent c5f9bed26d
commit bfad5d33b1
4 changed files with 152 additions and 0 deletions
@@ -0,0 +1,32 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { PresentationAssistantPane } from "./PresentationAssistantPane.js";
afterEach(cleanup);
describe("PresentationAssistantPane", () => {
it("renders a persistent prepared replay surface for the current phase", () => {
render(<PresentationAssistantPane phase="validate" />);
expect(screen.getByRole("complementary", { name: /prepared authoring assistant/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /authoring assistant/i })).toBeInTheDocument();
expect(screen.getByText(/current phase: validate/i)).toBeInTheDocument();
expect(screen.getByText(/prepared replay only/i)).toBeInTheDocument();
});
it("keeps the active tool group synchronized with the phase", () => {
render(<PresentationAssistantPane phase="artifact" />);
expect(screen.getByRole("button", { name: /artifact.*3 tool calls/i }))
.toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("button", { name: /validate.*2 tool calls/i }))
.toHaveAttribute("aria-expanded", "false");
});
it("does not expose a live or run action", () => {
render(<PresentationAssistantPane phase="deployment" />);
expect(screen.queryByRole("button", { name: /run|send|execute/i })).not.toBeInTheDocument();
expect(screen.queryByText(/workflow\.runs\.start/i)).not.toBeInTheDocument();
});
});
@@ -0,0 +1,45 @@
import { AuthoringConversation } from "./AuthoringConversation.js";
import type { AuthoringPhaseId } from "./authoring-recording.js";
type PresentationAssistantPaneProps = {
readonly phase: AuthoringPhaseId;
};
const phaseLabels: Readonly<Record<AuthoringPhaseId, string>> = {
discover: "Discover",
draft: "Draft",
validate: "Validate",
artifact: "Artifact",
deployment: "Deployment",
};
/**
* Stable Scene 9 boundary for the prepared assistant surface.
*
* The pane deliberately passes no action handlers: its conversation is a
* replay projection, not a live assistant runtime or authoring client.
*/
export const PresentationAssistantPane = ({ phase }: PresentationAssistantPaneProps) => (
<aside
className="presentation-assistant-pane"
aria-label="prepared authoring assistant"
data-phase={phase}
data-surface="prepared-replay"
>
<header className="presentation-assistant-pane__header">
<p className="presentation-assistant-pane__eyebrow">Prepared workflow</p>
<h2>Authoring assistant</h2>
<p>Current phase: {phaseLabels[phase]}</p>
<p className="presentation-assistant-pane__disclosure">
Prepared replay only. No live actions or RPC calls.
</p>
</header>
<div className="presentation-assistant-pane__conversation">
<AuthoringConversation
throughPhase={phase}
activePhase={phase}
surface="dock"
/>
</div>
</aside>
);
@@ -3256,6 +3256,58 @@
overflow: hidden;
}
.presentation-assistant-pane {
display: flex;
min-width: 0;
min-height: 0;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--stage-line);
border-radius: 0.65rem;
background: var(--stage-surface);
}
.presentation-assistant-pane__header {
flex: 0 0 auto;
padding: 0.85rem 0.9rem 0.7rem;
border-bottom: 1px solid var(--stage-line);
}
.presentation-assistant-pane__header h2,
.presentation-assistant-pane__header p {
margin: 0;
}
.presentation-assistant-pane__header h2 {
font: 600 1rem/1.2 var(--font-interface);
}
.presentation-assistant-pane__header > p:not(.presentation-assistant-pane__eyebrow):not(.presentation-assistant-pane__disclosure) {
margin-top: 0.35rem;
color: var(--text-secondary);
font-size: 0.78rem;
}
.presentation-assistant-pane__eyebrow {
margin-bottom: 0.3rem !important;
color: var(--accent-cyan);
font: 600 0.62rem/1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.presentation-assistant-pane__disclosure {
margin-top: 0.65rem !important;
color: var(--text-secondary);
font-size: 0.68rem;
line-height: 1.35;
}
.presentation-assistant-pane__conversation {
min-height: 0;
overflow: auto;
}
.prepared-lifecycle-scene[data-presentation-surface="editorial"] {
--authoring-paper: var(--color-editorial-paper, oklch(0.975 0.012 82));
--authoring-ink: var(--color-editorial-ink, oklch(0.19 0.015 65));
+23
View File
@@ -509,3 +509,26 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
display: block;
color: var(--color-slate);
}
@custom-variant data-open (&:where([data-state="open"], [data-open]:not([data-open="false"])));
@custom-variant data-closed (&:where([data-state="closed"], [data-closed]:not([data-closed="false"])));
@theme inline {
@keyframes collapsible-down {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto));
}
}
@keyframes collapsible-up {
from {
height: var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto));
}
to {
height: 0;
}
}
}