fix: make presentation problem scene read as tool loop

This commit is contained in:
lda
2026-07-10 04:12:20 +07:00 Verified
parent 277c1b7bd9
commit 942c64a8cb
3 changed files with 77 additions and 16 deletions
@@ -8,15 +8,16 @@ const problemScene = findScene("problem")!;
afterEach(() => cleanup());
describe("ProblemLoopScene", () => {
it("renders the action sequence as useful but insufficient", () => {
it("shows direct action as a vertical chat and tool transcript", () => {
render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "direct-actions")!} />);
const action = screen.getByRole("group", { name: "Action sequence" });
expect(within(action).getByText("think")).toBeInTheDocument();
expect(within(action).getAllByText("tool")).toHaveLength(2);
expect(within(action).getByText("observe")).toBeInTheDocument();
expect(within(action).getByText("done")).toBeInTheDocument();
expect(screen.getByText(/useful once/i)).toBeInTheDocument();
const transcript = screen.getByRole("list", { name: /one-off tool loop transcript/i });
expect(within(transcript).getByText("User prompt")).toBeInTheDocument();
expect(within(transcript).getByText("Agent reasoning")).toBeInTheDocument();
expect(within(transcript).getByText("Tool call")).toBeInTheDocument();
expect(within(transcript).getByText("Observation")).toBeInTheDocument();
expect(within(transcript).getByText("Final answer")).toBeInTheDocument();
expect(screen.queryByRole("group", { name: /^Action sequence$/i })).not.toBeInTheDocument();
});
it("renders reusable automation with simple verbs only", () => {
@@ -30,4 +31,19 @@ describe("ProblemLoopScene", () => {
expect(screen.queryByText(formalName)).not.toBeInTheDocument();
}
});
it("keeps reusable automation as the durable counterpart without formal lifecycle words", () => {
render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "missing-contracts")!} />);
expect(screen.getByRole("group", { name: /reusable automation/i })).toBeInTheDocument();
expect(screen.getByText("design")).toBeInTheDocument();
expect(screen.getByText("save")).toBeInTheDocument();
expect(screen.getByText("connect")).toBeInTheDocument();
expect(screen.getByText("run")).toBeInTheDocument();
expect(screen.getByText("inspect")).toBeInTheDocument();
expect(screen.queryByText("Draft")).not.toBeInTheDocument();
expect(screen.queryByText("Artifact")).not.toBeInTheDocument();
expect(screen.queryByText("Deployment")).not.toBeInTheDocument();
expect(screen.queryByText("Trace")).not.toBeInTheDocument();
});
});
@@ -2,6 +2,14 @@ import { StageCaption } from "../StageCaption.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
import { ConceptNode, ConceptRail } from "./ConceptPrimitives.js";
const toolLoopTurns = [
{ role: "User prompt", detail: "Do this workspace task once." },
{ role: "Agent reasoning", detail: "Decides the next action." },
{ role: "Tool call", detail: "Runs an operation directly." },
{ role: "Observation", detail: "Reads the result." },
{ role: "Final answer", detail: "Reports success, but keeps no reusable lifecycle." },
] as const;
type ProblemLoopSceneProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
@@ -15,15 +23,19 @@ export const ProblemLoopScene = ({ scene, beat }: ProblemLoopSceneProps) => {
<p>{beat.caption}</p>
</StageCaption>
<section className="problem-loop-scene" aria-label="action sequence versus reusable automation">
<div className="problem-loop-scene__side" data-problem-active={automationBeat ? "false" : "true"}>
<h2>Action sequence</h2>
<ConceptRail label="Action sequence">
<ConceptNode title="think" icon="think" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="tool" icon="toolCall" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="observe" icon="observe" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="tool" icon="toolCall" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="done" icon="done" emphasis={automationBeat ? "muted" : "normal"} />
</ConceptRail>
<div
className="problem-loop-scene__side problem-loop-scene__side--transcript"
data-problem-active={automationBeat ? "false" : "true"}
>
<h2>One-off tool loop</h2>
<ol className="problem-loop-transcript" aria-label="one-off tool loop transcript">
{toolLoopTurns.map((turn) => (
<li key={turn.role} className="problem-loop-transcript__turn">
<span>{turn.role}</span>
<p>{turn.detail}</p>
</li>
))}
</ol>
<p>Useful once. Hard to reuse.</p>
</div>
@@ -1437,6 +1437,39 @@
font: 0.95rem/1.35 var(--font-interface);
}
.problem-loop-scene__side--transcript {
align-self: stretch;
}
.problem-loop-transcript {
display: grid;
gap: 0.45rem;
margin: 0;
padding: 0;
list-style: none;
}
.problem-loop-transcript__turn {
display: grid;
grid-template-columns: 7.5rem minmax(0, 1fr);
gap: 0.65rem;
align-items: start;
border: 1px solid color-mix(in oklch, var(--stage-line) 66%, transparent);
border-radius: 0.65rem;
background: color-mix(in oklch, var(--stage-surface) 88%, transparent);
padding: 0.5rem 0.6rem;
}
.problem-loop-transcript__turn span {
color: var(--accent-cyan);
font: 700 0.72rem/1.15 var(--font-evidence);
}
.problem-loop-transcript__turn p {
color: var(--text-primary);
font: 0.8rem/1.25 var(--font-interface);
}
.problem-loop-scene .concept-rail {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));