Group and clear workspace status in the sidebar (#1317)
* Add status grouping to the workspace sidebar * Mark workspaces as read from the sidebar * Clean package builds before release checks * Fix sidebar status follow-up checks
This commit is contained in:
@@ -50,7 +50,7 @@ describe("deriveAgentStateBucket", () => {
|
||||
).toBe("attention");
|
||||
});
|
||||
|
||||
it("treats initializing agents as done for workspace buckets", () => {
|
||||
it("treats initializing agents as running for workspace buckets", () => {
|
||||
expect(
|
||||
deriveAgentStateBucket({
|
||||
status: "initializing",
|
||||
@@ -58,7 +58,7 @@ describe("deriveAgentStateBucket", () => {
|
||||
requiresAttention: false,
|
||||
attentionReason: null,
|
||||
}),
|
||||
).toBe("done");
|
||||
).toBe("running");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentLifecycleStatus } from "./agent-lifecycle.js";
|
||||
import type { WorkspaceStateBucket } from "./messages.js";
|
||||
|
||||
export type { WorkspaceStateBucket };
|
||||
export type AgentAttentionReason = "finished" | "error" | "permission" | null | undefined;
|
||||
|
||||
export interface AgentStateBucketInput {
|
||||
@@ -25,7 +26,7 @@ export function deriveAgentStateBucket(input: AgentStateBucketInput): WorkspaceS
|
||||
if (input.status === "error" || input.attentionReason === "error") {
|
||||
return "failed";
|
||||
}
|
||||
if (input.status === "running") {
|
||||
if (input.status === "running" || input.status === "initializing") {
|
||||
return "running";
|
||||
}
|
||||
if (input.requiresAttention) {
|
||||
|
||||
@@ -1595,6 +1595,12 @@ export const ArchiveWorkspaceRequestSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const WorkspaceClearAttentionRequestSchema = z.object({
|
||||
type: z.literal("workspace.clear_attention.request"),
|
||||
workspaceId: z.union([z.string(), z.array(z.string())]),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
// Highlighted diff token schema
|
||||
// Note: style can be a compound class name (e.g., "heading meta") from the syntax highlighter
|
||||
const HighlightTokenSchema = z.object({
|
||||
@@ -1898,6 +1904,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
LegacyOpenInEditorRequestSchema,
|
||||
OpenProjectRequestSchema,
|
||||
ArchiveWorkspaceRequestSchema,
|
||||
WorkspaceClearAttentionRequestSchema,
|
||||
FileExplorerRequestSchema,
|
||||
ProjectIconRequestSchema,
|
||||
FileDownloadTokenRequestSchema,
|
||||
@@ -2385,6 +2392,14 @@ export const WorkspaceDescriptorPayloadSchema = z
|
||||
name: z.string(),
|
||||
archivingAt: z.string().nullable().optional().default(null),
|
||||
status: WorkspaceStateBucketSchema,
|
||||
// Best-effort workspace status entry timestamp. Old daemons omit the
|
||||
// field; old clients treat missing and null equivalently. The transform
|
||||
// coerces a missing field to `null` so downstream code never has to
|
||||
// handle `undefined`.
|
||||
statusEnteredAt: z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((value) => value ?? null),
|
||||
activityAt: z.string().nullable(),
|
||||
diffStat: z
|
||||
.object({
|
||||
@@ -2668,6 +2683,25 @@ export const ClearAgentAttentionResponseMessageSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const WorkspaceClearAttentionResponseSchema = z.object({
|
||||
type: z.literal("workspace.clear_attention.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
workspaceId: z.union([z.string(), z.array(z.string())]),
|
||||
clearedAgentIds: z.array(z.string()),
|
||||
results: z.array(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
clearedAgentIds: z.array(z.string()),
|
||||
success: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
success: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const SendAgentMessageResponseMessageSchema = z.object({
|
||||
type: z.literal("send_agent_message_response"),
|
||||
payload: z.object({
|
||||
@@ -3667,6 +3701,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
FetchAgentTimelineResponseMessageSchema,
|
||||
CancelAgentResponseMessageSchema,
|
||||
ClearAgentAttentionResponseMessageSchema,
|
||||
WorkspaceClearAttentionResponseSchema,
|
||||
SendAgentMessageResponseMessageSchema,
|
||||
SetVoiceModeResponseMessageSchema,
|
||||
DaemonGetStatusResponseSchema,
|
||||
@@ -4010,6 +4045,7 @@ export type LegacyListAvailableEditorsRequest = z.infer<
|
||||
export type LegacyOpenInEditorRequest = z.infer<typeof LegacyOpenInEditorRequestSchema>;
|
||||
export type OpenProjectRequest = z.infer<typeof OpenProjectRequestSchema>;
|
||||
export type ArchiveWorkspaceRequest = z.infer<typeof ArchiveWorkspaceRequestSchema>;
|
||||
export type WorkspaceClearAttentionRequest = z.infer<typeof WorkspaceClearAttentionRequestSchema>;
|
||||
export type FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
|
||||
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
|
||||
export type ProjectIconRequest = z.infer<typeof ProjectIconRequestSchema>;
|
||||
|
||||
@@ -411,6 +411,73 @@ describe("workspace message schemas", () => {
|
||||
).toBe(archivingAt);
|
||||
});
|
||||
|
||||
// The protocol `statusEnteredAt` field is optional and defaults to null.
|
||||
// Old daemons omit it entirely and old clients must keep accepting that.
|
||||
test("defaults statusEnteredAt to null for legacy descriptors", () => {
|
||||
const baseWorkspace = {
|
||||
id: "ws-status-entered",
|
||||
projectId: "proj",
|
||||
projectDisplayName: "repo",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "worktree",
|
||||
name: "feature",
|
||||
status: "running",
|
||||
activityAt: null,
|
||||
scripts: [],
|
||||
} as const;
|
||||
expect(WorkspaceDescriptorPayloadSchema.parse(baseWorkspace).statusEnteredAt).toBeNull();
|
||||
});
|
||||
|
||||
test("preserves statusEnteredAt when present", () => {
|
||||
const baseWorkspace = {
|
||||
id: "ws-status-entered",
|
||||
projectId: "proj",
|
||||
projectDisplayName: "repo",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "worktree",
|
||||
name: "feature",
|
||||
status: "running",
|
||||
activityAt: null,
|
||||
scripts: [],
|
||||
} as const;
|
||||
const statusEnteredAt = "2026-05-12T10:00:00.000Z";
|
||||
expect(
|
||||
WorkspaceDescriptorPayloadSchema.parse({
|
||||
...baseWorkspace,
|
||||
statusEnteredAt,
|
||||
}).statusEnteredAt,
|
||||
).toBe(statusEnteredAt);
|
||||
});
|
||||
|
||||
test("preserves explicit statusEnteredAt: null for empty workspaces", () => {
|
||||
// The server emits `statusEnteredAt: null` for workspaces with no
|
||||
// contributing agents (the "done with no agents" case). The client must
|
||||
// distinguish this from "field omitted" — both parse to null, but the
|
||||
// round-trip must not lose the explicit null.
|
||||
const baseWorkspace = {
|
||||
id: "ws-status-entered",
|
||||
projectId: "proj",
|
||||
projectDisplayName: "repo",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "worktree",
|
||||
name: "feature",
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
scripts: [],
|
||||
} as const;
|
||||
const parsed = WorkspaceDescriptorPayloadSchema.parse({
|
||||
...baseWorkspace,
|
||||
statusEnteredAt: null,
|
||||
});
|
||||
expect(parsed.statusEnteredAt).toBeNull();
|
||||
});
|
||||
|
||||
test("parses legacy workspace descriptor enum values", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "workspace_update",
|
||||
|
||||
Reference in New Issue
Block a user