v0.21.14.0 fix(rm): enrich workspace-not-found diagnostic + pin remote dispatch to row's project#66
Merged
Conversation
`canopy rm <name>` (most painfully through the SSH dispatch on a remote host) used to fail with the bare `workspace.Find(X): workspace: not found`, leaving no signal about whether the cause was a typo, a stale TUI row, or the wrong project on the remote. `rmEnrichNotFound` replaces that line with: the project the lookup ran against, the workspaces that ARE registered there, a cross-project hint when a same-named workspace exists under another project root, and a pointer at --force as the silent-success escape hatch. Best-effort: store/list failures degrade to the shorter form rather than masking the underlying not-found signal. `--force`'s rmHandleFindErr shortcut still wins, so the TUI's force-delete-on-stale-row path stays quiet. Tests cover siblings listed, empty project, cross-project hint present, and no false-positive cross-project hint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…v0.21.14.0) When the TUI dispatched a remote `canopy rm`/`retry` for a row on tower, it ran the verb from the laptop user's cwd. If that cwd wasn't inside a registered project (the bare-`canopy`-from-`~` case), cmd/canopy's `resolveOnForSwitch` fell back to "first project on host" and rendered the dispatch source as `registry:tower/<project> (fallback)`. Benign on a one-project host, silently-wrong on a multi-project host. `internal/ui/update_remote.go` gains a `remoteCwdArg` helper that returns `["--remote-cwd", <path>]` when the in-memory host registry (`m.hostList[].Projects` — populated from `~/.canopy/hosts.json`) knows the path for (host, project), and `nil` otherwise. `update_delete.go` and `update_retry.go` thread it through `execRemoteVerb`. When the registry doesn't know the path we stay in the legacy no-flag shape on purpose — a guessed path that doesn't exist on the remote would trip `buildRemoteScript`'s exit-7 cwd pre-check and degrade the diagnostic further. `findDeleteTargetRemoteHost` now also returns the row's Project so the (host, project) lookup happens at the dispatch site without re-walking `m.filteredRows`. Composes with v0.21.13.0's enriched not-found error: this lands the verb in the right project, and when the workspace still isn't there the remote canopy explains why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two fixes that compose to address "remote
canopy rmis opaque when it fails."v0.21.13.0 — remote-side: enrich the not-found error.
cmd/canopy/rm.gogetsrmEnrichNotFound, replacing the bareworkspace.Find(X): workspace: not foundwith: the project the lookup ran against, the workspaces that ARE registered there, a cross-project hint when a same-named workspace exists under another project root, and a pointer at--forceas the silent-success escape hatch. The--forceshortcut inrmHandleFindErrstill wins, so the TUI's force-delete-on-stale-row path stays quiet. Best-effort:mgr.List/mgr.Store.Loadfailures degrade to the shorter form.v0.21.14.0 — local-side: pin the remote dispatch to the row's project. When the TUI dispatched a remote
canopy rmorretryfor a row on tower, it ran the verb from the laptop's cwd. If that cwd wasn't inside a registered project,cmd/canopy/host_resolve.go'sresolveOnForSwitchfell back to "first project on host" and rendered the dispatch source asregistry:tower/<project> (fallback). Benign on a one-project host; a real correctness risk on a multi-project host (the rm could land in the wrong project).internal/ui/update_remote.gogainsremoteCwdArg, returning[\"--remote-cwd\", <path>]when the in-memory host registry knows the path andnilotherwise.update_delete.goandupdate_retry.goboth thread it throughexecRemoteVerb.These compose: the dispatcher lands the verb in the right project (v0.21.14.0), and when the workspace still isn't there, the remote canopy explains why (v0.21.13.0).
Most useful through the SSH dispatch on a remote host. Before:
After:
Test Coverage
cmd/canopy/rm_test.go:TestRmEnrichNotFound_ListsSiblings— siblings listed; project name, root,--force, and stale-list hints all surface.TestRmEnrichNotFound_EmptyProject— fresh project; distinct "no workspaces registered" line.TestRmEnrichNotFound_CrossProjectHint— same-named workspace under another project root is surfaced.TestRmEnrichNotFound_NoCrossProjectFalsePositive— no cross-project hint when no other project has a same-named workspace.internal/ui/model_test.go:TestRemoteCwdArg_PinsKnownProject— when the host registry knows the (host, project) path,remoteCwdArgreturns[\"--remote-cwd\", <path>].TestRemoteCwdArg_UnknownProjectReturnsNil— when the registry doesn't know the path (unknown host or unknown project), returns nil so dispatch stays in its legacy shape. Both branches pinned.Existing
rmHandleFindErr/remoteCwdForRowtests still pass.go test ./...clean.Pre-Landing Review
Self-review on the diff:
listErr/sErrignored; original not-found still surfaced).remoteCwdArgreturns nil on unknown paths instead of guessing — guessed paths would tripbuildRemoteScript's exit-7 cwd pre-check and produce a worse diagnostic than the legacy fallback.findDeleteTargetRemoteHostsignature change: callers updated (one inupdate_delete.go, one inview.go).Test plan
go test ./...passes locallycanopyfrom~) and confirm:(fallback)annotation--remote-cwd /path/to/projectDeployment note
The v0.21.13.0 piece changes the remote canopy's error message — it only takes effect once the remote host upgrades to v0.21.13.0+. The v0.21.14.0 piece is purely local — it ships in the laptop binary and takes effect immediately. So once this PR lands and the laptop upgrades, the dispatch will already pin
--remote-cwdand drop(fallback); the richer remote error follows aftercanopy upgrade --on <host>.🤖 Generated with Claude Code