Skip to content

🤖 feat: archive merged workspaces in project#2132

Merged
ThomasK33 merged 9 commits intomainfrom
workspace-mgmt-tjyv
Feb 5, 2026
Merged

🤖 feat: archive merged workspaces in project#2132
ThomasK33 merged 9 commits intomainfrom
workspace-mgmt-tjyv

Conversation

@ThomasK33
Copy link
Member

Summary

Adds a Command Palette action to archive all workspaces in a project whose GitHub PR state is MERGED (detected via gh pr view).

Implementation

  • Backend: new oRPC endpoint workspace.archiveMergedInProject that checks each non-archived workspace in the project and archives those whose PR state is MERGED.
  • Frontend: new Command Palette command “Archive Merged Workspaces in Project…” under Projects.
  • UX: confirmation warns it may start/wake workspace runtimes; success is silent, but partial failures are surfaced as an alert summary.

Validation

  • make static-check

Risks

  • This action may be slow / resource-heavy for projects with many stopped workspaces, since it intentionally wakes runtimes to run gh and perform cleanup.

📋 Implementation Plan

Plan: Command Palette — Archive merged workspaces (GitHub PR state)

Context / Why

Add a new Command Palette action that lets you archive all workspaces whose GitHub PR status is MERGED (as determined by Mux’s existing GitHub integration via gh pr view). This provides one-click cleanup of “done” workspaces without deleting them.

Evidence

  • Command palette is driven by buildCoreSources() and CommandIds.
    • src/browser/utils/commands/sources.ts (buildCoreSources, project/workspace prompt patterns)
    • src/browser/utils/commandIds.ts (central command ID registry)
    • src/browser/App.tsx (registers sources + supplies BuildSourcesParams callbacks)
  • Workspace archiving exists today:
    • Backend route: src/node/orpc/router.tsworkspace.archivecontext.workspaceService.archive()
    • Backend impl: src/node/services/workspaceService.ts#archive() sets archivedAt and emits updated metadata
  • GitHub PR “merged” state is already defined/used:
    • src/browser/stores/PRStatusStore.ts runs gh pr view --json ... via workspace.executeBash and treats state === "MERGED" as merged.
    • src/common/types/links.ts defines GitHubPRStatus.state: "OPEN" | "CLOSED" | "MERGED".

Implementation details (recommended approach)

1) Add a new ORPC endpoint to batch-archive merged workspaces

Goal: Avoid O(n) frontend→backend IPC loops by doing detection + archiving in one backend call.

  1. Schema: Add a new workspace endpoint in src/common/orpc/schemas/api.ts:

    • Name: workspace.archiveMergedInProject
    • Input: { projectPath: string }
    • Output: ResultSchema({ archivedWorkspaceIds: string[], skippedWorkspaceIds: string[], errors: { workspaceId: string, error: string }[] })
    // src/common/orpc/schemas/api.ts
    archiveMergedInProject: {
      input: z.object({ projectPath: z.string() }),
      output: ResultSchema(
        z.object({
          archivedWorkspaceIds: z.array(z.string()),
          skippedWorkspaceIds: z.array(z.string()),
          errors: z.array(z.object({ workspaceId: z.string(), error: z.string() })),
        }),
        z.string()
      ),
    },
  2. Router wiring: In src/node/orpc/router.ts under workspace: { ... }, add:

    archiveMergedInProject: t
      .input(schemas.workspace.archiveMergedInProject.input)
      .output(schemas.workspace.archiveMergedInProject.output)
      .handler(async ({ context, input }) => {
        return context.workspaceService.archiveMergedInProject(input.projectPath);
      }),
  3. Service implementation: Add WorkspaceService.archiveMergedInProject(projectPath) in src/node/services/workspaceService.ts.

    Algorithm (defensive + best-effort):

    • Fetch all workspace metadata: await this.config.getAllWorkspaceMetadata().
    • Filter candidates:
      • meta.projectPath === projectPath
      • not archived (!isWorkspaceArchived(meta.archivedAt, meta.unarchivedAt))
      • not MUX_HELP_CHAT_WORKSPACE_ID
    • For each candidate workspace, run the same GitHub integration signal the UI uses (via gh pr view):
      • Script (match PRStatusStore behavior; never hard-fail on “no PR”):
        • gh pr view --json state 2>/dev/null || echo '{"no_pr":true}'
      • Execute via this.executeBash(workspaceId, script, { timeout_secs: 15 }).
      • Interpret (defensive):
        • If IPC/tooling fails (no runtime, exec error) or output isn’t valid JSON ⇒ collect an error and skip.
        • If parsed JSON contains no_pr ⇒ skip (not an error).
        • If state === "MERGED" ⇒ mark for archive; otherwise skip.
    • Archive marked workspaces:
      • Prefer a new internal helper archiveMany(workspaceIds: string[]) to update config once and emit metadata updates once.
      • If keeping changes minimal, loop await this.archive(workspaceId) (acceptable but less efficient).

    Concurrency: Use a small concurrency limit (e.g., 3–5) for running gh to avoid starting too many runtimes at once.

    Return value: Always return Ok({ archivedWorkspaceIds, skippedWorkspaceIds, errors }) unless the input project is invalid (then Err("Project not found")).

