Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/desktop/src/screens/CheckIt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ it("approves, locks the version, and offers Run it", async () => {
const onRun = vi.fn();
render(<CheckIt workflowId="wf-1" onExactChecks={() => {}} onRun={onRun} />);

fireEvent.click(await screen.findByRole("button", { name: "Approve for use" }));
fireEvent.click(await screen.findByRole("button", { name: "Approve this workflow" }));

expect(await screen.findByText("Approved and locked")).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Run it" }));
Expand All @@ -322,7 +322,7 @@ it("approves, locks the version, and offers Run it", async () => {
it("keeps approval off until the practice item passed", async () => {
engine(project());
render(<CheckIt workflowId="wf-1" onExactChecks={() => {}} onRun={() => {}} />);
const approve = await screen.findByRole("button", { name: "Approve for use" });
const approve = await screen.findByRole("button", { name: "Approve this workflow" });
expect((approve as HTMLButtonElement).disabled).toBe(true);
});

Expand Down
62 changes: 50 additions & 12 deletions packages/desktop/src/screens/CheckIt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,25 @@ function CheckSection({
>
<StepBadge state={state} index={index} passed={passed} />
<div className="check-step-body">
<div className="check-step-head">
<h2 id={`check-${index}`}>{title}</h2>
<span className="check-step-state">{stateWord(state, doneWord)}</span>
</div>
{children}
{/* A finished part folds to its title so the open question stands out;
the last part keeps its Run button in view. */}
{state === "done" && index < 6 ? (
<details className="check-step-fold">
<summary className="check-step-head">
<h2 id={`check-${index}`}>{title}</h2>
<span className="check-step-state">{stateWord(state, doneWord)}</span>
</summary>
{children}
</details>
) : (
<>
<div className="check-step-head">
<h2 id={`check-${index}`}>{title}</h2>
<span className="check-step-state">{stateWord(state, doneWord)}</span>
</div>
{children}
</>
)}
</div>
</section>
);
Expand Down Expand Up @@ -301,6 +315,18 @@ function EffectQuestion({
);
}

/** Armed saving steps grouped by their identical sentence, in first-seen order. */
function armedIdentityGroups<T extends { identityArmed: boolean; identityText: string }>(
writes: T[],
): { text: string; count: number }[] {
const groups = new Map<string, number>();
for (const step of writes) {
if (!step.identityArmed) continue;
groups.set(step.identityText, (groups.get(step.identityText) ?? 0) + 1);
}
return [...groups].map(([text, count]) => ({ text, count }));
}

export function CheckIt({
workflowId,
onExactChecks,
Expand Down Expand Up @@ -914,7 +940,16 @@ export function CheckIt({
it asks how to recognize the right record.
</p>
)}
{plan.writes.map((step) => (
{/* Saving steps that check the record the same way say it once. */}
{armedIdentityGroups(plan.writes).map((group) => (
<div className="check-detail" key={`armed-${group.text}`}>
<p>{group.text}</p>
{group.count > 1 && (
<p className="check-detail-count">For each of its {group.count} saving steps.</p>
)}
</div>
))}
{plan.writes.filter((step) => !step.identityArmed).map((step) => (
<div className="check-detail" key={step.id}>
<p>{step.identityText}</p>
{!step.identityArmed && step.canArm && plan.hasProject && (
Expand Down Expand Up @@ -1116,7 +1151,7 @@ export function CheckIt({

<CheckSection
index={6}
title="Approve for use"
title="Approve this workflow"
doneWord="Approved"
state={approved ? "done" : "todo"}
>
Expand All @@ -1127,7 +1162,8 @@ export function CheckIt({
</Status>
<p>
This version is checked, approved, and encrypted on this
computer. It's ready to run on real items.
computer. It&rsquo;s ready to run on real items, and it stops
to ask you only when something doesn&rsquo;t match.
</p>
<Button variant="primary" onClick={() => onRun(approvedId ?? workflowId)}>
Run it
Expand All @@ -1136,9 +1172,11 @@ export function CheckIt({
) : (
<>
<p>
Approving locks this version so it can't change without being
checked again. OpenAdapt approves it only when every check
above has passed.
You approve a workflow once. After that it runs on its own and
stops to ask a person only when something doesn&rsquo;t match.
Approving locks this version, so any change is checked again.
OpenAdapt lets you approve only when every check above has
passed.
</p>
{plan.blockers.length > 0 && (
<TechnicalDetails>
Expand All @@ -1160,7 +1198,7 @@ export function CheckIt({
}
onClick={() => void approve()}
>
{busy === "approve" ? "Approving…" : "Approve for use"}
{busy === "approve" ? "Approving…" : "Approve this workflow"}
</Button>
</div>
</>
Expand Down
8 changes: 4 additions & 4 deletions packages/desktop/src/screens/Qualification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ function signalNeedsExtraction(source: QualificationIdentitySourceKind): boolean
}

function identityLabel(node: QualificationNode): string {
if (!node.identity?.applicable) return "not applicable";
if (node.identity.armed) return "armed";
return node.identity.reason || "not armed";
if (!node.identity?.applicable) return "not needed";
if (node.identity.armed) return "checked";
return node.identity.reason || "not checked";
}

function actionTitle(node: QualificationNode): string {
Expand Down Expand Up @@ -1259,7 +1259,7 @@ export function Qualification({
}}
>
<Pill tone={selectedControls.identity.armed ? "ok" : "warn"}>
{selectedControls.identity.armed ? "armed" : "not armed"}
{selectedControls.identity.armed ? "checked" : "not checked"}
</Pill>
{selectedControls.identity.policy && (
<Pill
Expand Down
6 changes: 4 additions & 2 deletions packages/desktop/src/screens/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function Results({ onNeedsYou }: { onNeedsYou: () => void }) {
<div className="content simple wide">
<PageHead
title="Results"
sub="What OpenAdapt finished, what needed a person, and what it prevented. OpenAdapt doesn't send these results anywhere. If you turn on the hosted runner, it reports only a PHI-free outcome for each Cloud job it runs."
sub="What OpenAdapt finished, what needed a person, and what it prevented."
/>

<div className="stats">
Expand Down Expand Up @@ -309,7 +309,9 @@ export function Results({ onNeedsYou }: { onNeedsYou: () => void }) {
Only items that OpenAdapt saved and then read back from the record
count as done and checked. Items a person finished or confirmed count
under Needed a person. “No person” marks an item saved with every
check passed while “{SWITCH_LABEL}” was on.
check passed while “{SWITCH_LABEL}” was on. OpenAdapt doesn't send
these results anywhere. If you turn on the hosted runner, it reports
only a PHI-free outcome for each Cloud job it runs.
</p>
</section>
</div>
Expand Down
10 changes: 5 additions & 5 deletions packages/desktop/src/screens/WatchRun.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,19 @@ it("clears prior terminal evidence while a new run is in flight", async () => {
onTeach={() => {}}
/>,
);
await screen.findByText("Outcome evidence");
await screen.findByText("What was checked");

fireEvent.click(screen.getByRole("button", { name: "Run with safety gates" }));
fireEvent.click(screen.getByRole("button", { name: "Run it" }));

await waitFor(() =>
expect(screen.queryByText("Outcome evidence")).toBeNull(),
expect(screen.queryByText("What was checked")).toBeNull(),
);

await act(async () => {
finishRun(verified);
await pendingRun;
});
expect(await screen.findByText("Outcome evidence")).toBeTruthy();
expect(await screen.findByText("What was checked")).toBeTruthy();
});

it("keeps a report visible while local history is retried", async () => {
Expand Down Expand Up @@ -174,7 +174,7 @@ it("keeps a report visible while local history is retried", async () => {
await waitFor(() =>
expect(screen.queryByText("Local run history needs attention")).toBeNull(),
);
expect(screen.getByText("Outcome evidence")).toBeTruthy();
expect(screen.getByText("What was checked")).toBeTruthy();
});

function firstWorkflowReview(
Expand Down
58 changes: 27 additions & 31 deletions packages/desktop/src/screens/WatchRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ function issueForReport(report: RunReport): RunIssue | null {
}

const CONTRACT_LABELS = {
authorization: "Authorization",
identity: "Identity",
postcondition: "Postcondition",
effect: "Business effect",
authorization: "Allowed to act",
identity: "Right record",
postcondition: "Screen checks",
effect: "Read-back",
} as const;

function reviewRisk(
Expand Down Expand Up @@ -385,9 +385,7 @@ export function WatchRun({
<div className="content">
<div className="page-head">
<div className="titles">
<p className="eyebrow">
{firstWorkflow ? "First supervised run" : "Execute"}
</p>
{firstWorkflow && <p className="eyebrow">First supervised run</p>}
<h1>
{firstWorkflow
? "Review the workflow, then watch it run"
Expand All @@ -397,14 +395,14 @@ export function WatchRun({
{!firstWorkflow && (
<div className="row">
<Button disabled={executionBusy} onClick={() => execute("replay")}>
{executionBusy ? "Running…" : "Replay"}
{executionBusy ? "Running…" : "Replay the recording"}
</Button>
<Button
variant="primary"
disabled={executionBusy}
onClick={() => execute("run")}
>
Run with safety gates
Run it
</Button>
</div>
)}
Expand All @@ -413,8 +411,7 @@ export function WatchRun({
{firstWorkflow && (
<Card>
<CardHead
eyebrow="Compiled review"
title="Check the compiled workflow"
title="Check the steps"
sub="Read the steps and inputs before OpenAdapt touches the app."
/>
{reviewReady && currentReview ? (
Expand Down Expand Up @@ -522,7 +519,6 @@ export function WatchRun({

<Card>
<CardHead
eyebrow="Target"
title={
firstWorkflow
? "Recorded application target"
Expand All @@ -531,7 +527,7 @@ export function WatchRun({
sub={
firstWorkflow
? "The supervised run uses the same application target as the recording."
: "Choose the application surface. OpenAdapt uses the same compiled workflow and fail-closed verification on every target."
: "Pick the app it runs in. The steps and checks are the same everywhere."
}
/>
<ExecutionTargetForm
Expand All @@ -546,7 +542,7 @@ export function WatchRun({
/>

{!firstWorkflow && <details className="advanced-target">
<summary>Advanced deployment details</summary>
<summary>Settings for the person who sets this up</summary>
<div className="advanced-target-body">
<Field
label="Deployment config"
Expand Down Expand Up @@ -695,31 +691,32 @@ export function WatchRun({
{report?.outcome_details && (
<Card>
<CardHead
eyebrow="Execution contract"
title="Outcome evidence"
sub="The runtime reports what the profile required, what passed, and which external capabilities were used."
title="What was checked"
sub="What this run had to prove, what passed, and anything it reached outside this computer."
/>
<div className="row">
<Pill tone={report.outcome === "VERIFIED" ? "ok" : "warn"}>
{report.outcome.replaceAll("_", " ").toLowerCase()}
{resultCopy(plainResultForReport(report)).label}
</Pill>
<Pill tone={report.outcome_details.execution_completed ? "ok" : "warn"}>
{report.outcome_details.execution_completed
? "Ran to the end"
: "Stopped before the end"}
</Pill>
{report.outcome_details.profile && (
<Pill tone="neutral">{report.outcome_details.profile}</Pill>
)}
<Pill
tone={
report.outcome_details.production_eligible ? "ok" : "neutral"
}
>
{report.outcome_details.production_eligible
? "production eligible"
: "not production eligible"}
</Pill>
<Pill tone={report.outcome_details.execution_completed ? "ok" : "warn"}>
{report.outcome_details.execution_completed
? "execution completed"
: "execution stopped"}
? "Ready for real items"
: "Not ready for real items yet"}
</Pill>
{report.outcome_details.profile && (
<span className="technical-text" title="Profile">
{report.outcome_details.profile}
</span>
)}
</div>
<div className="metrics" style={{ marginTop: "var(--space-4)" }}>
{(
Expand Down Expand Up @@ -768,7 +765,7 @@ export function WatchRun({

{report?.metrics && (
<Card>
<CardHead eyebrow="Run report" title="Metrics" />
<CardHead title="Run details" />
<div className="metrics">
<div className="metric">
<span className="label">Steps</span>
Expand Down Expand Up @@ -797,8 +794,7 @@ export function WatchRun({
{firstRunFinished && (
<Card>
<CardHead
eyebrow="Next"
title="Qualify this workflow for repeated use"
title="Next: check it for everyday use"
sub="Your supervised run is saved. Now define which record may change and what result proves success."
/>
<p className="page-sub">
Expand Down
26 changes: 24 additions & 2 deletions packages/desktop/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,7 @@ select.input:focus {
.advanced-target summary {
color: var(--text-muted);
cursor: pointer;
font-family: var(--font-mono);
font-family: var(--font-ui);
font-size: var(--fs-sm);
}
.advanced-target-body {
Expand Down Expand Up @@ -1262,7 +1262,7 @@ select.input:focus {
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: var(--font-mono);
font-family: var(--font-ui);
font-size: var(--fs-sm);
color: var(--text-muted);
margin-bottom: var(--space-3);
Expand Down Expand Up @@ -3111,3 +3111,25 @@ select.input:focus {
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.check-detail-count {
margin: var(--space-1) 0 0;
color: var(--text-muted);
font-size: var(--fs-sm);
}
.check-step-fold > summary {
cursor: pointer;
list-style: none;
}
.check-step-fold > summary::-webkit-details-marker {
display: none;
}
.check-step-fold > summary .check-step-state::after {
content: "Show";
margin-left: var(--space-3);
color: var(--text-faint);
text-decoration: underline;
text-underline-offset: 3px;
}
.check-step-fold[open] > summary .check-step-state::after {
content: "Hide";
}
Loading
Loading