diff --git a/DESIGN.md b/DESIGN.md
index cff31c9a04c..7b860708c8e 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -153,7 +153,7 @@ results in place; keep failures actionable inline. Notifications for other work
remain useful. Repeated delivery of the same run outcome must refresh cached state
without repeating its toast, including after reconnecting. A terminal outcome
delivered more than five minutes after the run finished is historical and should
-refresh state silently. Expected cancellation is neutral gray, not an error. The composer's Stop action stops the current response and leaves the composer available for a new message. Pause work is a separate explicit task or subtree action. A paused task replaces the composer with an amber takeover. It says “Task is
+refresh state silently. Expected cancellation is neutral gray, not an error. A busy workspace is a transient system wait with no user action. Show it only as a quiet status in the composer strip and the Workspace property row, never as a pinned banner, conversation notice or toast. The composer's Stop action stops the current response and leaves the composer available for a new message. Pause work is a separate explicit task or subtree action. A paused task replaces the composer with an amber takeover. It says “Task is
paused.” and “Resume this task to send a message.” with a “Resume task” action.
Subtrees use “Subtree is paused.” and “Resume subtree.” The takeover cannot be
dismissed, retains drafts, and hides message inputs until the pause is released.
diff --git a/ui/src/components/IssueMonitorBanner.test.tsx b/ui/src/components/IssueMonitorBanner.test.tsx
index 9156408fc95..d117e87c1d6 100644
--- a/ui/src/components/IssueMonitorBanner.test.tsx
+++ b/ui/src/components/IssueMonitorBanner.test.tsx
@@ -206,13 +206,23 @@ describe("IssueMonitorBanner / IssueMonitorComposerStrip rendering", () => {
flushSync(() => root.unmount());
});
- it("explains automatic workspace waiting without promising that a reply bypasses the lock", () => {
+ it("does not raise a pinned banner for a workspace wait the user cannot act on (N-14)", () => {
+ const issue = {
+ status: "todo", scheduledRetry: { status: "scheduled_retry", scheduledRetryReason: "workspace_busy", scheduledRetryAt: NOW.toISOString() },
+ } as Issue;
+ const root = createRoot(container);
+ flushSync(() => root.render());
+ expect(container.textContent).toBe("");
+ flushSync(() => root.unmount());
+ });
+
+ it("keeps the workspace wait as a quiet composer status without promising that a reply bypasses the lock", () => {
const issue = {
status: "todo", scheduledRetry: { status: "scheduled_retry", scheduledRetryReason: "workspace_busy", scheduledRetryAt: NOW.toISOString() },
} as Issue;
const root = createRoot(container);
flushSync(() => root.render(<>>));
- expect(container.textContent).toContain("Waiting for workspace");
+ expect(container.querySelector('[data-testid="issue-monitor-composer-strip"]')?.textContent).toContain("Waiting for workspace");
expect(container.textContent).toContain("You can keep sending instructions while the agent waits.");
expect(container.textContent).not.toContain("wakes the agent now");
expect(container.querySelector("button")).toBeNull();
diff --git a/ui/src/components/IssueMonitorBanner.tsx b/ui/src/components/IssueMonitorBanner.tsx
index c0382a51f1d..21868393bad 100644
--- a/ui/src/components/IssueMonitorBanner.tsx
+++ b/ui/src/components/IssueMonitorBanner.tsx
@@ -278,6 +278,10 @@ export function IssueMonitorBanner({
const copy = useMonitorSurfaceCopy(issue, reviews.length);
if (!copy) return null;
+ // A busy workspace is a transient system wait the user cannot act on. It stays
+ // a quiet status (composer strip, properties row), never a pinned notice.
+ if (copy.workspaceWait) return null;
+
const canCancel = onCancelMonitor && deriveMonitorState(issue).source === "monitor";
return (
diff --git a/ui/src/components/IssueScheduledRetryCard.test.tsx b/ui/src/components/IssueScheduledRetryCard.test.tsx
index 8b6fdaea874..8099576fe17 100644
--- a/ui/src/components/IssueScheduledRetryCard.test.tsx
+++ b/ui/src/components/IssueScheduledRetryCard.test.tsx
@@ -121,11 +121,9 @@ function getRetryNowButton() {
}
describe("IssueScheduledRetryCard", () => {
- it("shows workspace contention as an automatic wait without failure or retry controls", () => {
+ it("raises no conversation notice for workspace contention the user cannot act on (N-14)", () => {
renderWithProviders();
- expect(container.textContent).toContain("Waiting for workspace");
- expect(container.textContent).toContain("Work starts automatically");
- expect(container.textContent).not.toMatch(/failed|Retry|Attempt|Replaces run/);
+ expect(container.textContent).toBe("");
expect(getRetryNowButton()).toBeNull();
});
it("surfaces model capacity and the automatic retry without a generic failure", () => {
diff --git a/ui/src/components/IssueScheduledRetryCard.tsx b/ui/src/components/IssueScheduledRetryCard.tsx
index 383fce3be87..dd6325bad5f 100644
--- a/ui/src/components/IssueScheduledRetryCard.tsx
+++ b/ui/src/components/IssueScheduledRetryCard.tsx
@@ -7,7 +7,6 @@ import { formatRetryReason } from "@/lib/runRetryState";
import type { IssueScheduledRetry } from "@paperclipai/shared";
import { useRetryNowMutation, type RetryNowError } from "../hooks/useRetryNowMutation";
import { Badge } from "@/components/ui/badge";
-import { InlineBanner } from "@/components/InlineBanner";
import { describeError } from "@/api/errors";
const MAX_TURN_CONTINUATION = "max_turns_continuation";
@@ -34,13 +33,9 @@ export function IssueScheduledRetryCard({
if (!scheduledRetry || !issueId) return null;
if (scheduledRetry.status !== "scheduled_retry") return null;
- if (scheduledRetry.scheduledRetryReason === "workspace_busy") {
- return (
-
- Another task is using this workspace. Work starts automatically when it is available.
-
- );
- }
+ // Workspace contention is a transient wait with nothing for the user to do.
+ // The composer strip and properties row carry the quiet status instead.
+ if (scheduledRetry.scheduledRetryReason === "workspace_busy") return null;
const continuation = isContinuationReason(scheduledRetry.scheduledRetryReason);
const dueAtIso = scheduledRetry.scheduledRetryAt