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
38 changes: 38 additions & 0 deletions apps/web/src/command-center/CommandCenterHome.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
projectBootstrap,
projectEnvironmentProjects,
routeReceiptFromResult,
routeReceiptFromRoute,
routeReceiptFromTimelineEntry,
routeTimelineMessage,
timelineMessages,
waitForRouteReceiptPaint,
Expand Down Expand Up @@ -347,6 +349,37 @@ describe("CommandCenterHome projection", () => {
expect(message).toMatchObject({ linkedRunId: "run-1", linkedThreadId: "thread-1" });
});

it("maps a persisted approval-required route from its authoritative run status", () => {
const projects = projectEnvironmentProjects(PROJECTS, BOOTSTRAP);
const options = buildRouteOptions(BOOTSTRAP, projects, PROVIDERS);
const display = { projects, options };
const route = RESULT.route;

expect(route.approvalRequired).toBe(true);
expect(routeReceiptFromRoute(route, "waiting_approval", BOOTSTRAP, display).status).toBe(
"waiting-approval",
);
expect(routeReceiptFromRoute(route, "running", BOOTSTRAP, display).status).toBe("running");
expect(routeReceiptFromRoute(route, "succeeded", BOOTSTRAP, display).status).toBe("complete");
expect(routeReceiptFromRoute(route, "failed", BOOTSTRAP, display).status).toBe("failed");
expect(routeReceiptFromRoute(route, "canceled", BOOTSTRAP, display).status).toBe("failed");
expect(routeReceiptFromRoute(route, "waiting", BOOTSTRAP, display).status).toBe(
"waiting-approval",
);
expect(routeReceiptFromRoute(route, "queued", BOOTSTRAP, display).status).toBe("running");

const entry = { route } as unknown as CommandCenterTimelineEntry;
expect(
routeReceiptFromTimelineEntry({ ...entry, status: "succeeded" }, BOOTSTRAP, display).status,
).toBe("complete");
expect(
routeReceiptFromTimelineEntry({ ...entry, status: "failed" }, BOOTSTRAP, display).status,
).toBe("failed");
expect(
routeReceiptFromTimelineEntry({ ...entry, status: "running" }, BOOTSTRAP, display).status,
).toBe("running");
});

it("waits through a complete browser paint boundary before starting work", async () => {
const frames: Array<() => void> = [];
const waiting = waitForRouteReceiptPaint((callback) => frames.push(callback));
Expand Down Expand Up @@ -424,6 +457,11 @@ describe("CommandCenterHome projection", () => {
author: "system",
authorLabel: "Route receipt",
linkedThreadId: "thread-durable",
receipt: {
providerName: "provider-1",
modelName: "model-1",
status: "running",
},
});
expect(messages[1]?.body).toContain("Studio App · project-studio");
expect(messages[1]?.body).toContain("provider-1 · model-1");
Expand Down
37 changes: 27 additions & 10 deletions apps/web/src/command-center/CommandCenterHome.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,25 @@ export function waitForRouteReceiptPaint(
});
}

