Delete Campfire lines through their Campfire, and explain Ping refusals
What was wrong
bc3#14171 makes Basecamp's generic recording routes answer an empty 403 Forbidden to any write on a recording in a Ping: trash, archive, unarchive, client visibility, title, publication, others' subscriptions, spotlights. Subscribing or unsubscribing yourself, bubbling up, boosts and the Campfire line endpoints still work.
The TUI trashed whatever its Detail view, its :trash palette action and its Hey view held through Recordings().Trash. A Campfire line reaches Detail from a search hit (bc3's search returns lines, Ping lines included), from recents, a quick jump, or a basecamp tui deep link to a Ping line. Pressing t twice on one was:
- a 403 in a Ping, once bc3#14171 deploys;
- in a project Campfire, a trash that skipped the checks
DELETE /chats/:chat/lines/:idmakes on who may delete a line and until when. That works today and still will after the deploy.
The Hey view named in review can't hold a line on main: its feed is Recordings().List of messages, to-dos and documents only. It now decides by type like the others anyway.
The CLI commands that write through the gated routes would report bc3's bare 403 as "access denied", with no hint.
What changed
TUI (dev builds only):
Hub.RemoveRecordingdecides by the API's recording type. It deletes aChat::Lines::*recording withCampfires().DeleteLineand trashes everything else.- It writes nothing when it isn't given a type.
- It reads a line's Campfire from the line when the caller doesn't have it.
- Detail reads the type and parent apart from the fields it displays, and passes them to
RemoveRecording.- Its confirmation reads "Press t again to delete this line for good", the result says "Deleted", and the help shows
t delete.
- Its confirmation reads "Press t again to delete this line for good", the result says "Deleted", and the help shows
:trashhas only the scope's label for the type, so it reads the recording first, through its bucket (bc3's bucket-less/recordings/<id>.jsonanswers 404 for a card).- It refuses a line and says to press
ttwice in the line's view, where deletion is confirmed. - It trashes anything else as before.
- It refuses a line and says to press
- Hey passes its entries' API type to
RemoveRecording.
CLI: pingWriteRefusal follows the clientsForbiddenError pattern. On a 403 from a gated route it reads the recording back once. If its bucket is a Circle, the error says Basecamp doesn't change a Ping that way and names the command that works:
- for a line,
basecamp chat delete <line> --room <campfire> --in <ping>, built withdeleteLineCmdso it carries--forceonly for machine output; - for the Ping's Campfire,
subscriptions subscribeorsubscriptions unsubscribe.
Any other 403, or a recording it can't read, converts as before. It's wired into:
recordings trash,archive,restoreandvisibility;- the typed nouns'
trash,archiveandrestore; subscriptions addandremove;spotlightandunspotlight.
Skill: one bullet in the Pings section of skills/basecamp/SKILL.md saying which commands a Ping refuses.
Sweep
Changed: the paths above.
Left as they are:
- TUI views of typed project items (messages, cards, docs and files, schedule, assignments, to-do lists) still use
Hub.TrashRecording. Their items are never lines, and they live in a project. - Detail's subscribe (
s) and boost (b) subscribe or unsubscribe you, or boost, which a Ping allows. Lines carry no comments, and the comment routes aren't gated. recordings trash <line in a project Campfire>still trashes the line through the generic route, which bc3 keeps serving outside Pings.- The command promises a recoverable trash. Turning it into a permanent delete would surprise a script, and refusing it would take away a route the API serves.
- Whether the CLI should close that route for project lines too is a call for a person.
chat update,chat delete,pings *,boost,subscriptions subscribeandunsubscribeuse the line, Ping, boost and self-subscription endpoints, which bc3#14171 leaves open.basecamp mcpsends what the agent asks to the API, from the basecamp/mcp catalog, and returns bc3's 403 as it comes.tools show,update,trash,enable,disableandrepositionon a Ping's Campfire now get a 404 or 403 (the same bc3 PR changesDocks::ToolsController). Reaching that means passing a Ping's Campfire ID to a dock-tool command, so the plain error stands.- The connector (
basecamp connect) only boosts lines and reads subscriptions. files moveonly moves items in a project's Docs & Files.
Tests
These failed on main first:
- Detail: a line opened under the API's type, the deep-link label
Campfire::Line, or no label (quick jump) is confirmed as a delete and sends onlyDELETE /chats/222/lines/111. That holds even when a field Detail displays won't parse. :trash: on the same three labels it sends no write and returns the line refusal. It writes nothing when it can't read the recording.- CLI: each of the nine commands above, given a 403 and a Ping line or a Ping's Campfire on the read-back, returns the explained refusal.
- The JSON-mode hint passes the
chat deleteconfirmation guard it would hit. - The styled hint leaves
--forceoff. - Run under
--profileand--account, every hint names both.
- The JSON-mode hint passes the
These pin behavior that main already had:
- Detail,
:trashand Hey still trash a non-line. - A 403 on a project recording, a project line or an unreadable recording keeps the plain error.
Hub.RemoveRecording has unit tests for each branch, including that it writes nothing without a type or without a line's Campfire.
bin/ci passes on the head.
Release
The TUI is built only with -tags dev; release builds carry a stub tui command, so that half ships in no released binary. The CLI half only changes the text of errors bc3 would return anyway. Nothing needs to be released before bc3#14171 deploys. The next routine release can carry this.