feat: add idle app status (#18415)

"Idle" is more accurate than "complete" since:

1. AgentAPI only knows if the screen is active; it has no way of knowing
    if the task is complete.
2. The LLM might be done with its current prompt, but that does not mean
    the task is complete either (it likely needs refinement).

The "complete" state will be reserved for future definition.

Additionally, in the case where the screen goes idle but the LLM never
reported a status update, we can get an idle icon without a message, and
it looks kinda janky in the UI so if there is no message I display the
state text.

Closes https://github.com/coder/internal/issues/699
This commit is contained in:
Asher
2025-06-20 14:34:31 -08:00
committed by GitHub
parent 0258f1d771
commit 0a483ea2b7
20 changed files with 115 additions and 16 deletions
+6 -1
View File
@@ -3622,11 +3622,16 @@ export interface WorkspaceAppStatus {
}
// From codersdk/workspaceapps.go
export type WorkspaceAppStatusState = "complete" | "failure" | "working";
export type WorkspaceAppStatusState =
| "complete"
| "failure"
| "idle"
| "working";
export const WorkspaceAppStatusStates: WorkspaceAppStatusState[] = [
"complete",
"failure",
"idle",
"working",
];
@@ -5,6 +5,7 @@ import {
CircleAlertIcon,
CircleCheckIcon,
HourglassIcon,
SquareIcon,
TriangleAlertIcon,
} from "lucide-react";
import type { FC } from "react";
@@ -26,6 +27,10 @@ export const AppStatusStateIcon: FC<AppStatusStateIconProps> = ({
const className = cn(["size-4 shrink-0", customClassName]);
switch (state) {
case "idle":
return (
<SquareIcon className={cn(["text-content-secondary", className])} />
);
case "complete":
return (
<CircleCheckIcon className={cn(["text-content-success", className])} />
@@ -39,6 +39,26 @@ export const Working: Story = {
},
};
export const Idle: Story = {
args: {
status: {
...MockWorkspaceAppStatus,
state: "idle",
message: "Done for now",
},
},
};
export const NoMessage: Story = {
args: {
status: {
...MockWorkspaceAppStatus,
state: "idle",
message: "",
},
},
};
export const LongMessage: Story = {
args: {
status: {
@@ -5,6 +5,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import capitalize from "lodash/capitalize";
import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon";
import { cn } from "utils/cn";
@@ -25,6 +26,7 @@ export const WorkspaceAppStatus = ({
);
}
const message = status.message || capitalize(status.state);
return (
<div className="flex flex-col text-content-secondary">
<TooltipProvider>
@@ -40,11 +42,11 @@ export const WorkspaceAppStatus = ({
})}
/>
<span className="whitespace-nowrap max-w-72 overflow-hidden text-ellipsis text-sm text-content-primary font-medium">
{status.message}
{message}
</span>
</div>
</TooltipTrigger>
<TooltipContent>{status.message}</TooltipContent>
<TooltipContent>{message}</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="text-xs first-letter:uppercase block pl-6">
@@ -48,6 +48,40 @@ export const WorkingState: Story = {
},
};
export const IdleState: Story = {
args: {
agent: mockAgent([
{
...MockWorkspaceAppStatus,
id: "status-8",
icon: "",
message: "Done for now",
created_at: createTimestamp(5, 20),
uri: "",
state: "idle" as const,
},
...MockWorkspaceAppStatuses,
]),
},
};
export const NoMessage: Story = {
args: {
agent: mockAgent([
{
...MockWorkspaceAppStatus,
id: "status-8",
icon: "",
message: "",
created_at: createTimestamp(5, 20),
uri: "",
state: "idle" as const,
},
...MockWorkspaceAppStatuses,
]),
},
};
export const LongStatusText: Story = {
args: {
agent: mockAgent([
+3 -2
View File
@@ -12,6 +12,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import capitalize from "lodash/capitalize";
import { timeFrom } from "utils/time";
import {
@@ -77,7 +78,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
<div className="text-sm font-medium text-content-primary flex items-center gap-2 ">
<AppStatusStateIcon state={latestStatus.state} latest />
<span className="block flex-1 whitespace-nowrap overflow-hidden text-ellipsis">
{latestStatus.message}
{latestStatus.message || capitalize(latestStatus.state)}
</span>
</div>
<span className="text-xs text-content-secondary first-letter:uppercase block pl-[26px]">
@@ -160,7 +161,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
latest={false}
className="size-icon-xs w-[18px]"
/>
{status.message}
{status.message || capitalize(status.state)}
</span>
<span className="text-2xs text-content-secondary first-letter:uppercase block pl-[26px]">
{formattedTimestamp}