From 634fe1c32a9a4ff6febf3aead2ddf233f4a857f6 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 17 Jun 2025 11:40:39 -0800 Subject: [PATCH 1/2] feat: add idle AI agent status "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. --- cli/exp_mcp.go | 4 ++-- cli/exp_mcp_test.go | 4 ++-- coderd/apidoc/docs.go | 2 ++ coderd/apidoc/swagger.json | 3 ++- coderd/database/dump.sql | 3 ++- .../000338_workspace_app_status_idle.down.sql | 1 + .../000338_workspace_app_status_idle.up.sql | 1 + coderd/database/models.go | 5 ++++- coderd/workspaceagents.go | 5 ++++- codersdk/toolsdk/toolsdk.go | 6 +++--- codersdk/workspaceapps.go | 1 + docs/reference/api/builds.md | 2 ++ docs/reference/api/schemas.md | 1 + docs/reference/api/templates.md | 2 ++ site/src/api/typesGenerated.ts | 7 ++++++- site/src/modules/apps/AppStatusStateIcon.tsx | 5 +++++ .../WorkspaceAppStatus.stories.tsx | 10 ++++++++++ .../pages/WorkspacePage/AppStatuses.stories.tsx | 16 ++++++++++++++++ 18 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 coderd/database/migrations/000338_workspace_app_status_idle.down.sql create mode 100644 coderd/database/migrations/000338_workspace_app_status_idle.up.sql diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go index d487af5691bca..0a1c9fcbeaf87 100644 --- a/cli/exp_mcp.go +++ b/cli/exp_mcp.go @@ -585,10 +585,10 @@ func (s *mcpServer) startWatcher(ctx context.Context, inv *serpent.Invocation) { case event := <-eventsCh: switch ev := event.(type) { case agentapi.EventStatusChange: - // If the screen is stable, assume complete. + // If the screen is stable, report idle. state := codersdk.WorkspaceAppStatusStateWorking if ev.Status == agentapi.StatusStable { - state = codersdk.WorkspaceAppStatusStateComplete + state = codersdk.WorkspaceAppStatusStateIdle } err := s.queue.Push(taskReport{ state: state, diff --git a/cli/exp_mcp_test.go b/cli/exp_mcp_test.go index 08d6fbc4e2ce6..bcfafb0204874 100644 --- a/cli/exp_mcp_test.go +++ b/cli/exp_mcp_test.go @@ -900,7 +900,7 @@ func TestExpMcpReporter(t *testing.T) { { event: makeStatusEvent(agentapi.StatusStable), expected: &codersdk.WorkspaceAppStatus{ - State: codersdk.WorkspaceAppStatusStateComplete, + State: codersdk.WorkspaceAppStatusStateIdle, Message: "doing work", URI: "https://dev.coder.com", }, @@ -948,7 +948,7 @@ func TestExpMcpReporter(t *testing.T) { { event: makeStatusEvent(agentapi.StatusStable), expected: &codersdk.WorkspaceAppStatus{ - State: codersdk.WorkspaceAppStatusStateComplete, + State: codersdk.WorkspaceAppStatusStateIdle, Message: "oops", URI: "", }, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f2a7dd2dee7a2..ef2b847d43214 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -18076,11 +18076,13 @@ const docTemplate = `{ "type": "string", "enum": [ "working", + "idle", "complete", "failure" ], "x-enum-varnames": [ "WorkspaceAppStatusStateWorking", + "WorkspaceAppStatusStateIdle", "WorkspaceAppStatusStateComplete", "WorkspaceAppStatusStateFailure" ] diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 74b5aad0afed5..29082d80ae3b7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -16517,9 +16517,10 @@ }, "codersdk.WorkspaceAppStatusState": { "type": "string", - "enum": ["working", "complete", "failure"], + "enum": ["working", "idle", "complete", "failure"], "x-enum-varnames": [ "WorkspaceAppStatusStateWorking", + "WorkspaceAppStatusStateIdle", "WorkspaceAppStatusStateComplete", "WorkspaceAppStatusStateFailure" ] diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 457ba8e65ce5a..50825ad3a943d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -324,7 +324,8 @@ CREATE TYPE workspace_app_open_in AS ENUM ( CREATE TYPE workspace_app_status_state AS ENUM ( 'working', 'complete', - 'failure' + 'failure', + 'idle' ); CREATE TYPE workspace_transition AS ENUM ( diff --git a/coderd/database/migrations/000338_workspace_app_status_idle.down.sql b/coderd/database/migrations/000338_workspace_app_status_idle.down.sql new file mode 100644 index 0000000000000..bc5535cf614be --- /dev/null +++ b/coderd/database/migrations/000338_workspace_app_status_idle.down.sql @@ -0,0 +1 @@ +-- It is not possible to delete enum values. diff --git a/coderd/database/migrations/000338_workspace_app_status_idle.up.sql b/coderd/database/migrations/000338_workspace_app_status_idle.up.sql new file mode 100644 index 0000000000000..1630e3580f45c --- /dev/null +++ b/coderd/database/migrations/000338_workspace_app_status_idle.up.sql @@ -0,0 +1 @@ +ALTER TYPE workspace_app_status_state ADD VALUE IF NOT EXISTS 'idle'; diff --git a/coderd/database/models.go b/coderd/database/models.go index c54a218d4b41d..847fb223f5cda 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2628,6 +2628,7 @@ const ( WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working" WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete" WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure" + WorkspaceAppStatusStateIdle WorkspaceAppStatusState = "idle" ) func (e *WorkspaceAppStatusState) Scan(src interface{}) error { @@ -2669,7 +2670,8 @@ func (e WorkspaceAppStatusState) Valid() bool { switch e { case WorkspaceAppStatusStateWorking, WorkspaceAppStatusStateComplete, - WorkspaceAppStatusStateFailure: + WorkspaceAppStatusStateFailure, + WorkspaceAppStatusStateIdle: return true } return false @@ -2680,6 +2682,7 @@ func AllWorkspaceAppStatusStateValues() []WorkspaceAppStatusState { WorkspaceAppStatusStateWorking, WorkspaceAppStatusStateComplete, WorkspaceAppStatusStateFailure, + WorkspaceAppStatusStateIdle, } } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index ed3f554a89b75..8282eb9e7d01f 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -359,7 +359,10 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req } switch req.State { - case codersdk.WorkspaceAppStatusStateComplete, codersdk.WorkspaceAppStatusStateFailure, codersdk.WorkspaceAppStatusStateWorking: // valid states + case codersdk.WorkspaceAppStatusStateComplete, + codersdk.WorkspaceAppStatusStateFailure, + codersdk.WorkspaceAppStatusStateWorking, + codersdk.WorkspaceAppStatusStateIdle: // valid states default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid state provided.", diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index bb1649efa1993..3b992124005ac 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -191,7 +191,7 @@ Bad Tasks Use the "state" field to indicate your progress. Periodically report progress with state "working" to keep the user updated. It is not possible to send too many updates! -ONLY report a "complete" or "failure" state if you have FULLY completed the task. +ONLY report an "idle" or "failure" state if you have FULLY completed the task. `, Schema: aisdk.Schema{ Properties: map[string]any{ @@ -205,10 +205,10 @@ ONLY report a "complete" or "failure" state if you have FULLY completed the task }, "state": map[string]any{ "type": "string", - "description": "The state of your task. This can be one of the following: working, complete, or failure. Select the state that best represents your current progress.", + "description": "The state of your task. This can be one of the following: working, idle, or failure. Select the state that best represents your current progress.", "enum": []string{ string(codersdk.WorkspaceAppStatusStateWorking), - string(codersdk.WorkspaceAppStatusStateComplete), + string(codersdk.WorkspaceAppStatusStateIdle), string(codersdk.WorkspaceAppStatusStateFailure), }, }, diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 556b3adb27b2e..6e95377bbaf42 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -19,6 +19,7 @@ type WorkspaceAppStatusState string const ( WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working" + WorkspaceAppStatusStateIdle WorkspaceAppStatusState = "idle" WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete" WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure" ) diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 7df27dca8fd4d..5dcfcd913ca65 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -929,6 +929,7 @@ Status Code **200** | `sharing_level` | `organization` | | `sharing_level` | `public` | | `state` | `working` | +| `state` | `idle` | | `state` | `complete` | | `state` | `failure` | | `lifecycle_state` | `created` | @@ -1685,6 +1686,7 @@ Status Code **200** | `sharing_level` | `organization` | | `sharing_level` | `public` | | `state` | `working` | +| `state` | `idle` | | `state` | `complete` | | `state` | `failure` | | `lifecycle_state` | `created` | diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index dd6f162f83a38..66c113a020af5 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -9680,6 +9680,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Value | |------------| | `working` | +| `idle` | | `complete` | | `failure` | diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index d695be4122951..85e865d8b4b37 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2557,6 +2557,7 @@ Status Code **200** | `sharing_level` | `organization` | | `sharing_level` | `public` | | `state` | `working` | +| `state` | `idle` | | `state` | `complete` | | `state` | `failure` | | `lifecycle_state` | `created` | @@ -3233,6 +3234,7 @@ Status Code **200** | `sharing_level` | `organization` | | `sharing_level` | `public` | | `state` | `working` | +| `state` | `idle` | | `state` | `complete` | | `state` | `failure` | | `lifecycle_state` | `created` | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 06acdfed6ef8d..586025f713a73 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3619,11 +3619,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", ]; diff --git a/site/src/modules/apps/AppStatusStateIcon.tsx b/site/src/modules/apps/AppStatusStateIcon.tsx index 829a8288235de..f713f49ed24b0 100644 --- a/site/src/modules/apps/AppStatusStateIcon.tsx +++ b/site/src/modules/apps/AppStatusStateIcon.tsx @@ -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 = ({ const className = cn(["size-4 shrink-0", customClassName]); switch (state) { + case "idle": + return ( + + ); case "complete": return ( diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx index d95444e658d64..43910b10e017a 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx @@ -39,6 +39,16 @@ export const Working: Story = { }, }; +export const Idle: Story = { + args: { + status: { + ...MockWorkspaceAppStatus, + state: "idle", + message: "Done for now", + }, + }, +}; + export const LongMessage: Story = { args: { status: { diff --git a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx index 90be0f194fef3..0bdc94e1f7d56 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx @@ -48,6 +48,22 @@ 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 LongStatusText: Story = { args: { agent: mockAgent([ From 86cf445d985f0e29d542502b5af40eb552570626 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 17 Jun 2025 12:13:57 -0800 Subject: [PATCH 2/2] chore: show status when message is blank The status row looks janky with just the icon. We can get into a "no message" state if the LLM never reported a status update. --- .../WorkspaceAppStatus.stories.tsx | 10 ++++++++++ .../WorkspaceAppStatus/WorkspaceAppStatus.tsx | 6 ++++-- .../WorkspacePage/AppStatuses.stories.tsx | 18 ++++++++++++++++++ site/src/pages/WorkspacePage/AppStatuses.tsx | 5 +++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx index 43910b10e017a..0e229467b994b 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx @@ -49,6 +49,16 @@ export const Idle: Story = { }, }; +export const NoMessage: Story = { + args: { + status: { + ...MockWorkspaceAppStatus, + state: "idle", + message: "", + }, + }, +}; + export const LongMessage: Story = { args: { status: { diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx index 0b999f54402a8..587ae9f5b062f 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx @@ -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 (
@@ -40,11 +42,11 @@ export const WorkspaceAppStatus = ({ })} /> - {status.message} + {message}
- {status.message} + {message} diff --git a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx index 0bdc94e1f7d56..c7ec5eb56f417 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx @@ -64,6 +64,24 @@ export const IdleState: Story = { ]), }, }; + +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([ diff --git a/site/src/pages/WorkspacePage/AppStatuses.tsx b/site/src/pages/WorkspacePage/AppStatuses.tsx index 35d4db46c3ac9..95e3f9c95a472 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.tsx @@ -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 = ({
- {latestStatus.message} + {latestStatus.message || capitalize(latestStatus.state)}
@@ -160,7 +161,7 @@ export const AppStatuses: FC = ({ latest={false} className="size-icon-xs w-[18px]" /> - {status.message} + {status.message || capitalize(status.state)} {formattedTimestamp}