Skip to content

Delete Campfire lines through their Campfire, and explain Ping refusals - #970

Merged
jeremy merged 4 commits into
mainfrom
campfire-line-delete
Oct 27, 2026
Merged

jeremy merged 4 commits into
mainfrom
campfire-line-delete

Conversation

@jeremy

@jeremy jeremy commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

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/:id makes 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.RemoveRecording decides by the API's recording type. It deletes a Chat::Lines::* recording with Campfires().DeleteLine and 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.
  • :trash has only the scope's label for the type, so it reads the recording first, through its bucket (bc3's bucket-less /recordings/<id>.json answers 404 for a card).
    • It refuses a line and says to press t twice in the line's view, where deletion is confirmed.
    • It trashes anything else as before.
  • 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 with deleteLineCmd so it carries --force only for machine output;
  • for the Ping's Campfire, subscriptions subscribe or subscriptions unsubscribe.

Any other 403, or a recording it can't read, converts as before. It's wired into:

  • recordings trash, archive, restore and visibility;
  • the typed nouns' trash, archive and restore;
  • subscriptions add and remove;
  • spotlight and unspotlight.

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 subscribe and unsubscribe use the line, Ping, boost and self-subscription endpoints, which bc3#14171 leaves open.
  • basecamp mcp sends 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, disable and reposition on a Ping's Campfire now get a 404 or 403 (the same bc3 PR changes Docks::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 move only 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 only DELETE /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 delete confirmation guard it would hit.
    • The styled hint leaves --force off.
    • Run under --profile and --account, every hint names both.

These pin behavior that main already had:

  • Detail, :trash and 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.

bc3#14171 makes the generic recording routes answer 403 to any write on a
recording in a Ping. The TUI trashed whatever its Detail view, the :trash
action and the Hey view held through the generic trash route, so a Campfire
line opened from search went there too: in a Ping that is now a 403, and in
a project Campfire it moved the line to the trash past the checks the line
endpoint makes on who may delete a line and until when.

Those three now go through Hub.RemoveRecording, which decides by the API's
own recording type: a Chat::Lines::* recording is deleted through
Campfires().DeleteLine, anything else is trashed as before. Detail passes the
type and parent it loaded; :trash has only the scope's label, so the hub reads
the recording first, and writes nothing when it can't tell. The confirmation
and status say delete for a line, since that can't be undone.

The CLI commands that send a write to one of the gated routes (recordings
trash, archive, restore and visibility, the typed nouns' trash, archive and
restore, subscriptions add and remove, spotlight and unspotlight) explain the
refusal: bc3's 403 is empty, so on that status the recording is read back
once, and when it is in a Ping the error says so and names the command that
works, chat delete for a line or subscriptions subscribe/unsubscribe for the
Ping's Campfire.
…eader

The palette's :trash deleted a Campfire line outright, with none of the
confirmation t gives in the line's view, though deleting a line can't be
undone. It now reads the recording, and for a line says to press t twice
there instead, writing nothing; anything else it trashes as before.

The Ping-line refusal named chat delete without --force, which chat delete
refuses wherever no confirmation can be shown, so an agent reading JSON could
not run it. It now builds the command with deleteLineCmd, which adds --force
for machine output and leaves it off for a person who will be asked.
Detail read a recording's type with the rest of its fields, so one field it
couldn't parse, such as a timestamp, left the type unknown: t then asked to
trash, and RemoveRecording, reading the type itself, deleted the line for
good. Detail now reads the type and parent on their own, and RemoveRecording
takes only a type the caller has, writing nothing without one, since the
caller's confirmation can't have said which would happen. Its read is left
for the one thing a caller may lack, a line's Campfire.
@jeremy
jeremy requested a review from a team as a code owner October 27, 2026 15:15
Copilot AI balanced review requested due to automatic review settings October 27, 2026 15:15
@github-actions github-actions Bot added commands CLI command implementations tui Terminal UI tests Tests (unit and e2e) skills Agent skills labels Oct 27, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T15:26:35.687171Z 805c2d8 New commits
🔒 Security Review ✅ Completed 2026-10-27T15:27:48.158672Z 805c2d8 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a92a24f921

ℹ️ 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".

Comment thread internal/tui/workspace/actions.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Recovery hints omit the originating profile and account, so copied commands can run under different credentials or account scope.

1 open issue
What changed in this PR

This PR routes Campfire line deletion through the Campfire API in the dev-only TUI and adds actionable CLI errors for writes that Pings refuse.

Changes:

  • Distinguishes permanent line deletion from recoverable trash, with appropriate confirmation and palette safeguards.
  • Explains Ping write refusals and suggests supported commands.
  • Adds routing and error-handling tests, plus Ping restrictions in the skill documentation.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
skills/​basecamp/​SKILL.md Documents Ping write restrictions.
internal/​tui/​workspace/​views/​hey.go Uses type-aware removal and confirmation.
internal/​tui/​workspace/​views/​hey_test.go Tests trash and line-deletion routing.
internal/​tui/​workspace/​views/​detail.go Tracks API type and parent for confirmed deletion.
internal/​tui/​workspace/​views/​detail_test.go Tests line labels, malformed display fields, and ordinary trash.
internal/​tui/​workspace/​data/​remove_recording_test.go Tests removal routing and fail-safe behavior.
internal/​tui/​workspace/​data/​hub.go Adds type-aware removal and recording metadata lookup.
internal/​tui/​workspace/​actions.go Refuses palette trash for Campfire lines.
internal/​tui/​workspace/​actions_test.go Tests palette safeguards and ordinary trash.
internal/​commands/​subscriptions.go Explains refused Ping subscription updates.
internal/​commands/​spotlight.go Explains refused Ping spotlight changes.
internal/​commands/​recordings.go Adds shared Ping refusal detection and recovery hints.
internal/​commands/​recordings_test.go Tests refusal hints, output modes, and fallback errors.

Review effort: Balanced

Comment thread internal/commands/recordings.go
The TUI read a recording's type from the bucket-less /recordings/<id>.json,
which bc3 answers with a 404 for some types, such as Kanban::Card, so :trash
on a card stopped trashing it. RecordingKind and RemoveRecording now take the
project and read /buckets/<project>/recordings/<id>.json, as Detail does.

The commands a Ping refusal names now carry the profile and account the
refused command ran under, read before it resolved its account, as the
spotlight breadcrumbs do, so pasting one into a fresh shell acts on the same
account.
Copilot AI balanced review requested due to automatic review settings October 27, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approved

Static review found no blocking issues, with regression coverage for deletion safeguards, scoped recovery hints, and preserved fallback behavior.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@jeremy

jeremy commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Review threads: 2 resolved (2 fixed, 0 declined).

On 805c2d8, CI is green, Copilot has approved, and Codex's code and security passes completed with no findings. The cli team review request is still open.

@jeremy
jeremy merged commit 0dc1d47 into main Oct 27, 2026
32 checks passed
@jeremy
jeremy deleted the campfire-line-delete branch October 27, 2026 16:17
robzolkos added a commit that referenced this pull request Oct 28, 2026
* origin/main:
  Fail when Basecamp drops a requested assignee (#981)
  Point the skill at config show for project context (#980)
  Emit "data": [] for a list that found nothing (#979)
  Delete Campfire lines through their Campfire, and explain Ping refusals (#970)
@robzolkos robzolkos mentioned this pull request Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands CLI command implementations skills Agent skills tests Tests (unit and e2e) tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants