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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts";

const clientSettings: ClientSettings = {
appearanceContrast: 100,
dotMatrixMotion: "smooth",
browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" },
browserDefaultZoomFactor: 1.25,
browserDefaultAppearance: "dark",
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/orchestration/ThreadBackgroundLiveness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ describe("ThreadBackgroundLiveness", () => {
expect(liveness.getThreadBackgroundLiveness("thread")).toBeNull();
});

it("does not present a waiting task as active work", () => {
const liveness = ThreadBackgroundLiveness.make();
const input = {
threadId: "thread",
taskId: "task",
taskType: undefined,
} as const;
liveness.recordTaskLiveness({ ...input, status: "running", kind: "started" });
expect(liveness.getThreadBackgroundLiveness("thread")).toBe("working");

liveness.recordTaskLiveness({ ...input, status: "waiting", kind: "updated" });
expect(liveness.getThreadBackgroundLiveness("thread")).toBeNull();

// A description-only tick cannot falsely restart it.
liveness.recordTaskLiveness({ ...input, status: undefined, kind: "progress" });
expect(liveness.getThreadBackgroundLiveness("thread")).toBeNull();

liveness.recordTaskLiveness({ ...input, status: "running", kind: "progress" });
expect(liveness.getThreadBackgroundLiveness("thread")).toBe("working");
});

it("agents present as working; monitors as monitoring; agents win", () => {
const liveness = ThreadBackgroundLiveness.make();
const threadId = "t-live-1";
Expand Down
10 changes: 6 additions & 4 deletions apps/server/src/orchestration/ThreadBackgroundLiveness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@ export function make(): ThreadBackgroundLivenessService["Service"] {
return;
}

// Idle counts as not-live: a resting (resumable) Codex child isn't
// doing anything, and an all-idle fleet must not pin Working.
const terminal =
// Idle and waiting count as not-live: neither state is actively doing
// work, so an all-resting fleet must not pin Working. A later explicit
// running transition adds the task back.
const inactive =
input.kind === "completed" ||
input.status === "idle" ||
input.status === "waiting" ||
(input.status !== undefined && TERMINAL_STATUSES.has(input.status));
if (terminal) {
if (inactive) {
drop(input.threadId, input.taskId);
return;
}
Expand Down
12 changes: 12 additions & 0 deletions apps/web/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@ Copyright (c) 2016 Roberto Huertas

Licensed under the MIT License. The full license text is available in the
upstream repository: <https://github.com/vscode-icons/vscode-icons/blob/master/LICENSE>.

## assistant-ui Dot Matrix

The Dot Matrix state patterns in `src/components/ui/dot-matrix.tsx` are adapted
from assistant-ui's [standalone Dot Matrix](https://www.assistant-ui.com/standalone/dot-matrix)
component. Pylon adds queued, terminal, and orchestration states and offers
smooth or stepped animation timing for status surfaces.

Copyright (c) AgentbaseAI Inc.

Licensed under the MIT License. The full license text is available in the
upstream repository: <https://github.com/assistant-ui/assistant-ui/blob/main/LICENSE>.
27 changes: 27 additions & 0 deletions apps/web/src/components/AgentsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ describe("AgentsPanel agent cancellation", () => {
expect(markup).toContain("Working");
});

it("keeps queued, running, waiting, and settled agent states distinct", () => {
const statuses: RuntimeSubagent["status"][] = [
"pending",
"running",
"waiting",
"idle",
"completed",
"failed",
"cancelled",
];
const statusModel = {
...model,
directAgents: statuses.map((status) => agent(`agent-${status}`, status, status)),
};
const markup = renderToStaticMarkup(<AgentsPanel model={statusModel} />);

expect(markup).toContain('data-state="orchestrating"');
expect(markup).toContain('data-state="queued"');
expect(markup).toContain('data-state="waiting"');
expect(markup).toContain('data-state="paused"');
expect(markup).toContain('data-state="success"');
expect(markup).toContain('data-state="error"');
expect(markup).toContain('data-state="stopped"');
expect(markup).toContain("Queued");
expect(markup).toContain("Waiting");
});

it("keeps every agent row read-only without an advertised capability", () => {
const markup = renderToStaticMarkup(<AgentsPanel model={model} />);
expect(markup).not.toContain('aria-label="Stop ');
Expand Down
24 changes: 10 additions & 14 deletions apps/web/src/components/AgentsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,20 @@ import { DotMatrix, type DotMatrixState } from "./ui/dot-matrix";
import { AgentLiveActivity } from "./AgentLiveActivity";

/**
* In-flight states all present as Working (one steady state, per the
* monitoring-pill design: detail belongs in the activity sub-line, and a
* stalled/waiting/queued subagent is still the fleet doing its job, not a
* user problem). Only settled states differentiate.
* Agent status is a truth-bearing visual contract: the orbit is reserved for
* running subagents, while queued and waiting agents use their own patterns.
* Settled outcomes stay static except for attention states such as failure.
*/
const STATUS_VISUALS: Record<RuntimeSubagent["status"], { matrix: DotMatrixState; label: string }> =
{
pending: { matrix: "spinner", label: "Working" },
running: { matrix: "spinner", label: "Working" },
waiting: { matrix: "spinner", label: "Working" },
// Idle reads as settled (muted, not primary): a resting Codex child looks
// done unless resumed — live-test: sky idle dots read as stuck in-progress.
idle: { matrix: "idle", label: "Idle · resumable" },
completed: { matrix: "done", label: "Completed" },
pending: { matrix: "queued", label: "Queued" },
running: { matrix: "orchestrating", label: "Working" },
waiting: { matrix: "waiting", label: "Waiting" },
idle: { matrix: "paused", label: "Idle · resumable" },
completed: { matrix: "success", label: "Completed" },
failed: { matrix: "error", label: "Failed" },
// Stopped is settled-but-not-finished: inert dots, no success or error hue.
cancelled: { matrix: "idle", label: "Stopped" },
interrupted: { matrix: "idle", label: "Stopped" },
cancelled: { matrix: "stopped", label: "Stopped" },
interrupted: { matrix: "stopped", label: "Stopped" },
};

function StatusDot({ status }: { status: RuntimeSubagent["status"] }) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5085,7 +5085,7 @@ function ChatViewContent(props: ChatViewProps) {
icon: (
<DotMatrix
aria-hidden
state={working ? "spinner" : "live"}
state={working ? "orchestrating" : "listening"}
className="size-3.5 text-foreground"
/>
),
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/components/ConnectionStatusDot.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import { ConnectionStatusDot, connectionPhaseDotMatrixState } from "./ConnectionStatusDot";

describe("ConnectionStatusDot", () => {
it("maps connection lifecycle facts to semantic states", () => {
expect(connectionPhaseDotMatrixState("connected")).toBe("success");
expect(connectionPhaseDotMatrixState("connecting")).toBe("connecting");
expect(connectionPhaseDotMatrixState("reconnecting")).toBe("connecting");
expect(connectionPhaseDotMatrixState("error")).toBe("error");
expect(connectionPhaseDotMatrixState("offline")).toBe("offline");
expect(connectionPhaseDotMatrixState("available")).toBe("offline");
});

it("renders the connected outcome as a static success glyph", () => {
const markup = renderToStaticMarkup(
<ConnectionStatusDot state="success" tooltipText="Connected" />,
);
expect(markup).toContain('data-state="success"');
expect(markup).toContain("text-success");
expect(markup).not.toContain("data-animated");
});

it("keeps a persisted pairing link static while it waits to be used", () => {
const markup = renderToStaticMarkup(
<ConnectionStatusDot state="queued" colorClassName="text-warning" />,
);
expect(markup).toContain('data-state="queued"');
expect(markup).toContain("text-warning");
expect(markup).not.toContain("data-animated");
});
});
29 changes: 6 additions & 23 deletions apps/web/src/components/ConnectionStatusDot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,6 @@ import { cn } from "~/lib/utils";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { DotMatrix, type DotMatrixState } from "~/components/ui/dot-matrix";

/** Canonical connection-phase → dot color mapping shared by every status dot. */
export function connectionPhaseDotClassName(phase: EnvironmentConnectionPhase): string {
switch (phase) {
case "connected":
return "bg-success";
case "connecting":
case "reconnecting":
return "bg-warning";
case "error":
return "bg-destructive";
default:
return "bg-muted-foreground/40";
}
}

/** Ping halo for transitional phases; null renders no ping. */
export function connectionPhasePingClassName(phase: EnvironmentConnectionPhase): string | null {
return phase === "connecting" || phase === "reconnecting" ? "bg-warning/60 duration-2000" : null;
}

/**
* Connection phase as a DotMatrix state. The dot carries hue and motion
* together, so callers pass a phase rather than assembling colors themselves.
Expand All @@ -33,20 +13,23 @@ export function connectionPhaseDotMatrixState(
): ConnectionStatusDotProps["state"] {
switch (phase) {
case "connected":
return "live";
return "success";
case "connecting":
case "reconnecting":
return "connecting";
case "error":
return "error";
default:
return "idle";
return "offline";
}
}

type ConnectionStatusDotProps = {
tooltipText?: string | null;
state: Extract<DotMatrixState, "live" | "connecting" | "error" | "idle">;
state: Extract<
DotMatrixState,
"success" | "connecting" | "waiting" | "queued" | "error" | "offline"
>;
/** Only needed when a caller wants a hue other than the state's canonical
* tone (see dot-matrix.tsx's TONE map). */
colorClassName?: string | undefined;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/LegacySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
/>
}
>
<DotMatrix aria-hidden state="terminal" className="size-3" />
<DotMatrix aria-hidden state="terminal-active" className="size-3" />
</TooltipTrigger>
<TooltipPopup side="top">{terminalStatus.label}</TooltipPopup>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ProviderUpdateEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function EnvironmentUpdateRow({
trailing = <Spinner className="size-4 text-muted-foreground" />;
break;
case "success":
trailing = <DotMatrix aria-hidden state="done" className="size-3.5" />;
trailing = <DotMatrix aria-hidden state="success" className="size-3.5" />;
break;
case "failed":
case "unchanged":
Expand Down
16 changes: 8 additions & 8 deletions apps/web/src/components/ServerUpdateAction.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,14 @@ describe("ServerUpdateProgress", () => {
);

expect(markup).toContain("Restarting…");
// One row, no versions and no step rail. The wait carries the shared
// DotMatrix "spinner" marker (blue, from its canonical tone) rather than
// a bespoke breathing dot, so there is nothing green to read as "done"
// and no second pulse animation to own.
// One row, no versions and no step rail. Restarting uses the shared
// neutral syncing pattern; only a completed outcome may turn green.
expect(markup).not.toContain("0.0.30");
expect(markup).not.toContain("Resum");
expect(markup).not.toContain("text-success");
expect(markup).toContain("text-primary");
expect(markup).toContain('data-state="spinner"');
expect(markup).not.toContain('data-state="done"');
expect(markup).toContain("text-foreground");
expect(markup).toContain('data-state="syncing"');
expect(markup).not.toContain('data-state="success"');
expect(markup).not.toContain("animate-status-pulse");
expect(markup).not.toContain("animate-spin");
});
Expand All @@ -142,6 +140,7 @@ describe("ServerUpdateProgress", () => {

expect(markup).toContain("Downloading…");
expect(markup).not.toContain("Install");
expect(markup).toContain('data-state="downloading"');
});

it("keeps the failure visible with its retryable error", () => {
Expand All @@ -160,6 +159,7 @@ describe("ServerUpdateProgress", () => {
expect(markup).toContain('role="alert"');
expect(markup).toContain("The package could not be verified.");
expect(markup).not.toContain("animate-status-pulse");
expect(markup).not.toContain('data-state="spinner"');
expect(markup).not.toContain('data-state="syncing"');
expect(markup).not.toContain('data-state="downloading"');
});
});
6 changes: 5 additions & 1 deletion apps/web/src/components/ServerUpdateAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export function ServerUpdateProgress({
}
return (
<div className="mt-1 flex items-center gap-2 text-xs font-medium text-foreground">
<DotMatrix aria-hidden state="spinner" className="size-3.5 shrink-0" />
<DotMatrix
aria-hidden
state={state.stage === "resuming" ? "syncing" : "downloading"}
className="size-3.5 shrink-0"
/>
<span>{serverUpdateStageLabel(state.stage)}</span>
</div>
);
Expand Down
Loading
Loading