From b946f00f1a31f16a51f9bd1f579baf40c8eaed83 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Mar 2026 09:42:19 -0700 Subject: [PATCH] Show Plan Ready status pill for settled plan threads - add "Plan Ready" sidebar status when a plan-mode turn is settled with a proposed plan - update sidebar logic tests to cover the new status and expanded thread inputs --- apps/web/src/components/Sidebar.logic.test.ts | 32 +++++++++++++++++++ apps/web/src/components/Sidebar.logic.ts | 28 ++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 8a5e11e1afafb1ae9cb196d8dbdb0439ed0b344e..d0216d0e40c5b4b8bfae3ae64b999987a48616d5 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -20,8 +20,10 @@ describe("hasUnseenCompletion", () => { it("returns true when a thread completed after its last visit", () => { expect( hasUnseenCompletion({ + interactionMode: "default", latestTurn: makeLatestTurn(), lastVisitedAt: "2026-03-09T10:04:00.000Z", + proposedPlans: [], session: null, }), ).toBe(true); @@ -30,8 +32,10 @@ describe("hasUnseenCompletion", () => { describe("resolveThreadStatusPill", () => { const baseThread = { + interactionMode: "plan" as const, latestTurn: null, lastVisitedAt: undefined, + proposedPlans: [], session: { provider: "codex" as const, status: "running" as const, @@ -71,11 +75,39 @@ describe("resolveThreadStatusPill", () => { ).toMatchObject({ label: "Working", pulse: true }); }); + it("shows plan ready when a settled plan turn has a proposed plan ready for follow-up", () => { + expect( + resolveThreadStatusPill({ + thread: { + ...baseThread, + latestTurn: makeLatestTurn(), + proposedPlans: [ + { + id: "plan-1" as never, + turnId: "turn-1" as never, + createdAt: "2026-03-09T10:00:00.000Z", + updatedAt: "2026-03-09T10:05:00.000Z", + planMarkdown: "# Plan", + }, + ], + session: { + ...baseThread.session, + status: "ready", + orchestrationStatus: "ready", + }, + }, + hasPendingApprovals: false, + hasPendingUserInput: false, + }), + ).toMatchObject({ label: "Plan Ready", pulse: false }); + }); + it("shows completed when there is an unseen completion and no active blocker", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, + interactionMode: "default", latestTurn: makeLatestTurn(), lastVisitedAt: "2026-03-09T10:04:00.000Z", session: { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 44626cf14738dae2707d611749810768cb925d28..e950d8de6e06de3fdb0beab3e176b5f4cd89fd20 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1,13 +1,23 @@ import type { Thread } from "../types"; +import { findLatestProposedPlan, isLatestTurnSettled } from "../session-logic"; export interface ThreadStatusPill { - label: "Working" | "Connecting" | "Completed" | "Pending Approval" | "Awaiting Input"; + label: + | "Working" + | "Connecting" + | "Completed" + | "Pending Approval" + | "Awaiting Input" + | "Plan Ready"; colorClass: string; dotClass: string; pulse: boolean; } -type ThreadStatusInput = Pick; +type ThreadStatusInput = Pick< + Thread, + "interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session" +>; export function hasUnseenCompletion(thread: ThreadStatusInput): boolean { if (!thread.latestTurn?.completedAt) return false; @@ -63,6 +73,20 @@ export function resolveThreadStatusPill(input: { }; } + const hasPlanReadyPrompt = + !hasPendingUserInput && + thread.interactionMode === "plan" && + isLatestTurnSettled(thread.latestTurn, thread.session) && + findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null) !== null; + if (hasPlanReadyPrompt) { + return { + label: "Plan Ready", + colorClass: "text-violet-600 dark:text-violet-300/90", + dotClass: "bg-violet-500 dark:bg-violet-300/90", + pulse: false, + }; + } + if (hasUnseenCompletion(thread)) { return { label: "Completed", -- 2.54.0