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:
Mohamed Boudra
2026-06-04 01:20:20 +08:00
committed by GitHub
parent 71ce96b913
commit 31ae545289
65 changed files with 3758 additions and 290 deletions
@@ -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");
});
});
+2 -1
View File
@@ -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) {
+36
View File
@@ -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",