2) Add the Command Palette command

  1. Command ID: Add an ID builder in src/browser/utils/commandIds.ts:

    workspaceArchiveMergedInProject: () => "ws:archive-merged-in-project" as const,
  2. BuildSourcesParams: Extend BuildSourcesParams in src/browser/utils/commands/sources.ts:

    onArchiveMergedWorkspacesInProject: (projectPath: string) => Promise<void>;
  3. Command action: Add a new command under the Projects section in buildCoreSources() (same pattern as “Create New Workspace in Project…”), prompting for a project then running the callback.

    list.push({
      id: CommandIds.workspaceArchiveMergedInProject(),
      title: "Archive Merged Workspaces in Project…",
      section: section.projects,
      keywords: ["archive", "merged", "pr", "github", "cleanup"],
      run: () => undefined,
      prompt: {
        title: "Archive Merged Workspaces",
        fields: [
          {
            type: "select",
            name: "projectPath",
            label: "Select project",
            placeholder: "Search projects…",
            getOptions: () =>
              Array.from(p.projects.keys()).map((projectPath) => ({
                id: projectPath,
                label: projectPath.split("/").pop() ?? projectPath,
                keywords: [projectPath],
              })),
          },
        ],
        onSubmit: async (vals) => {
          const projectPath = vals.projectPath;
          const ok = confirm(
            `Archive all merged workspaces in ${projectPath.split("/").pop() ?? projectPath}?\n\n` +
              `This uses the GitHub integration (gh pr view) to detect merged PRs and will hide those workspaces from the sidebar (reversible).`
          );
          if (!ok) return;
          await p.onArchiveMergedWorkspacesInProject(projectPath);
        },
      },
    });

3) Wire the command callback in App.tsx

  • In src/browser/App.tsx, create a callback that calls the new API method:
    • api.workspace.archiveMergedInProject({ projectPath })
    • On failure: alert(result.error) (palette actions should never throw)
    • On success: no toast required (the sidebar update is visible)
  • Pass this callback through registerParamsRef.current as onArchiveMergedWorkspacesInProject.

Tests / Validation

  • Backend unit test (preferred): a test that stubs WorkspaceService.executeBash to return:
    • JSON { "state": "MERGED" } for some workspaces and { "state": "OPEN" } for others
    • Verifies only merged ones end up archived.
  • Command source sanity (optional): unit test that buildCoreSources() includes the new command.

Estimated net new LoC (product code)

  • Recommended (backend batch + palette command): ~180–260 LoC
    • ORPC schema + router: ~30–50
    • WorkspaceService logic + concurrency + result plumbing: ~100–160
    • Command palette wiring (IDs + sources + App callback): ~50
Alternative (smaller change, but not preferred): frontend loops

Implement the command purely in the browser by:

  • Iterating workspaceMetadata for the chosen project
  • For each workspace: api.workspace.executeBash({ workspaceId, script: 'gh pr view --json state ...' })
  • For each merged: api.workspace.archive({ workspaceId })

Pros: ~60–90 LoC, no backend changes.
Cons: violates the repo guideline to avoid O(n) frontend→backend IPC loops; slow/fragile for many workspaces.

Estimated net new LoC: ~70–110 LoC.


Generated with mux • Model: openai:gpt-5.2 • Thinking: xhigh • Cost: $17.48

@github-actions github-actions bot added the enhancement New feature or functionality label Feb 3, 2026
@ThomasK33
Copy link
Member Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 force-pushed the workspace-mgmt-tjyv branch from 7340a04 to 0539351 Compare February 5, 2026 16:00
@ThomasK33 ThomasK33 added this pull request to the merge queue Feb 5, 2026
Merged via the queue into main with commit cdf1ce8 Feb 5, 2026
23 checks passed
@ThomasK33 ThomasK33 deleted the workspace-mgmt-tjyv branch February 5, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant