Skip to content

feat: bind your own shell commands to keys on each screen - #39

Merged
JacobAtchley merged 18 commits into
mainfrom
feat/custom-commands
Oct 6, 2026
Merged

JacobAtchley merged 18 commits into
mainfrom
feat/custom-commands

Conversation

@JacobAtchley

@JacobAtchley JacobAtchley commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

What

The config gains a commands list. Each entry binds a single key, on a kind of screen (pullRequest, workItem or build), to a shell command of your own:

"commands": [
  { "on": "pullRequest", "key": "ctrl+r", "name": "review with claude", "run": "~/bin/pr-review.sh" }
]

On that screen, pressing the key runs the command detached, with what the screen shows passed in BOARDWALK_* environment variables (ID, TITLE, URL, REPO, SOURCE_BRANCH, …). The status line reports ran, failed: <last stderr line>, or started if it is still going after two seconds. Commands show in the ? panel and the command palette.

The motivating example (in the README): on a pull request, ctrl+r opens a herdr tab in the current workspace and starts a Claude Code review of that PR.

How

  • config: Command, the three kinds, ValidateCommands.
  • ui/custom.go: the runner. It uses sh -c (cmd /S /C on Windows) in its own process group. stdin is /dev/null and stdout is dropped. stderr goes to an unlinked temp file rather than a pipe, so a command outlives boardwalk without dying of SIGPIPE, and a backgrounded child cannot hold up the result.
  • ui/subject.go: eight views gain Subject(): the PR list, detail, diff and file views; the work item list and item; the build list and logs.
  • ui/rootcommands.go: Root matches the key after the prompt, filter and armed-vote guards. A screen's own key always wins. The result goes to the view the command ran on, wherever it now sits in the stack. Startup refuses keys that cannot work: unparseable, reserved, list or viewport movement, ones every screen uses, and the palette key.
  • Logs, PR detail and work item detail now show a StatusMsg error in red without marking the discussion as failed to load.

Values reach commands only as environment variables, never spliced into the command string, so a PR title full of shell syntax stays a title.

Design: docs/superpowers/specs/2026-10-06-custom-commands-design.md. Plan: docs/superpowers/plans/2026-10-06-custom-commands.md.

Testing

  • go test ./..., go vet ./... and GOOS=windows go build ./... all pass.
  • The runner is tested against a real sh. Tests cover the environment, failure output, a 10,000-line stderr flood, detaching from a long command, a background child holding stderr, and shell syntax in a value staying inert.
  • Root tests cover dispatch, kind matching, built-in keys winning, an armed vote blocking the command, a result for a view no longer on top, and the help panel and palette.
  • Startup check confirmed by hand: a config binding q exits at once with an error naming the file.
  • Not yet done: pressing the key in a live TUI, and running anything on Windows (it is only compile-checked).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq

JacobAtchley and others added 18 commits October 6, 2026 16:36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Use CmdLine directly to avoid Go's exec escaping, which cmd.exe doesn't
understand. Build the command line ourselves with proper cmd quoting rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Logs, PullRequestDetail, and Item now properly handle StatusMsg with error colour:
- Logs adds the StatusMsg case to record status and failed state
- PullRequestDetail and Item track errStatus separately from failed, so error
  colour follows the status message without claiming the discussion failed
- All three views return the error flag from Status() when displaying an error
  status message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
A pipe's read end died with boardwalk, so a running command got SIGPIPE
on its next write, and a backgrounded child held Wait open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jn8iZy7cV652kj9axpyrUq
@JacobAtchley
JacobAtchley merged commit 8dd3e86 into main Oct 6, 2026
3 checks passed
@JacobAtchley
JacobAtchley deleted the feat/custom-commands branch October 6, 2026 22:44
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.

1 participant