function routeReceiptFromRoute(
function routeStatusFromRunStatus(runStatus: RunStatus): CommandCenterRouteReceipt["status"] {
switch (runStatus) {
case "succeeded":
return "complete";
case "failed":
case "canceled":
return "failed";
case "waiting":
case "waiting_approval":
return "waiting-approval";
case "queued":
case "running":
return "running";
}
runStatus satisfies never;
throw new Error("Unsupported run status");
}

export function routeReceiptFromRoute(
route: RouteDecision,
runStatus: RunStatus,
bootstrap: CommandCenterBootstrap | null,
Expand All @@ -525,14 +543,8 @@ function routeReceiptFromRoute(
(provider) => provider.id === route.providerId,
);
const routedModel = display.options.models.find((model) => model.id === route.modelId);
const status =
route.status === "blocked"
? ("blocked" as const)
: route.approvalRequired || runStatus === "waiting_approval"
? ("waiting-approval" as const)
: runStatus === "succeeded"
? ("complete" as const)
: ("running" as const);
const status: CommandCenterRouteReceipt["status"] =
route.status === "blocked" ? "blocked" : routeStatusFromRunStatus(runStatus);
const reasonSummary =
route.reasons.length > 0
? route.reasons.join(" ")
Expand Down Expand Up @@ -642,7 +654,11 @@ export function routeTimelineMessage(
? "The route was blocked."
: receipt.status === "waiting-approval"
? "The route is waiting for approval."
: "The route is active.";
: receipt.status === "complete"
? "The run completed."
: receipt.status === "failed"
? "The run failed."
: "The route is active.";
const linkedContext = [receipt.repositoryName, receipt.projectName]
.filter((value): value is string => value !== undefined)
.join(" · ");
Expand All @@ -657,6 +673,7 @@ export function routeTimelineMessage(
body: `${routeState} ${receipt.spaceName}${linkedContext ? ` · ${linkedContext}` : ""} · ${receipt.providerName} · ${receipt.modelName}. ${receipt.summary} ${capabilitySummary}`,
createdAtLabel,
linkedRunId: result.run.id,
receipt,
...(result.run.threadId === undefined ? {} : { linkedThreadId: result.run.threadId }),
};
}
23 changes: 19 additions & 4 deletions apps/web/src/command-center/CommandCenterHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
projectBootstrap,
projectEnvironmentProjects,
routeReceiptFromResult,
routeReceiptFromTimelineEntry,
routeTimelineMessage,
timelineMessages,
waitForRouteReceiptPaint,
Expand Down Expand Up @@ -82,6 +83,11 @@ function failureMessage(failure: unknown): string {
: "Command Center could not submit the command.";
}

interface LastRouteReceipt {
readonly receipt: CommandCenterRouteReceipt;
readonly runId?: string | undefined;
}

export function CommandCenterHome() {
const navigate = useNavigate();
const environmentId = usePrimaryEnvironmentId();
Expand All @@ -93,7 +99,7 @@ export function CommandCenterHome() {
const [draft, setDraft] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [resolvingNeedsYouId, setResolvingNeedsYouId] = useState<string>();
const [lastReceipt, setLastReceipt] = useState<CommandCenterRouteReceipt | null>(null);
const [lastReceipt, setLastReceipt] = useState<LastRouteReceipt | null>(null);
const selectedSpaceId =
routeSelection.spaceId === undefined ? undefined : SpaceId.make(routeSelection.spaceId);
const bootstrapQuery = useEnvironmentQuery(
Expand Down Expand Up @@ -181,7 +187,15 @@ export function CommandCenterHome() {
[authoritativeMessages, optimisticMessages],
);
const routeReceipt = useMemo(() => {
if (lastReceipt !== null) return lastReceipt;
if (lastReceipt !== null) {
const timelineEntry =
lastReceipt.runId === undefined
? undefined
: timelineQuery.data?.entries.find((entry) => entry.runId === lastReceipt.runId);
return timelineEntry === undefined
? lastReceipt.receipt
: routeReceiptFromTimelineEntry(timelineEntry, bootstrap, routeDisplay);
}
if (timelineQuery.error !== null || eventQuery.error !== null) {
return blockedReceipt("The durable Command timeline is not available right now.");
}
Expand All @@ -200,6 +214,7 @@ export function CommandCenterHome() {
lastReceipt,
routeDisplay,
routeSelection,
timelineQuery.data?.entries,
timelineQuery.error,
]);

Expand Down Expand Up @@ -421,7 +436,7 @@ export function CommandCenterHome() {

if (result._tag === "Success") {
const receipt = routeReceiptFromResult(result.value, bootstrap, routeDisplay);
setLastReceipt(receipt);
setLastReceipt({ receipt, runId: result.value.run.id });
setActiveConversationId(result.value.run.id);
setOptimisticMessages((current) => [
...current,
Expand Down Expand Up @@ -454,7 +469,7 @@ export function CommandCenterHome() {
}
} else {
const message = failureMessage(squashAtomCommandFailure(result));
setLastReceipt(blockedReceipt(message));
setLastReceipt({ receipt: blockedReceipt(message) });
setOptimisticMessages((current) => [
...current,
{
Expand Down
88 changes: 86 additions & 2 deletions apps/web/src/command-center/CommandCenterShell.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vite-plus/test";

import { CommandCenterShell, shouldSubmitCommandComposerOnKeyDown } from "./CommandCenterShell";
import type { CommandCenterShellProps } from "./types";
import {
CommandCenterShell,
Messages,
shouldSubmitCommandComposerOnKeyDown,
} from "./CommandCenterShell";
import type {
CommandCenterMessage,
CommandCenterRouteReceipt,
CommandCenterShellProps,
} from "./types";

const FIXTURE: CommandCenterShellProps = {
activeConversationId: "conversation-1",
Expand Down Expand Up @@ -177,6 +185,82 @@ describe("CommandCenterShell", () => {
).toBe(false);
});

it("renders each historical route receipt from its own message state", () => {
const completeReceipt: CommandCenterRouteReceipt = {
...FIXTURE.routeReceipt,
spaceName: "Completed Space",
status: "complete",
summary: "The completed run settled successfully.",
};
const runningReceipt: CommandCenterRouteReceipt = {
...FIXTURE.routeReceipt,
spaceName: "Running Space",
status: "running",
summary: "The current run is still active.",
};
const messages: readonly CommandCenterMessage[] = [
{
id: "completed-route",
author: "system",
authorLabel: "Route receipt",
body: "The run completed.",
createdAtLabel: "10:30 AM",
receipt: completeReceipt,
},
{
id: "running-route",
author: "system",
authorLabel: "Route receipt",
body: "The route is active.",
createdAtLabel: "10:31 AM",
receipt: runningReceipt,
},
];

const html = renderToStaticMarkup(<Messages messages={messages} receipt={runningReceipt} />);
const runningRowStart = html.indexOf(">Working<");
const completedRow = html.slice(0, runningRowStart);
const runningRow = html.slice(runningRowStart);

expect(runningRowStart).toBeGreaterThan(-1);
expect(completedRow).toContain(">Work complete<");
expect(completedRow).toContain(">Complete<");
expect(completedRow).toContain("Completed Space");
expect(completedRow).not.toContain(">Working<");
expect(completedRow).not.toContain(">Running<");
expect(runningRow).toContain(">Working<");
expect(runningRow).toContain(">Running<");
expect(runningRow).toContain("Running Space");
});

it("renders failed route receipts as destructive and inactive", () => {
const failedReceipt: CommandCenterRouteReceipt = {
...FIXTURE.routeReceipt,
status: "failed",
summary: "The run stopped with an error.",
};
const html = renderToStaticMarkup(
<Messages
messages={[
{
id: "failed-route",
author: "system",
authorLabel: "Route receipt",
body: "The run failed.",
createdAtLabel: "10:30 AM",
receipt: failedReceipt,
},
]}
receipt={FIXTURE.routeReceipt}
/>,
);

expect(html).toContain(">Run failed<");
expect(html).toContain(">Failed<");
expect(html).toContain("bg-destructive");
expect(html).not.toContain("animate-pulse");
});

it("renders the T3 command surface and visible route receipt", () => {
const html = renderToStaticMarkup(<CommandCenterShell {...FIXTURE} />);

Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/command-center/CommandCenterShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ function RouteReceipt({ receipt }: { readonly receipt: CommandCenterRouteReceipt
? "Waiting for approval"
: receipt.status === "complete"
? "Work complete"
: receipt.status === "running"
? "Working"
: "Route ready"}
: receipt.status === "failed"
? "Run failed"
: receipt.status === "running"
? "Working"
: "Route ready"}
</span>
<span className="min-w-0 flex-1 truncate text-muted-foreground">
{receipt.spaceName}
Expand All @@ -143,7 +145,7 @@ function RouteReceipt({ receipt }: { readonly receipt: CommandCenterRouteReceipt
"size-1.5 rounded-full",
isActive ? "animate-pulse bg-info" : "bg-success",
receipt.status === "waiting-approval" && "bg-warning",
receipt.status === "blocked" && "bg-destructive",
(receipt.status === "blocked" || receipt.status === "failed") && "bg-destructive",
)}
/>
{receipt.status === "blocked"
Expand All @@ -152,9 +154,11 @@ function RouteReceipt({ receipt }: { readonly receipt: CommandCenterRouteReceipt
? "Approval"
: receipt.status === "complete"
? "Complete"
: receipt.status === "running"
? "Running"
: "Ready"}
: receipt.status === "failed"
? "Failed"
: receipt.status === "running"
? "Running"
: "Ready"}
</span>
<ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground transition-transform group-open:rotate-180" />
</summary>
Expand Down Expand Up @@ -207,7 +211,7 @@ function RouteReceipt({ receipt }: { readonly receipt: CommandCenterRouteReceipt
);
}

function Messages({
export function Messages({
messages,
receipt,
onOpenLinkedThread,
Expand Down Expand Up @@ -250,7 +254,7 @@ function Messages({
if (message.authorLabel === "Route receipt") {
return (
<div className="mb-8" key={message.id}>
<RouteReceipt receipt={receipt} />
<RouteReceipt receipt={message.receipt ?? receipt} />
</div>
);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/command-center/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface CommandCenterMessage {
readonly authorLabel?: string | undefined;
readonly linkedRunId?: string | undefined;
readonly linkedThreadId?: string | undefined;
readonly receipt?: CommandCenterRouteReceipt | undefined;
}

export type CommandCenterRisk = "low" | "reversible" | "approval-required" | "blocked";
Expand All @@ -47,6 +48,7 @@ export type CommandCenterRouteStatus =
| "running"
| "waiting-approval"
| "complete"
| "failed"
| "blocked";

export type CommandCenterRouteSource =
Expand Down
Loading