From c04ed02f8767cf48fecd74a2f9a1abebfec55438 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 7 Aug 2026 14:42:53 -0700 Subject: [PATCH 01/10] feat(environments): add archive undo and continue in new thread --- .../promptbox/FollowUpPromptBox.stories.tsx | 2 +- .../ThreadPromptContextBanner.stories.tsx | 2 + .../banner/ThreadPromptContextBanner.test.tsx | 76 +++++++++++ .../banner/ThreadPromptContextBanner.tsx | 68 ++++++---- .../hooks/mutations/thread-state-mutations.ts | 33 +++++ .../src/lib/thread-handoff-request.test.ts | 73 ++++++++++- apps/app/src/lib/thread-handoff-request.ts | 85 ++++++++++--- apps/app/src/views/RootComposeView.test.ts | 5 +- apps/app/src/views/RootComposeView.tsx | 118 +++++++++++++++++- .../ThreadDetailPromptArea.test.tsx | 4 +- .../thread-detail/ThreadDetailPromptArea.tsx | 32 ++++- .../views/thread-detail/ThreadDetailView.tsx | 6 + apps/server/src/constants.ts | 9 ++ apps/server/src/routes/threads/actions.ts | 18 ++- .../environment-cleanup-internal.ts | 24 ++++ .../src/services/system/periodic-sweeps.ts | 40 +++--- apps/server/src/start-server.ts | 2 + apps/server/src/types.ts | 7 ++ apps/server/test/helpers/test-app.ts | 2 + ...blic-thread-environment-decoupling.test.ts | 12 +- ...naged-environment-cleanup-recovery.test.ts | 101 +++++++++++---- .../test/system/bb-app-managed-config.test.ts | 1 + packages/db/src/data/index.ts | 1 + packages/db/src/data/sweeps.ts | 7 ++ packages/db/src/data/threads.ts | 32 +++++ ...onment-archive-grace-period-and-handoff.md | 101 +++++++++++++++ .../environment-isolation.test.ts | 77 ++++++++++++ tests/integration/helpers/api.ts | 13 +- tests/integration/helpers/fixtures.ts | 5 +- tests/integration/helpers/harness.ts | 5 + 30 files changed, 848 insertions(+), 113 deletions(-) create mode 100644 plans/environment-archive-grace-period-and-handoff.md diff --git a/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx b/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx index 58a2b99403..96d758582e 100644 --- a/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx +++ b/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx @@ -496,7 +496,7 @@ const archivedContextBannerElement: ReactNode = ( const environmentGoneContextBannerElement: ReactNode = ( { }, ); + it("renders an enabled handoff action once the environment is destroyed", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Continue in new thread"); + expect(markup).toContain(" { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Cleaning up..."); + expect(markup).toContain('disabled=""'); + expect(markup).not.toContain("Continue in new thread"); + }); + + it("prioritizes destroyed-environment handoff over unarchiving", () => { + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain("Environment is unavailable"); + expect(markup).toContain("Continue in new thread"); + expect(markup).not.toContain("Thread is archived"); + expect(markup).not.toContain(">Unarchive<"); + }); + it("labels a standalone pull request without non-actionable attention text", () => { const markup = renderToStaticMarkup( ; + /** + * Hands the surviving thread context off to a new thread. Enabled once the old + * workspace is fully gone (`destroyed`); while `destroying` the action shows a + * disabled "Cleaning up…" state. Omitted when no safe environment target can + * be derived. + */ + onHandoff?: () => void; } /** @@ -285,8 +292,7 @@ function SectionToggleButton({ // icon — the icons' own internal padding provides enough separation, // and a gap here makes the pair look untethered. label !== null && label !== undefined ? "gap-1.5" : "gap-0", - !active && - (isExpanded ? "text-foreground" : "text-muted-foreground"), + !active && (isExpanded ? "text-foreground" : "text-muted-foreground"), )} > {icon} @@ -317,9 +323,7 @@ function SectionToggleButton({ ; + onHandoff: () => void; +}) { + const cleaningUp = status === "destroying"; + const label = cleaningUp ? "Cleaning up..." : "Continue in new thread"; + return ( + + ); +} + const PULL_REQUEST_MERGE_ACTIONS: readonly { method: PullRequestMergeMethod; label: string; @@ -771,8 +796,7 @@ function ReadOnlyContextBanner({ }: ReadOnlyContextBannerProps) { const isParentThreadExpanded = expandedSection === "parentThread" && parentThreadSection !== null; - const hasMultipleSegments = parentThreadSection !== null; - const showStatusAction = statusAction !== null && !hasMultipleSegments; + const showStatusAction = statusAction !== null; return (