From 96e2f3134e8bfb66d7ee292f3db52f02c8b985be Mon Sep 17 00:00:00 2001 From: onevcat Date: Fri, 25 Sep 2026 17:06:52 +0800 Subject: [PATCH] Document workspace editing --- docs-ai/072-workspace-editing/000-plan.md | 2 +- docs-ai/072-workspace-editing/001-action.md | 89 +++++++++++++++++++ docs/components/command-palette.md | 6 +- docs/components/repositories-and-worktrees.md | 7 +- docs/components/settings.md | 2 +- docs/components/workspaces.md | 72 ++++++++++++++- 6 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 docs-ai/072-workspace-editing/001-action.md diff --git a/docs-ai/072-workspace-editing/000-plan.md b/docs-ai/072-workspace-editing/000-plan.md index 786e3747..fcd0ca48 100644 --- a/docs-ai/072-workspace-editing/000-plan.md +++ b/docs-ai/072-workspace-editing/000-plan.md @@ -2,7 +2,7 @@ | | | | --- | --- | -| **Status** | Planned | +| **Status** | Implemented | | **Anchor date** | 2026-09-25 | | **Primary PRs** | (fill in as they merge) | | **Related** | [042-project-workspaces](../042-project-workspaces/000-plan.md), [062-workspace-child-diff](../062-workspace-child-diff/000-plan.md), `docs/components/workspaces.md`, PR #602 (superseded reference) | diff --git a/docs-ai/072-workspace-editing/001-action.md b/docs-ai/072-workspace-editing/001-action.md new file mode 100644 index 00000000..86be1258 --- /dev/null +++ b/docs-ai/072-workspace-editing/001-action.md @@ -0,0 +1,89 @@ +# 072 — Workspace Editing: Action Log + +## Timeline + +| Date | Change | Ref | +| --- | --- | --- | +| 2026-09-25 | Plan written after reading PR #602 and the entry-042 code; branch `feature/workspace-editing` from `main` (`869b5d9f`) | this entry | +| 2026-09-25 | Domain: `ProjectWorkspace.update`, `ProjectWorkspaceUpdateRequest` / `Member` / `Removal` / `Result`, description, task links, and role on creation, unknown-key-preserving `encodeMetadata`, minimum member count 1 | PR #TBD | +| 2026-09-25 | `WorkspaceCreationPromptFeature` → `WorkspaceEditorFeature` with `mode`, existing-member rows, staged removal, reorder, task links, description; `WorkspaceCreationPromptView` → `WorkspaceEditorView` | PR #TBD | +| 2026-09-25 | `RepositoriesFeature+WorkspaceEditing.swift` (`workspaceEditing` action family), entry points in sidebar, detail view, Settings, Command Palette, Worktrees menu | PR #TBD | +| 2026-09-25 | Tests, `docs/` manual updates, this record | PR #TBD | + +## Outcome & current state (as of 2026-09-25) + +- **Domain** — `supacode/Domain/ProjectWorkspace.swift` + - `ProjectWorkspaceCreationDraft` carries `description` and `taskLinks`; + `ProjectWorkspaceRepositoryPlan` / `ProjectWorkspaceCreationRepository` carry `role`. + `create` writes them and now accepts one repository (`notEnoughRepositories` reads + "Add at least one repository."). + - `ProjectWorkspace.update(_:fileManager:gitRunner:)` takes a + `ProjectWorkspaceUpdateRequest` (root, title, description, task links, ordered + `members` of `.existing(entry)` / `.added(plan)`, `removals`, `updatedAt`) and returns a + `ProjectWorkspaceUpdateResult` (saved workspace, `cleanupFailures`, + `completedRemovals`). Order: validate → materialize additions (ledger rollback on + failure, occupied names seeded with every current entry path) → write metadata → + best-effort cleanup of removals with `deleteFiles`. Cleanup deletes a symlink, a + remote clone folder, or runs `git worktree remove --force` against the recorded source; + paths outside the root and entries without a source are reported, never deleted. + Branch deletion is left to the caller through `completedRemovals`. + - `encodeMetadata(_:preservingUnknownKeysIn:)` merges the encoded model over the + existing JSON so unknown top-level and per-entry keys (matched by `id`, falling back + to `path` / `name`) survive; known keys always follow the model. +- **Editor reducer** — `supacode/Features/Repositories/Reducer/WorkspaceEditorFeature.swift` + - `State.mode` (`.create` / `.edit(repositoryID:)`), `existingRepositories` + (`WorkspaceEditorExistingRepository`: entry, editable name/role, `removal` with + `deleteFiles` / `deleteBranch`), `repositories` (new rows, unchanged), `description`, + `taskLinks` (`WorkspaceTaskLinkDraft`), `isSaving`. `init(editing:rootURL:repositoryID:...)` + pre-fills from a `ProjectWorkspace`. + - `submitButtonTapped` validates (title, ≥1 remaining member, new-row plans) and emits + `Delegate.submit(.create(draft))` or `.submit(.update(request))`; `updatedAt` comes + from `@Dependency(\.date.now)`. +- **Repositories wiring** — + `supacode/Features/Repositories/Reducer/RepositoriesFeature+WorkspaceEditing.swift` + - `WorkspaceEditingAction`: `promptRequested(id, removingChildID:)` re-reads the metadata + from disk in an effect and presents the editor (`promptLoaded`), optionally pre-marking + a child (sidebar row id = working-directory path); `saveWorkspace` runs + `ProjectWorkspace.update`, deletes opted-in branches through + `gitClient.deleteLocalBranch` (protected branches skipped), then `workspaceSaved` + reloads repositories, toasts "Workspace saved", and alerts on cleanup failures; + `workspaceSaveFailed` keeps the sheet open with the message (or alerts when the sheet + is already gone). Save is deliberately not cancellable. + - `RepositoriesFeature.State.workspaceEditor` replaces `workspaceCreationPrompt`; + `RepositoriesFeature+CoreReducer.swift` routes the editor delegate by mode. +- **Views** — `supacode/Features/Repositories/Views/WorkspaceEditorView.swift` (shared form; + existing rows with Name/Role, provenance line, Move Up/Down, trash → inline removal + options + Undo; new rows gain Role; Description and Task Links sections; Cancel disabled + while saving in edit mode), `WorkspaceDetailView.swift` (Edit Workspace… button, + clickable http(s) task links), `WorkspaceChildRowsView.swift` (Edit Workspace… / + Remove from Workspace…), `RepositorySectionView.swift` (header menu item), + `supacode/Features/Settings/Views/RepositorySettingsView.swift` (Edit Workspace… button + replacing the read-only note). +- **Other entry points** — `RepositorySettingsFeature.Delegate.editWorkspace` → + `SettingsFeature.Delegate.editWorkspace` → `AppFeature` surfaces the main window and + sends `promptRequested`; `CommandPaletteItem.Kind.editWorkspace` (offered when the active + repository is a workspace); `supacode/Commands/WorktreeCommands.swift` "Edit Workspace...". +- **Tests** — `supacodeTests/ProjectWorkspaceUpdateTests.swift`, + `supacodeTests/WorkspaceEditorFeatureTests.swift`, + `supacodeTests/RepositoriesFeatureWorkspaceEditingTests.swift`, + `supacodeTests/AppFeatureWorkspaceEditingTests.swift`; existing creation tests updated + for the rename and the new submission enum. +- **Docs** — `docs/components/workspaces.md` (Editing a workspace, metadata no longer + read-only, minimum count), `docs/components/command-palette.md`, + `docs/components/repositories-and-worktrees.md`, `docs/components/settings.md`. + +## Deviations from plan + +- The plan listed "settings-originated request through `AppFeature`" as a test; it exists, + and `AppFeature` uses `appLifecycleClient.surfaceMainWindow` rather than a new client. +- `.dismiss` on the sheet cannot be blocked while saving: TCA's `ifLet` clears the child + state before the reducer sees the action. The Cancel button is disabled instead, and a + failure arriving after the sheet is gone is shown as an alert. Not a behavior gap, but + the plan implied the reducer could refuse a dismissal. +- Unknown-key preservation also matches entries without an `id` (by `path`, then `name`), + which the plan did not spell out. + +## Open questions + +- Reordering is Move Up / Move Down buttons; drag reordering inside the sheet was not + attempted. Revisit if the list grows beyond a handful of members. diff --git a/docs/components/command-palette.md b/docs/components/command-palette.md index 13021080..2ed453bc 100644 --- a/docs/components/command-palette.md +++ b/docs/components/command-palette.md @@ -43,8 +43,10 @@ selected worktree has a pull request). Merge PR, Close PR. See [github-pull-requests](github-pull-requests.md). - **Terminal:** font size, find, tab/pane selection, new/close terminal (mirrors the Ghostty-bridged commands; search-only). -- **App:** Check for Updates, Open Settings, Open Repository, **Install Command - Line Tool**, Repo Settings. +- **App:** Check for Updates, Open Settings, Open Repository, New Workspace, + **Install Command Line Tool**, Repo Settings, and **Edit Workspace** while a + workspace (or one of its children) is selected — see + [workspaces](workspaces.md). - **Custom commands:** enabled local and Global Custom Commands appear here with their source. Same-titled commands can coexist; disabled commands do not appear. - **Agent profiles** (when a terminal worktree is selected): a diff --git a/docs/components/repositories-and-worktrees.md b/docs/components/repositories-and-worktrees.md index fe8f2415..cf61d209 100644 --- a/docs/components/repositories-and-worktrees.md +++ b/docs/components/repositories-and-worktrees.md @@ -228,9 +228,10 @@ and the confirmation names the target worktree). The repository **header** menu (right-click, or the **⋯** button) offers **New Worktree** (git repos only), **Repo Settings…**, and **Remove Repository**. Plain folders and workspaces get **Copy Path** / -**Reveal in Finder** instead of New Worktree; git repo headers deliberately have -no path actions because a repository's root can be a bare directory — use the -worktree rows for paths. +**Reveal in Finder** instead of New Worktree, and workspaces additionally get +**Edit Workspace…** (see [workspaces](workspaces.md)); git repo headers +deliberately have no path actions because a repository's root can be a bare +directory — use the worktree rows for paths. ## Repository appearance (icon & color) diff --git a/docs/components/settings.md b/docs/components/settings.md index c32e4cd7..18e69a99 100644 --- a/docs/components/settings.md +++ b/docs/components/settings.md @@ -39,7 +39,7 @@ and opens that section's root. | **Agents → Profiles** | Named launch presets for supported agent runtimes (model, effort, execution mode, tab/split placement, extra arguments, opt-in dedicated home for a separate account) with a live launch preview. List order is the recommendation fallback. → [agent-profiles](agent-profiles.md) | | **Agents → CLI & Skills** | Install/status for the bundled `prowl` CLI, the local socket path it uses to reach the app and whether this app is **listening** on it, and the **Agent Skills** section that links the bundled skills into your agents' skill folders. → [cli](cli.md) | | **Agents → Workflows** | Compact Built-in and personal workflow lists. Select a row for Enabled, explicit Run target, role profile preferences, Run Setup, validation, and source-file actions; **New Workflow…**, **Create with Agent…**, the **Run History** summary with **Clear History…**, and the CLI dependency banner remain on the index. → [workflows](workflows.md) | -| **Repositories / Repo Settings** | Per-repository: setup/archive/run scripts, **Custom Commands**, Global-command visibility, **Default Agent Profile**, a direct **Workflows** list, default base ref & directory, copy-files overrides, open-with app, custom title, icon & color, PR merge strategy, line-diff & PR-state fetching. Reached from the sidebar context menu → "Repo Settings". → [custom-actions](custom-actions.md), [repositories-and-worktrees](repositories-and-worktrees.md), [workflows](workflows.md) | +| **Repositories / Repo Settings** | Per-repository: setup/archive/run scripts, **Custom Commands**, Global-command visibility, **Default Agent Profile**, a direct **Workflows** list, default base ref & directory, copy-files overrides, open-with app, custom title, icon & color, PR merge strategy, line-diff & PR-state fetching. Reached from the sidebar context menu → "Repo Settings". A workspace's page also shows its metadata with an **Edit Workspace…** button that opens the editor on the main window. → [custom-actions](custom-actions.md), [repositories-and-worktrees](repositories-and-worktrees.md), [workflows](workflows.md), [workspaces](workspaces.md) | ## Where settings live on disk diff --git a/docs/components/workspaces.md b/docs/components/workspaces.md index cab3df17..91859983 100644 --- a/docs/components/workspaces.md +++ b/docs/components/workspaces.md @@ -23,7 +23,13 @@ Use **Add...** from the sidebar toolbar and choose **Add Workspace**, or use the Worktrees menu or command palette to create a workspace. Prowl creates the shared folder, materializes the selected repositories, writes `.prowl/workspace.json`, and opens the workspace as a runnable folder. A -workspace needs at least two repositories. +workspace needs at least one repository; more can be added later (see +[Editing a workspace](#editing-a-workspace)). + +Besides the title and folder, the creation sheet takes an optional +**Description**, a list of **Task Links** (URLs or issue keys, one per row), +and an optional **Role** per repository (`app`, `backend`, `docs`, …). All of +them land in `.prowl/workspace.json` so agents can read them. While a workspace is being created the prompt shows a spinner. **Cancel** stops the creation and rolls back everything created so far: cloned folders, created @@ -112,7 +118,9 @@ including immediately after a newly created workspace is opened. Click a child row to select it and focus its terminal tab rooted at that repository folder inside the workspace, creating that tab the first time it is selected. Right-click a child row for **Copy Path** / **Reveal in Finder** / -**Show Diff** / **Show Outgoing Changes**. +**Show Diff** / **Show Outgoing Changes** / **Edit Workspace…** / +**Remove from Workspace…** (opens the editor with that repository already +marked for removal). Diff works per child repository: click the child's `+N/-M` badge to open the diff for that repository, or, with a child selected, use `⌘⇧Y` (Show Diff), @@ -120,6 +128,59 @@ diff for that repository, or, with a child selected, use `⌘⇧Y` (Show Diff), configured diff tools apply; the Hunk tool opens a workspace terminal tab rooted at the child folder. See [diff-view](diff-view.md). +## Editing a workspace + +A workspace stays editable after creation. Open the editor from any of: + +- the sidebar: right-click the workspace row (or its `…` menu) → **Edit + Workspace…**, or right-click a child row → **Edit Workspace…** / **Remove + from Workspace…**; +- the workspace detail view (select the workspace row) → **Edit Workspace…**; +- Settings → the workspace under Repositories → **Edit Workspace…** (the editor + opens as a sheet on the main window, which is brought to the front); +- the Command Palette → **Edit Workspace** while a workspace or one of its + children is selected; +- the Worktrees menu → **Edit Workspace...** (enabled when a workspace is + selected). + +The editor is the same sheet as creation, pre-filled from +`.prowl/workspace.json` (re-read from disk when it opens, so edits made +outside Prowl are respected). It lets you change: + +- **Title**, **Description**, and **Task Links**. The title is display-only; + the folder on disk keeps its name. +- **Name** and **Role** of every existing repository. Source, path, and + checkout are shown as read-only provenance; to change them, remove the + repository and add it again. +- **Order**: move existing repositories up or down. The sidebar and the + metadata follow the new order. +- **Added repositories**: the same **Add Opened** / **Add Remote** / **Add + Local** sources and Link / Create Branch / Use Existing checkouts as + creation. They are materialized when you save. +- **Removed repositories**: the trash button marks a repository for removal + (**Undo** restores it). By default only the metadata entry goes away and the + folder stays. Tick **Also delete the folder inside the workspace** to remove + what Prowl materialized: the symlink for a linked repository (the source is + untouched), `git worktree remove --force` plus the folder for a worktree, or + the cloned folder for a remote. Worktree entries with a recorded branch + additionally offer **Delete branch … in the source repository**, which goes + through the same protected-branch guard as the rest of Prowl. A workspace + must keep at least one repository. + +Nothing changes until you press **Save**. Save runs in this order so the +workspace is never left half-changed: added repositories are materialized +first (and rolled back if one fails, leaving the old metadata untouched), then +`.prowl/workspace.json` is rewritten, then removed repositories are cleaned +up. Cleanup is best-effort: if a worktree cannot be unregistered, or a +repository path lies outside the workspace folder, the entry is still removed +from the metadata and Prowl shows an alert listing what was left on disk. Save +cannot be canceled once it has started. After a successful save the sidebar +reloads and a **Workspace saved** toast appears. + +Paths inside the workspace are kept unique: adding a repository whose folder +name is still occupied (for example by a member you are removing without +deleting its folder) gets a `-2` suffix instead of failing. + ## Removing a workspace **Remove Repository** on a workspace opens a dedicated confirmation. By default @@ -141,8 +202,11 @@ anyway would leave a dangling worktree registration in the source repository. ## Metadata The workspace's repository settings page (Settings → the workspace under -Repositories) shows this metadata read-only; edit `.prowl/workspace.json` to -change it. +Repositories) shows this metadata and offers **Edit Workspace…**, which opens +the editor described above. `.prowl/workspace.json` remains the source of +truth: hand edits are picked up on the next reload, and when Prowl saves it +keeps top-level and per-repository keys it does not know about, so fields +written by other tools survive. Example `.prowl/workspace.json`: -- 2.51.2