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
2 changes: 1 addition & 1 deletion apps/web/src/pages/agents-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const AGENT_ROSTER_STATUS_LABEL: Record<AgentRosterStatus, string> = {
// the same liveness marker that vocabulary already carries); idle is the
// green "healthy, nothing wrong" tone (`pill-ok` in the spec); blocked and
// archived were already right.
const AGENT_ROSTER_STATUS_TONE: Record<AgentRosterStatus, BadgeTone> = {
export const AGENT_ROSTER_STATUS_TONE: Record<AgentRosterStatus, BadgeTone> = {
running: "info",
idle: "success",
blocked: "danger",
Expand Down
44 changes: 20 additions & 24 deletions apps/web/src/pages/insights-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
TimeSeriesChart,
TokenMosaic,
TraceWaterfall,
type BadgeTone,
type RunStatus,
type TraceSpan,
} from "@corbits/react-ui";
import { ChartBar } from "@corbits/icons";
Expand All @@ -50,6 +52,7 @@ import {
type OverallUsage,
} from "@corbits/insights/client";

import type { WorkflowRunStatus } from "@intx/types";
import { SignedOutNotice, type APIQuery } from "@corbits/api-query";
import {
workbenchesQueryKey,
Expand Down Expand Up @@ -128,30 +131,23 @@ export function formatWhen(iso: string): string {
});
}

export function statusTone(
status: string,
): "success" | "warning" | "danger" | "neutral" | "info" {
switch (status) {
case "completed":
case "succeeded":
case "ok":
case "deployed":
return "success";
case "running":
case "pending":
case "awaiting":
case "updating":
return "info";
case "failed":
case "errored":
case "error":
return "danger";
case "cancelled":
case "stopped":
return "warning";
default:
return "neutral";
}
/** A platform workflow run's status (`WorkflowRunStatus`) doesn't spell
* react-ui's `RunStatus` vocabulary the same way — normalize onto it here
* so the badge tone always comes from `RUN_STATUS_TONE`, the one source
* every run-status tone reads from, rather than a second opinion invented
* on this page. */
const WORKFLOW_RUN_STATUS_ALIAS: Readonly<
Record<WorkflowRunStatus, RunStatus>
> = {
deployed: "completed",
running: "running",
updating: "running",
error: "failed",
stopped: "stopped",
};

export function statusTone(status: WorkflowRunStatus): BadgeTone {
return RUN_STATUS_TONE[WORKFLOW_RUN_STATUS_ALIAS[status]];
}

function tileValue(value: string | number | null, loading: boolean): string {
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/pages/mission-control-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Button,
PageShell,
RichEmptyState,
RUN_STATUS_TONE,
Skeleton,
StatGrid,
StatGridItem,
Expand Down Expand Up @@ -73,7 +74,14 @@ function taskInFlightRow(task: WorkingTask): InFlightRow {
context: `${task.agentName} · task`,
createdAt: task.createdAt,
statusLabel,
statusTone: task.status === "needs-you" ? "accent" : "info",
// needs-you is react-ui's `awaiting` (the one status a person can act
// on); every other in-flight task reads `running` — both read their
// tone from react-ui's own `RUN_STATUS_TONE` rather than a hand-picked
// one.
statusTone:
task.status === "needs-you"
? RUN_STATUS_TONE.awaiting
: RUN_STATUS_TONE.running,
// stepCount is the task's planned total; runIds is how many legs have
// actually dispatched so far — a real ratio, not an invented total.
steps: `${task.runIds.length}/${task.stepCount}`,
Expand All @@ -87,7 +95,7 @@ function routineInFlightRow(routine: RoutineActivityItem): InFlightRow {
context: "routine",
createdAt: routine.startedAt,
statusLabel: "Running",
statusTone: "info",
statusTone: RUN_STATUS_TONE.running,
// The routine feed carries no step count — an honest dash, not a guess.
steps: "—",
};
Expand Down
30 changes: 19 additions & 11 deletions apps/web/src/pages/routines-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
EmptyState,
formatRelativeTime,
RichEmptyState,
RUN_STATUS_TONE,
RunNowButton,
Switch,
Table,
Expand All @@ -31,7 +32,7 @@ import {
TableHeader,
TableRow,
} from "@corbits/react-ui";
import type { BadgeTone } from "@corbits/react-ui";
import type { BadgeTone, RunStatus } from "@corbits/react-ui";
import { Clock, PlayCircle, Plus } from "@corbits/icons";
import type { KeyboardEvent } from "react";
import {
Expand All @@ -58,13 +59,24 @@ import type { RoutineRun } from "../routines-api";

export type { GlobalRoutineRow } from "../global-routines";

const RUN_STATUS_TONE: Record<string, BadgeTone> = {
running: "info",
completed: "success",
failed: "danger",
cancelled: "neutral",
/** Platform run status strings that don't spell their canonical
* `RunStatus` name the same way — everything else already matches
* react-ui's `RunStatus` vocabulary and passes through unchanged. */
const RUN_STATUS_ALIAS: Readonly<Record<string, RunStatus>> = {
cancelled: "stopped",
queued: "provisioning",
pending: "provisioning",
};

/** Tone for a platform run status, normalized onto `RunStatus` first so
* this reads the same map every other surface does
* (`RUN_STATUS_TONE` in react-ui's `workflow-run.ts`) instead of a second,
* drifting opinion. */
export function runStatusTone(status: string): BadgeTone {
const canonicalStatus = RUN_STATUS_ALIAS[status] ?? (status as RunStatus);
return RUN_STATUS_TONE[canonicalStatus] ?? "neutral";
}

/**
* Recent-run rows deep-link to the workbench the routine delivers to — a
* routine has one `deliveryWorkbenchId`, not a per-run one, so every row
Expand Down Expand Up @@ -171,11 +183,7 @@ export function RunStatusCell({ run }: { readonly run: RoutineRun }) {
if (typeof status !== "string") {
return <span className="text-[var(--ui-fg-muted)]">—</span>;
}
return (
<Badge tone={RUN_STATUS_TONE[status] ?? "neutral"}>
{runStatusLabel(status)}
</Badge>
);
return <Badge tone={runStatusTone(status)}>{runStatusLabel(status)}</Badge>;
}

/** A routine's health, from the telemetry the scheduler already records —
Expand Down
90 changes: 90 additions & 0 deletions apps/web/src/pages/run-status-tone-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Guards against the exact bug this file was added for (CL-6499 design
// review): a page grows its own run-status → tone map/function instead of
// reading react-ui's `RUN_STATUS_TONE` (`workflow-run.ts`), and it quietly
// disagrees — a cancelled run reading neutral grey on Routines and amber
// warning on Insights, the same status meaning two different things in one
// product. `RUN_STATUS_TONE` is the one place a run-status tone is allowed
// to be decided; every surface below must normalize its own status
// vocabulary onto react-ui's `RunStatus` and read the tone from there.
//
// A static scan can't verify this: the divergence this catches was a
// `case "stopped": return "warning"` inside a switch, not an object
// literal, and re-deriving "is this tone value equal to canonical" from
// source text would mean parsing arbitrary JS — the general-purpose lint
// framework this ticket explicitly says not to build. Calling the real
// exported code and comparing its output to `RUN_STATUS_TONE` catches both
// shapes (switch or map) with no parser.
import { describe, expect, test } from "bun:test";

import { RUN_STATUS_TONE } from "@corbits/react-ui";

import { statusTone } from "./insights-page";
import { runStatusTone } from "./routines-page";
import { computeInFlightRows } from "./mission-control-page";
import { AGENT_ROSTER_STATUS_TONE } from "./agents-page";
import type { WorkingTask } from "@corbits/tasks-ui";
import type { RoutineActivityItem } from "../shell/routine-activity";

function workingTask(overrides: Partial<WorkingTask>): WorkingTask {
return {
id: "tsk_1",
definitionId: "def_1",
workbenchId: null,
agentName: "Myra",
prompt: "Do the thing",
modelPreference: null,
status: "running",
runId: "run_1",
runIds: ["run_1"],
stepCount: 1,
resultMailId: null,
createdAt: new Date().toISOString(),
completedAt: null,
...overrides,
};
}

describe("run-status tone parity with react-ui's RUN_STATUS_TONE", () => {
test("Insights' statusTone agrees with canonical for every shared status", () => {
// WorkflowRunStatus ("running"/"stopped") spells these two the same way
// RunStatus does — the exact pair the reviewer caught disagreeing.
expect(statusTone("running")).toBe(RUN_STATUS_TONE.running);
expect(statusTone("stopped")).toBe(RUN_STATUS_TONE.stopped);
});

test("Routines' runStatusTone agrees with canonical for every shared status", () => {
expect(runStatusTone("running")).toBe(RUN_STATUS_TONE.running);
expect(runStatusTone("completed")).toBe(RUN_STATUS_TONE.completed);
expect(runStatusTone("failed")).toBe(RUN_STATUS_TONE.failed);
});

test("Mission Control's in-flight rows agree with canonical for every shared status", () => {
const [needsYouRow] = computeInFlightRows(
[workingTask({ id: "tsk_needs_you", status: "needs-you" })],
[],
);
expect(needsYouRow?.statusTone).toBe(RUN_STATUS_TONE.awaiting);

const [runningRow] = computeInFlightRows(
[workingTask({ id: "tsk_running", status: "running" })],
[],
);
expect(runningRow?.statusTone).toBe(RUN_STATUS_TONE.running);

const routine: RoutineActivityItem = {
id: "rtn_1",
name: "Daily brief",
status: "running",
startedAt: new Date().toISOString(),
};
const [routineRow] = computeInFlightRows([], [routine]);
expect(routineRow?.statusTone).toBe(RUN_STATUS_TONE.running);
});

test("Agents' roster status tone (a genuinely different vocabulary) still agrees where it overlaps", () => {
// AgentRosterStatus is its own enum, not RunStatus — this is the one
// local map the ticket calls defensible. It only shares one name
// ("running") with RunStatus, and it must keep agreeing on that one.
expect(AGENT_ROSTER_STATUS_TONE.running).toBe(RUN_STATUS_TONE.running);
});
});
Loading