mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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: "",
|
||||
},
|
||||
|
||||
Generated
+2
@@ -18081,11 +18081,13 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"working",
|
||||
"idle",
|
||||
"complete",
|
||||
"failure"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"WorkspaceAppStatusStateWorking",
|
||||
"WorkspaceAppStatusStateIdle",
|
||||
"WorkspaceAppStatusStateComplete",
|
||||
"WorkspaceAppStatusStateFailure"
|
||||
]
|
||||
|
||||
Generated
+2
-1
@@ -16522,9 +16522,10 @@
|
||||
},
|
||||
"codersdk.WorkspaceAppStatusState": {
|
||||
"type": "string",
|
||||
"enum": ["working", "complete", "failure"],
|
||||
"enum": ["working", "idle", "complete", "failure"],
|
||||
"x-enum-varnames": [
|
||||
"WorkspaceAppStatusStateWorking",
|
||||
"WorkspaceAppStatusStateIdle",
|
||||
"WorkspaceAppStatusStateComplete",
|
||||
"WorkspaceAppStatusStateFailure"
|
||||
]
|
||||
|
||||
Generated
+2
-1
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- It is not possible to delete a value from an enum, so we have to recreate it.
|
||||
CREATE TYPE old_workspace_app_status_state AS ENUM ('working', 'complete', 'failure');
|
||||
|
||||
-- Convert the new "idle" state into "complete". This means we lose some
|
||||
-- information when downgrading, but this is necessary to swap to the old enum.
|
||||
UPDATE workspace_app_statuses SET state = 'complete' WHERE state = 'idle';
|
||||
|
||||
-- Swap to the old enum.
|
||||
ALTER TABLE workspace_app_statuses
|
||||
ALTER COLUMN state TYPE old_workspace_app_status_state
|
||||
USING (state::text::old_workspace_app_status_state);
|
||||
|
||||
-- Drop the new enum and rename the old one to the final name.
|
||||
DROP TYPE workspace_app_status_state;
|
||||
ALTER TYPE old_workspace_app_status_state RENAME TO workspace_app_status_state;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE workspace_app_status_state ADD VALUE IF NOT EXISTS 'idle';
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ type WorkspaceAppStatusState string
|
||||
|
||||
const (
|
||||
WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working"
|
||||
WorkspaceAppStatusStateIdle WorkspaceAppStatusState = "idle"
|
||||
WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete"
|
||||
WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure"
|
||||
)
|
||||
|
||||
Generated
+2
@@ -933,6 +933,7 @@ Status Code **200**
|
||||
| `sharing_level` | `organization` |
|
||||
| `sharing_level` | `public` |
|
||||
| `state` | `working` |
|
||||
| `state` | `idle` |
|
||||
| `state` | `complete` |
|
||||
| `state` | `failure` |
|
||||
| `lifecycle_state` | `created` |
|
||||
@@ -1695,6 +1696,7 @@ Status Code **200**
|
||||
| `sharing_level` | `organization` |
|
||||
| `sharing_level` | `public` |
|
||||
| `state` | `working` |
|
||||
| `state` | `idle` |
|
||||
| `state` | `complete` |
|
||||
| `state` | `failure` |
|
||||
| `lifecycle_state` | `created` |
|
||||
|
||||
Generated
+1
@@ -9686,6 +9686,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
| Value |
|
||||
|------------|
|
||||
| `working` |
|
||||
| `idle` |
|
||||
| `complete` |
|
||||
| `failure` |
|
||||
|
||||
|
||||
Generated
+2
@@ -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` |
|
||||
|
||||
Generated
+6
-1
@@ -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([
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user