Skip to content

Add pings commands: list, show, start, messages, post, upload - #913

Draft
robzolkos wants to merge 5 commits into
mainfrom
pings-commands
Draft

robzolkos wants to merge 5 commits into
mainfrom
pings-commands

Conversation

@robzolkos

@robzolkos robzolkos commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

Makes Pings usable from the CLI. Until now a ping could only be found through search, chat show and show <ping url> failed on one, chat line refused /circles/{id}@{line} URLs, and the TUI Pings view was always empty (it filtered the account Campfire listing, which never returns a ping's Campfire).

"Ping" is the name everywhere a person reads it. The wire path is /circles and the bucket type is Circle; that shows up only in the API docs and the agent notes.

Commands

basecamp pings list
basecamp pings show <ping>
basecamp pings start <person>... [--new-group]     (alias: with)
basecamp pings messages <ping> [-n N]
basecamp pings post <ping> [message] [--attach FILE] [--content-type T]
basecamp pings upload <ping> <file>

A <ping> is a ping id, a ping URL (https://app.basecamp.com/{account}/circles/{id}, 3.basecamp.com and the @{line} form too), or a person (name, email or person id), meaning your direct ping with them. A bare number is always a ping id. For a person, show and messages read the direct ping from pings list and never start one; post and upload start it if missing.

pings start relies on the API reusing: one direct ping per person, one group per set of people unless --new-group. So pings start Jason is also "open my ping with Jason". People are resolved among the people you can ping (people pingable), and people Basecamp drops from the ping are reported. 403 Pings have been disabled and the agent 422 get plain messages.

messages, post and upload run through chat's own code on the ping's Campfire. post, messages and upload in chat.go now take a resolved Campfire, the commands that continue from it (so breadcrumbs say pings ...), and a mention resolver. @mentions in a ping post resolve among the ping's people only.

Also:

  • show <ping url> shows the ping; show and chat line|update|delete take circles/{id}@{line} message URLs.
  • A bucket with no dock that is a ping resolves to its Campfire, so chat messages --in <ping> works.
  • chat list --all and its help say pings are not in the list and point to pings list.
  • TUI: PingRooms is built from Pings().List; rooms carry a Title (pings can be groups) and the wording no longer says 1:1.
  • Skill: new Pings section and triggers. The skill had no ping workflow before.
  • .surface, API-COVERAGE.md, README, smoke coverage and a small e2e/pings.bats.

Not done

pings line, pings update and pings delete are not added. Messages in a ping are edited and deleted with chat update and chat delete on the message URL, which now work for ping URLs, so a second spelling would only duplicate chat's confirmation and stdin handling.

Dependencies

  • basecamp/bc3#13943 puts GET /circles.json on the API host. Until it deploys, pings list returns an empty list in production; show and messages by id or URL already work there (read-only checks; post and start were not run against production).
  • Add the Pings service and find a Ping's chat-line Campfire basecamp-sdk#1015 adds Pings(). This branch builds against it through a replace, in its own first commit, "DO NOT MERGE: build against unreleased SDK pings branch". Once the SDK releases, drop that commit and run make bump-sdk. That commit also adapts lineup.go to the SDK 0.25.0 return values of CreateMarker and UpdateMarker; the open dependabot bump to 0.25.0 (deps: bump github.com/basecamp/basecamp-sdk/go from 0.24.0 to 0.25.0 in the go-dependencies group #911) needs the same change.
  • The mcpserver composite test gains a /circles route: with the SDK change, chat-line discovery probes a bucket as a ping before falling back to the account listing.

Verification

  • Unit tests with httptest fakes: URL parsing (both hosts, local dev, @line), people resolution, direct reuse, group and --new-group, dropped people, 403 and 422, mention resolution among the ping's people, read-by-person never creating, show and chat line on ping URLs, the chat dock fallback, and the TUI room fetch.
  • End to end against bc3 dev with the built binary, as two users: list, start (direct twice returns the same id; group; --new-group), post by id, URL and person, messages, show on app and web URLs, chat line on a message URL, upload, chat delete on a ping line, and the second user sees the lines.
  • Production, read-only: pings show and pings messages of a real ping, show on its URL; pings list is empty until #13943 deploys.
  • bin/ci: every gate passes except lint with the local golangci-lint 2.15.0, which flags internal/connector/admission/policy.go:415 (cap shadows a predeclared identifier). That file is untouched here and CI pins 2.11.1. The SDK provenance check is skipped while the replace is active.

🤖 Generated with Claude Code


Summary by cubic

Adds a pings command group — list, show, start, messages, post, and upload — for private conversations outside projects, so pings are reachable from the CLI instead of only through search. A <ping> is a ping id, a ping URL, or a person's name; start reuses the API's existing direct or group ping (unless --new-group), and post/upload start one if missing. This also fixes show and chat line failing on ping URLs, and the TUI Pings view being always empty because it filtered the account Campfire listing, which never returns a ping's Campfire.

  • show and chat line now take ping URLs (circles/{id}, circles/{id}@{line}, and the my/sidebar link form); chat messages --in <ping> works through a dock fallback for ping buckets.
  • chat list --all no longer implies pings are project chats and points to pings list.
  • @mentions in a ping post resolve among the ping's people only.
  • TUI Pings view now builds rooms from the Pings API; rooms carry a Title since pings can be groups.
  • pings line/update/delete are intentionally not added — edit and delete ping lines with chat update/chat delete on the message URL, which now work.
  • Skill, README, API coverage, .surface, and smoke tests updated.

Dependencies

Written for commit 22891ab. Summary will update on new commits.

View guided diff Turn on auto-fix

robzolkos and others added 4 commits October 19, 2026 11:50
Temporary replace onto basecamp-sdk circles-service (9f265cd), which sits
on SDK 0.25.0. Also adapts lineup.go to the 0.25.0 Create/UpdateMarker
return values. Drop this commit and run 'make bump-sdk' once the SDK
releases the Pings service.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A Ping's web URL is circles/{ping} or circles/{ping}@{line}. The router
sees the @ form as a type named after the whole segment, so urlarg now
rewrites it the way it does chats/{room}@{line}: a Ping is its own
bucket, so the form parses as a chat line in that bucket, with the Ping
in the new PingID field. The Campfire is not in the URL; it comes from
the Ping.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Pings view built its rooms from the account Campfire listing, which
never returns a ping's Campfire, so it was always empty. Build them from
Pings().List instead, with each ping's Campfire from its room URL; the
ping is its own bucket, so it is the room's project. Pings can be groups,
so the room carries a Title rather than a person's name, and the wording
no longer calls them 1:1 threads.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
basecamp pings list, show, start (alias with), messages, post and upload
cover a Ping: a private conversation, direct or group, held in a Circle
bucket with one Campfire. A <ping> is an id, a ping URL, or a person; a
bare number is always a ping id. start opens or reuses (the API returns
the existing ping), reporting people Basecamp left out and the 403 and
422 refusals. messages, post and upload run through chat's own code on
the ping's Campfire: chat's post, messages and upload now take a resolved
Campfire, the commands that continue from it, and a mention resolver, so
@mentions in a ping resolve among its people.

show and chat line take ping URLs (circles/{id}, circles/{id}@{line}); a
bucket with no dock that is a Ping now resolves to its Campfire, so
chat messages --in <ping> works; chat list --all says pings are not in
it; and the skill, README, API coverage, surface and smoke coverage
follow.

The mcpserver composite test gains the /circles probe the SDK now makes
for a bucket without a dock.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 19, 2026 15:50
@github-actions github-actions Bot added commands CLI command implementations tui Terminal UI tests Tests (unit and e2e) skills Agent skills output Output formatting and presentation docs deps labels Oct 19, 2026

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

The machine-local SDK dependency blocks portable builds, and unresolved validation, routing, and input-handling issues remain.

7 open issues
What changed in this PR

Adds private Pings conversations to the CLI and connects the TUI Pings view to the dedicated API.

Changes:

  • Adds six pings commands with person resolution and ping-scoped mentions.
  • Supports ping URLs through existing show/chat workflows and updates TUI discovery.
  • Adds documentation, command registration, and test coverage.

[!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 workflows and triggers.
README.md Adds a ping example.
internal/​urlarg/​urlarg.go Parses ping and message URLs.
internal/​urlarg/​urlarg_ping_test.go Tests ping URL parsing.
internal/​tui/​workspace/​views/​sort_test.go Updates room-title assertions.
internal/​tui/​workspace/​views/​pings.go Displays direct and group ping titles.
internal/​tui/​workspace/​views/​bonfire_sidebar.go Uses ping titles in the sidebar.
internal/​tui/​workspace/​data/​types.go Updates ping room metadata.
internal/​tui/​workspace/​data/​ping_rooms_test.go Tests API-backed ping discovery.
internal/​tui/​workspace/​data/​hub_global.go Fetches pings and recent messages.
internal/​tui/​workspace/​actions.go Updates navigation wording.
internal/​tui/​empty/​messages.go Adds ping empty-state guidance.
internal/​presenter/​schemas/​ping.yaml Defines ping presentation.
internal/​names/​resolver.go Resolves pingable people.
internal/​names/​ping_people_test.go Tests people resolution.
internal/​names/​mention.go Supports fixed-set mention resolution.
internal/​mcpserver/​composite_test.go Adds a ping discovery fixture.
internal/​commands/​show.go Routes ping URLs to ping details.
internal/​commands/​show_test.go Tests ping message routing.
internal/​commands/​pings.go Implements six ping commands.
internal/​commands/​pings_test.go Tests ping command behavior.
internal/​commands/​lineup.go Adapts SDK return signatures.
internal/​commands/​helpers.go Shares mention lookup handling.
internal/​commands/​commands.go Registers ping catalog entries.
internal/​commands/​commands_test.go Includes pings in registration tests.
internal/​commands/​chat.go Shares chat operations and resolves ping buckets.
internal/​commands/​chat_test.go Checks ping discovery guidance.
internal/​cli/​root.go Registers the pings command.
internal/​cli/​help.go Includes pings in core help.
go.sum Removes released SDK checksums.
go.mod Adds a local SDK replacement.
e2e/​smoke/​smoke_pings.bats Adds read-only ping smoke checks.
e2e/​smoke/​smoke_aliases.bats Accounts for ping aliases.
e2e/​pings.bats Tests help and input validation.
API-COVERAGE.md Records Pings endpoint coverage.
.surface Updates command and flag snapshots.

Review effort: Balanced

Comment thread go.mod
modernc.org/memory v1.12.1 // indirect
)

replace github.com/basecamp/basecamp-sdk/go => /home/rzolkos/Work/basecamp/basecamp-sdk-circles/go
Comment thread internal/commands/chat.go
Comment on lines +1322 to +1325
id, err := getDockToolID(cmd.Context(), app, projectID, "chat", "", "chat room", "room")
if err != nil {
if campfireID, ok := pingCampfireID(cmd.Context(), app, projectID, err); ok {
return campfireID, nil
Comment thread internal/commands/chat.go
Comment on lines +1344 to +1346
ping, err := app.Account().Pings().Get(ctx, id)
if err != nil {
return "", false
if err != nil {
return err
}
ping, err := startPing(cmd.Context(), app, people, newGroup)
Comment on lines +305 to +307
messageContent, err := readPostInput(cmd, args[1:], 1, content, *contentType, attachFiles, func() error { return nil })
if err != nil || messageContent == "" {
return err
Comment on lines +453 to +455
if parsed.AccountID != "" && app.Config.AccountID != "" && parsed.AccountID != app.Config.AccountID {
return pingTarget{}, output.ErrUsage(fmt.Sprintf("URL account %s does not match the configured account %s", parsed.AccountID, app.Config.AccountID))
}
Comment on lines +597 to +600
if strings.ContainsAny(s, " \t\"'") {
return strconv.Quote(s)
}
return s
Basecamp also links a ping as {account}/my/sidebar/circles/{id}, with
circles/{id}@{line} under the same prefix. Drop the my/sidebar prefix
before routing so pings, show and chat parse it like the plain form.
Copilot AI balanced review requested due to automatic review settings October 19, 2026 21:08

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.

🔵 Needs a closer look

Numeric target handling can select an unintended recipient, and ping routing bypasses recording fragments.

7 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Zero target incorrectly resolves to sole person

internal/​commands/​pings.go:477

A numeric target of 0 falls through to person resolution with an empty name. The name resolver matches an empty string against everyone, so an account with exactly one pingable person resolves that person. Consequently, pings post 0 "message" can start their ping and send the message to an unintended recipient. Distinguish numeric targets using the empty person field so zero remains an ID, rather than becoming a person lookup. Add a regression test with one pingable person that verifies no ping or message is created.

Medium severity Preserve recording fragments in ping URL dispatch

internal/​commands/​show.go:70

This early return bypasses the recording-fragment handling immediately below it. For example, show https://app.basecamp.com/123/circles/456#__recording_111 shows ping 456 instead of recording 111, contrary to the command's documented fragment behavior. Dispatch directly to showPingURL only when no recording fragment is present, and add a regression test for this URL form.

Review effort: Balanced

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands CLI command implementations deps docs output Output formatting and presentation 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