Add "Mark as ready to review" to workspace context menu#2166
Conversation
Adds the inverse of "Mark as read" — users can now manually move done workspaces back to the "Ready to review" bucket via the workspace context menu. Introduces a new `workspace.set_attention` RPC (parallel to `workspace.clear_attention`) that sets attention on all non-archived agents in a workspace with a new `"manual"` attention reason. The `"manual"` reason was added to the `AgentAttentionReason` union across the protocol, server storage schema, client, and app. The menu item appears only when `workspace.status === "done"`.
|
| Filename | Overview |
|---|---|
| packages/app/src/utils/agent-attention.ts | Added "manual" to ATTENTION_REASON_PRIORITY but missing exemption in shouldClearAgentAttention — manually-flagged workspaces will silently auto-clear when the user navigates into them. |
| packages/server/src/server/agent/agent-manager.ts | New markAgentAttention method mirrors clearAgentAttention correctly; contains a dead null-guard that the TypeScript type already prevents. |
| packages/server/src/server/session.ts | New handleWorkspaceSetAttentionRequest handler follows the existing clear_attention pattern; correctly scopes agent marking to workspaceId and handles batch workspaceId input. |
| packages/app/src/hooks/use-mark-workspace-attention.ts | New hook mirrors useClearWorkspaceAttention; correctly gates on workspace.status === "done" and reuses the hostDisconnected i18n key for the client-not-found error. |
| packages/protocol/src/messages.ts | Adds WorkspaceSetAttentionRequest/ResponseSchema and registers them in both inbound and outbound discriminated unions; enum updates are consistent across all four relevant schemas. |
| packages/app/src/components/sidebar/sidebar-workspace-menu.tsx | New menu item wired correctly; also fixes the previously hardcoded "Mark as read" string to use the i18n key. |
| packages/client/src/daemon-client.ts | New markWorkspaceAttention method is a clean mirror of the existing clearWorkspaceAttention pattern. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant SidebarRow as SidebarWorkspaceRow
participant Hook as useMarkWorkspaceAttention
participant Client as DaemonClient
participant Session as Session (Server)
participant AgentMgr as AgentManager
participant Storage as AgentStorage
User->>SidebarRow: Right-click "Mark as ready to review"
SidebarRow->>Hook: markAttention()
Hook->>Hook: "Guard: workspace.status === "done"?"
Hook->>Client: markWorkspaceAttention(workspaceId)
Client->>Session: workspace.set_attention.request
Session->>Session: listAgentPayloads()
loop For each agent in workspace
alt Live agent
Session->>AgentMgr: markAgentAttention(agentId, "manual")
AgentMgr->>Storage: persistSnapshot(agent)
AgentMgr->>Session: emitState(agent)
else Stored agent
Session->>Storage: agentStorage.upsert(nextRecord)
Session->>Client: agent_update event
end
end
Session->>Client: emitWorkspaceUpdateForWorkspaceId
Session->>Client: workspace.set_attention.response
Client->>SidebarRow: resolve / throw
Note over SidebarRow: Workspace moves from Done to Ready to review
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant SidebarRow as SidebarWorkspaceRow
participant Hook as useMarkWorkspaceAttention
participant Client as DaemonClient
participant Session as Session (Server)
participant AgentMgr as AgentManager
participant Storage as AgentStorage
User->>SidebarRow: Right-click "Mark as ready to review"
SidebarRow->>Hook: markAttention()
Hook->>Hook: "Guard: workspace.status === "done"?"
Hook->>Client: markWorkspaceAttention(workspaceId)
Client->>Session: workspace.set_attention.request
Session->>Session: listAgentPayloads()
loop For each agent in workspace
alt Live agent
Session->>AgentMgr: markAgentAttention(agentId, "manual")
AgentMgr->>Storage: persistSnapshot(agent)
AgentMgr->>Session: emitState(agent)
else Stored agent
Session->>Storage: agentStorage.upsert(nextRecord)
Session->>Client: agent_update event
end
end
Session->>Client: emitWorkspaceUpdateForWorkspaceId
Session->>Client: workspace.set_attention.response
Client->>SidebarRow: resolve / throw
Note over SidebarRow: Workspace moves from Done to Ready to review
Comments Outside Diff (1)
-
packages/app/src/utils/agent-attention.ts, line 64-82 (link)Manually-flagged workspaces auto-clear on navigation
shouldClearAgentAttentionexempts"permission"from auto-clearing but has no such exemption for"manual". Any trigger (focus-entry,input-focus,prompt-send,agent-blur) will clear a manually-flagged workspace as soon as the user opens it. The net effect: a user clicks "Mark as ready to review", then navigates into the workspace to inspect it, and the workspace immediately silently returns to "Done" — precisely the state the user was trying to escape. Adding"manual"to the exemption list (alongside"permission") would preserve the flag until the user explicitly selects "Mark as read".
Reviews (1): Last reviewed commit: "feat(app): add "Mark as ready to review"..." | Re-trigger Greptile
| async markAgentAttention( | ||
| agentId: string, | ||
| reason: Exclude<AgentAttentionReason, null | undefined>, | ||
| ): Promise<void> { | ||
| if (!reason) { | ||
| throw new Error("Cannot mark agent attention without a reason"); | ||
| } | ||
| const agent = this.requireAgent(agentId); |
There was a problem hiding this comment.
The
if (!reason) guard is unreachable — the TypeScript type Exclude<AgentAttentionReason, null | undefined> already excludes null and undefined from the call-site, so this branch can never fire. It's unnecessary defensive code that the type system renders dead.
| async markAgentAttention( | |
| agentId: string, | |
| reason: Exclude<AgentAttentionReason, null | undefined>, | |
| ): Promise<void> { | |
| if (!reason) { | |
| throw new Error("Cannot mark agent attention without a reason"); | |
| } | |
| const agent = this.requireAgent(agentId); | |
| async markAgentAttention( | |
| agentId: string, | |
| reason: Exclude<AgentAttentionReason, null | undefined>, | |
| ): Promise<void> { | |
| const agent = this.requireAgent(agentId); |
Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Adds a context menu item that lets users manually move a workspace from the Done bucket back to the Ready to review bucket. This is the inverse of the existing "Mark as read" action (which clears attention).
Motivation
When a workspace finishes and moves to "Done", there's no way to manually flag it for review later. Users sometimes need to re-surface a completed workspace for review without restarting an agent. This provides that capability through a simple context menu action.
Changes
New RPC:
workspace.set_attentionWorkspaceSetAttentionRequestSchema/WorkspaceSetAttentionResponseSchema— mirrors the existingworkspace.clear_attentionRPCAgentManager.markAgentAttention()method +handleWorkspaceSetAttentionRequest()handler in session.tsDaemonClient.markWorkspaceAttention()methodNew attention reason:
"manual"The existing reasons (
"finished","error","permission") are all set automatically by agent state transitions. Manual marking needs its own reason to distinguish it from an agent that genuinely just finished. Added"manual"toAgentAttentionReasonacross all locations:messages.ts,agent-state-bucket.ts,agent-attention-notification.ts)agent-storage.ts)session-store.ts,agent-attention.ts,use-agent-attention-clear.ts,session-context.tsx)UI
useMarkWorkspaceAttention(parallel touseClearWorkspaceAttention)sidebar-workspace-menu.tsxwithEyeiconsidebar-workspace-row.tsx— appears only whenworkspace.status === "done"i18n
Added
markAsReadyToReviewandmarkAsReadtranslations across all 8 languages (en, ar, es, fr, ja, pt-BR, ru, zh-CN). Also fixed the previously hardcoded "Mark as read" label to use an i18n key.How it works
client.markWorkspaceAttention(workspaceId)requiresAttention: true, attentionReason: "manual"on all non-archived agents in the workspaceVerification
npm run typecheck✅npm run lint✅npm run build:client✅npm run build:server✅Files changed
packages/protocol/src/agent-state-bucket.ts"manual"to reason typepackages/protocol/src/agent-attention-notification.ts"manual"to reason type + resolverspackages/protocol/src/messages.tspackages/server/src/server/agent/agent-storage.ts"manual"to stored schema enumpackages/server/src/server/agent/agent-manager.tsmarkAgentAttention()methodpackages/server/src/server/session.tspackages/client/src/daemon-client.tsmarkWorkspaceAttention()methodpackages/app/src/hooks/use-mark-workspace-attention.tspackages/app/src/utils/agent-attention.ts"manual"to priority mappackages/app/src/hooks/use-agent-attention-clear.tsAttentionReasontypepackages/app/src/contexts/session-context.tsxnotifyAgentAttentionsignaturepackages/app/src/stores/session-store.tspackages/app/src/components/sidebar/sidebar-workspace-menu.tsxpackages/app/src/components/sidebar/sidebar-workspace-row.tsxpackages/app/src/i18n/resources/*.ts(8 files)markAsReadyToReview+markAsRead