Skip to content

fix(tui): apply scope dialog to all selected plugins#40

Open
piekstra wants to merge 3 commits into
mainfrom
fix/multiselect-scope-applies-to-all
Open

fix(tui): apply scope dialog to all selected plugins#40
piekstra wants to merge 3 commits into
mainfrom
fix/multiselect-scope-applies-to-all

Conversation

@piekstra

Copy link
Copy Markdown
Contributor

Closes #39

Summary

Root cause: The "select scope" dialog (Shift-S) only acted on the focused/cursor plugin, ignoring the rest of a multi-selection. openScopeDialogForSelected called getSelectedPlugin() (singular), and scopeDialogState carried a single pluginID. When the dialog was confirmed, applyScopeDialogDelta wrote a pending operation for just that one plugin — so with several plugins bulk-selected (space / a), Shift-S still changed the scope of only one of them.

By contrast, the other bulk operations (l/p install, u uninstall) already used getSelectedPlugins() (plural). Those paths even bailed out to the single-plugin scope dialog (return // Dialog handles single plugin at a time) precisely because the dialog itself couldn't handle more than one plugin.

Fix: The scope dialog now acts on every bulk-selected plugin, falling back to the focused plugin when nothing is multi-selected (matching the existing bulk install/uninstall UX).

  • scopeDialogState gains a targets []scopeDialogTarget list; each target carries its own originalScopes. pluginID/originalScopes still mirror the first target so rendering and existing single-plugin callers/tests are unchanged.
  • openScopeDialogForSelected gathers getSelectedPlugins(), builds a target per non-header plugin, and seeds the checkboxes from the focused plugin's scopes.
  • applyScopeDialogDelta iterates over all targets, computing the install / uninstall / mixed-OpScopeChange delta per target against that plugin's own original scopes. This is the correct semantics: the checkboxes describe the desired end-state scope set, and each plugin transitions toward it from wherever it currently is.
  • The dialog title shows Scopes for N selected plugins when more than one is targeted.
  • openScopeDialog (single-plugin path used mid-loop by install/uninstall) is preserved as a thin wrapper over the new multi-target path, so behavior there is unchanged.

Testing

All commands run from the repo root.

  • go vet ./... — pass
  • gofmt -l . — no files need formatting
  • go run mvdan.cc/gofumpt@v0.9.2 -l -extra . — no files need formatting (matches mise run fmt)
  • golangci-lint run ./...0 issues.
  • go test -race -count=1 ./... — all packages pass (matches mise run test)
  • go build ... -o cpm ./cmd/cpm — builds successfully

New regression tests in internal/tui/model_test.go:

  • TestOpenScopeDialogForSelectedMultiSelectTargets — Shift-S loads all bulk-selected plugins as targets (not just the focused one)
  • TestOpenScopeDialogForSelectedFallsBackToFocused — with no multi-selection, only the focused plugin is targeted
  • TestApplyScopeDialogDeltaMultiSelectInstall / ...Uninstall — confirming applies the change to every selected plugin
  • TestApplyScopeDialogDeltaMultiSelectPerTargetDelta — each plugin's delta is computed against its own scopes (e.g. one gets OpScopeChange, another no-ops)
  • TestRenderScopeDialogMultiTargetTitle — multi-plugin title rendering

Caveats: mise/gofumpt weren't installed locally, so the documented mise run ... tasks were executed via their underlying commands (go test -race, golangci-lint run, gofumpt via go run, go build), which is exactly what those tasks invoke.

@piekstra piekstra marked this pull request as ready for review June 17, 2026 12:56
@piekstra piekstra requested a review from piekstra-dev June 17, 2026 12:56
Comment thread internal/tui/update.go
Comment thread internal/tui/update.go
@piekstra-dev

Copy link
Copy Markdown

Automated PR Review

Reviewed commit: 94f26845191e
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 1
go:implementation-tests (1 finding)

Minor - internal/tui/update.go:185

checkboxScopes is assigned focused.InstalledScopes without cloning, while every targets[i].originalScopes in the same function is explicitly maps.Cloned. openScopeDialogForTargets currently only reads checkboxScopes to initialize checkbox bits, so there is no present bug—but if that function is ever changed to retain a reference, it would silently alias live plugin state. Apply maps.Clone(focused.InstalledScopes) here to match the defensive style used for all other scope maps in this path.

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 2m 19s | ~$0.17 (est.) | claude-sonnet-4-6 | cr 0.4.161
Field Value
Model claude-sonnet-4-6
Reviewers go:implementation-tests
Engine claude_cli · claude-sonnet-4-6
Reviewed by cr · piekstra-dev
Duration 2m 19s wall · 2m 18s compute
Cost ~$0.17 (est.)
Tokens 10 in / 753 out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-4-6 4 507 7.2k 12.3k ~$0.06 (est.) 12s
go:implementation-tests claude-sonnet-4-6 3 123 3.9k 19.4k ~$0.08 (est.) 1m 54s
orchestrator-rollup claude-sonnet-4-6 3 123 3.9k 8.1k ~$0.03 (est.) 11s

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed with outcome: approved.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

PR Build

Version: 0.3.6-pr.40+e712c41

Platform Archive
Linux (amd64) cpm-linux-amd64
Linux (arm64) cpm-linux-arm64
macOS (universal) cpm-macos-universal
Windows (amd64) cpm-windows-amd64

Download artifacts

Built from 73b0872

Comment thread internal/tui/model.go
Comment thread internal/tui/model_test.go
@piekstra-dev

Copy link
Copy Markdown

Automated PR Review

Reviewed commit: 94f26845191e
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 1
go:implementation-tests (1 finding)

Minor - internal/tui/model_test.go

No test covers the branch in openScopeDialogForSelected where the focused plugin is absent from bulkSelected (user is hovering an unselected entry while other plugins are bulk-selected). In that path checkboxScopes falls back to targets[0].originalScopes rather than the focused plugin's scopes. The behaviour is non-obvious and can regress silently. Add a test: bulk-select {alpha, beta}, move focus to gamma (not selected), call openScopeDialogForSelected, and assert that targets contain only alpha and beta and that scopeDialog.scopes reflects the first target's (alpha's) installed scopes, not gamma's.

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 2m 36s | ~$0.26 (est.) | claude-sonnet-4-6 | cr 0.4.161
Field Value
Model claude-sonnet-4-6
Reviewers go:implementation-tests
Engine claude_cli · claude-sonnet-4-6
Reviewed by cr · piekstra-dev
Duration 2m 36s wall · 2m 35s compute
Cost ~$0.26 (est.)
Tokens 13 in / 2.3k out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-4-6 5 1.3k 20.1k 15.1k ~$0.08 (est.) 30s
go:implementation-tests claude-sonnet-4-6 4 682 27.7k 29.8k ~$0.13 (est.) 1m 54s
orchestrator-rollup claude-sonnet-4-6 4 407 7.7k 11.4k ~$0.05 (est.) 10s

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed with outcome: approved.

@piekstra piekstra requested a review from piekstra-dev June 17, 2026 13:08
@piekstra-dev

Copy link
Copy Markdown

Automated PR Review

Reviewed commit: b27c21958c4b
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 0

2 PR discussion threads considered. 2 summarized; 2 resolved.


Completed in 2m 51s | ~$0.16 (est.) | claude-sonnet-4-6 | cr 0.4.161
Field Value
Model claude-sonnet-4-6
Reviewers go:implementation-tests
Engine claude_cli · claude-sonnet-4-6
Reviewed by cr · piekstra-dev
Duration 2m 51s wall · 2m 49s compute
Cost ~$0.16 (est.)
Tokens 11 in / 2.3k out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-4-6 4 1.9k 7.2k 16.3k ~$0.09 (est.) 38s
go:implementation-tests claude-sonnet-4-6 3 130 3.3k 4.8k ~$0.02 (est.) 2m 03s
orchestrator-rollup claude-sonnet-4-6 4 293 7.2k 10.8k ~$0.05 (est.) 8s

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed with outcome: approved.

@piekstra piekstra requested a review from piekstra-dev June 17, 2026 13:45
@piekstra-dev

Copy link
Copy Markdown

Automated PR Review

Reviewed commit: 8c4e8e2b0486
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 0

4 PR discussion threads considered. 4 summarized; 4 resolved.


Completed in 2m 09s | ~$0.30 (est.) | claude-sonnet-4-6 | cr 0.4.161
Field Value
Model claude-sonnet-4-6
Reviewers go:implementation-tests
Engine claude_cli · claude-sonnet-4-6
Reviewed by cr · piekstra-dev
Duration 2m 09s wall · 2m 05s compute
Cost ~$0.30 (est.)
Tokens 12 in / 6.8k out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-4-6 4 1.2k 7.2k 18.1k ~$0.09 (est.) 26s
go:implementation-tests claude-sonnet-4-6 4 5.3k 7.2k 22.3k ~$0.17 (est.) 1m 30s
orchestrator-rollup claude-sonnet-4-6 4 307 7.2k 10.8k ~$0.05 (est.) 8s

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed with outcome: approved.

piekstra added 3 commits June 17, 2026 12:30
The "select scope" dialog (Shift-S) operated only on the focused
plugin: openScopeDialogForSelected called getSelectedPlugin (singular)
and scopeDialogState carried a single pluginID, so confirming the
dialog produced a pending operation for just the cursor item even when
multiple plugins were bulk-selected.

Extend the scope dialog to act on every bulk-selected plugin (falling
back to the focused plugin when nothing is multi-selected, matching the
existing bulk install/uninstall UX). scopeDialogState now tracks a list
of targets, each with its own original scopes, and applyScopeDialogDelta
computes the install/uninstall/scope-change delta per target relative to
that plugin's current scopes. The dialog title reflects when multiple
plugins are selected.

Add regression tests for multi-select target loading, focused-item
fallback, and per-target delta computation (install, uninstall, mixed
scope-change).

Closes #39
- Clone focused.InstalledScopes when seeding the scope dialog checkboxes,
  matching the maps.Clone style used for every target's originalScopes and
  avoiding aliasing live plugin state if the reference is later retained.
- Add TestApplyScopeDialogDeltaTargetlessFallback to explicitly exercise the
  len(targets)==0 backward-compatible fallback in applyScopeDialogDelta, and
  document the path from the fallback comment.
The scopeDialogState.originalScopes field aliased targets[0].originalScopes,
so mutating either backing map silently corrupted the other. The field was
read only by a targetless fallback that existed solely for direct struct
assignments in tests. Remove the mirror field (and the now-dead fallback),
reading original scopes from each target instead, and convert the affected
tests to populate targets directly.

Also add coverage for openScopeDialogForSelected when the focused plugin is
not bulk-selected: targets contain only the bulk selection and the dialog
checkboxes seed from the first target's scopes, not the focused plugin's.
@piekstra piekstra force-pushed the fix/multiselect-scope-applies-to-all branch from 8c4e8e2 to 73b0872 Compare June 17, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When multiple plugins are selected, the "select scope" mode (Shift-S) doesn't act on all of them

2 participants