Skip to content

Port over basecamp cli improvements - #52

Merged
monorkin merged 4 commits into
mainfrom
port-over-basecamp-cli-improvements
Mar 31, 2026
Merged

Port over basecamp cli improvements#52
monorkin merged 4 commits into
mainfrom
port-over-basecamp-cli-improvements

Conversation

@monorkin

Copy link
Copy Markdown
Collaborator

No description provided.

monorkin and others added 4 commits March 31, 2026 14:48
- Enhanced User-Agent: include repo URL and SDK version for API observability
- Added debug.ReadBuildInfo() fallback so go-install builds report real version
- Linting: add contextcheck, goimports formatter, additional revive rules
- Release script: colored output, --dry-run flag, jq check, existing-tag handling
- Added .pre-commit-config.yaml with golangci-lint, gitleaks, and standard hooks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Cobra's default flat help with categorized, styled output:
- Root help groups commands into EMAIL, CALENDAR & TASKS, AUTH & CONFIG
- Subcommand help shows styled sections with curated inherited flags
- Parent-scoped flags promoted into FLAGS, not buried in globals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 15:49
@github-actions github-actions Bot added the enhancement New feature or request label Mar 31, 2026
@monorkin
monorkin merged commit 3873a1a into main Mar 31, 2026
27 of 28 checks passed
@monorkin
monorkin deleted the port-over-basecamp-cli-improvements branch March 31, 2026 15:53

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.

Pull request overview

Ports CLI/TUI improvements inspired by Basecamp’s hey-cli, including a redesigned TUI navigation model, richer help output, and improved release/version metadata.

Changes:

  • Replaced the Bubbletea “view-state + bubbles/list” TUI with a 2-row navigation header, help bar, and custom content lists/viewport threads.
  • Added custom Cobra help rendering with curated root categories and consistent command help layout.
  • Improved release tooling and version/user-agent metadata; upgraded hey-sdk dependency and linting/pre-commit config.

Reviewed changes

Copilot reviewed 28 out of 29 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
scripts/release.sh Adds validation, dry-run mode, tag handling, and nicer output for release tagging/push flow.
internal/version/version.go Reads module build info for dev builds; expands user-agent; adds IsDev().
internal/tui/tui.go Major TUI architecture rewrite: sections/focus rows, viewport thread rendering, async loading/spinner, posting actions.
internal/tui/tui_test.go Updates tests to match new TUI model (focus cycling, double-ctrl+c quit, ordering, lists, journal dates).
internal/tui/styles.go Adds loading/error views + animation helpers; updates styling to ANSI-adaptive colors.
internal/tui/nav.go New navigation model and header rendering (sections/subnav, box ordering/shortcuts).
internal/tui/help.go New bottom help bar with dynamic key bindings and wrapping.
internal/tui/content.go New custom mail/calendar list renderers (cursor, scrolling, formatting).
internal/tui/topic.go Removes prior topic sub-model (replaced by shared viewport/thread rendering in tui.go).
internal/tui/detail.go Removes prior detail sub-model (replaced by shared viewport/thread rendering in tui.go).
internal/tui/boxes.go Removes legacy bubbles/list mailboxes model (superseded by new nav + content list).
internal/tui/box.go Removes legacy bubbles/list box postings model (superseded by contentList).
internal/tui/calendars.go Removes legacy bubbles/list calendars model (superseded by new nav + recording list).
internal/tui/calendar.go Removes legacy bubbles/list calendar recordings model (superseded by recordingList).
internal/tui/journal.go Removes legacy bubbles/list journal model (superseded by journal date subnav + viewport).
internal/tui/journal_test.go Removes tests for legacy journal list behavior (no longer applicable).
internal/tui/calendar_test.go Removes tests for legacy calendar list ordering behavior (no longer applicable).
internal/tui/debug_test.go Removes debug-oriented tests (no longer applicable after input refactor).
internal/models/box.go Expands Posting model to include fields used by new list UI (name, counts, extensions).
internal/cmd/root.go Switches to the new custom help renderer.
internal/cmd/help.go Adds custom help renderer (root curated sections + styled per-command help).
internal/cmd/writeok_test.go Updates expectations for the new help output format.
internal/cmd/sdk.go Appends SDK default UA to CLI UA; minor import ordering.
internal/client/client.go Appends SDK default UA to CLI UA for legacy client requests too.
internal/cmd/formatting.go Import grouping tweak.
go.mod Bumps github.com/basecamp/hey-sdk/go to v0.2.0; removes unused indirect deps.
go.sum Updates sums for hey-sdk bump and dependency pruning.
.pre-commit-config.yaml Adds pre-commit hooks (lint, leaks, tidy, short tests).
.golangci.yml Enables gofmt/goimports formatting and additional linters/rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/release.sh
Comment on lines +99 to +105
if git rev-parse "$VERSION" >/dev/null 2>&1; then
EXISTING_SHA=$(git rev-parse "${VERSION}^{commit}")
if [[ "$EXISTING_SHA" == "$LOCAL" ]]; then
info "Tag $VERSION already exists at HEAD"
else
die "Tag $VERSION already exists at ${EXISTING_SHA:0:7} (not HEAD). Delete it first or choose a different version."
fi

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

git rev-parse "$VERSION" can succeed for non-tag refs (e.g., a branch named v1.2.3), which could make the script think a tag exists when it doesn’t. Prefer checking specifically for a tag ref (e.g., verify refs/tags/$VERSION) before treating it as an existing tag.

Copilot uses AI. Check for mistakes.
Comment thread scripts/release.sh
Comment on lines +72 to 76
# Verify required tools
if ! command -v jq >/dev/null 2>&1; then
die "jq is required but not found. Install with your package manager."
fi

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

This script doesn’t appear to use jq anywhere in the updated content, so hard-failing when jq is missing adds an unnecessary dependency for releases. Either remove this check or introduce the jq usage that requires it (and keep the check).

Suggested change
# Verify required tools
if ! command -v jq >/dev/null 2>&1; then
die "jq is required but not found. Install with your package manager."
fi

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/content.go
Comment on lines +136 to +142
dateWidth := len(date)
prefixWidth := 4 // "│ ● " or " "
subjectWidth := max(c.width-prefixWidth-dateWidth-2, 10) // 2 for gap
if len(subject) > subjectWidth {
subject = subject[:subjectWidth-3] + "..."
}
gap := max(c.width-prefixWidth-len(subject)-dateWidth, 1)

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

Truncating subject via byte slicing can split multi-byte UTF-8 runes, producing invalid text and broken rendering in terminals. Use a rune-aware truncation approach (e.g., convert to []rune or truncate by display width with lipgloss.Width/runewidth) and then append ....

Suggested change
dateWidth := len(date)
prefixWidth := 4 // "│ ● " or " "
subjectWidth := max(c.width-prefixWidth-dateWidth-2, 10) // 2 for gap
if len(subject) > subjectWidth {
subject = subject[:subjectWidth-3] + "..."
}
gap := max(c.width-prefixWidth-len(subject)-dateWidth, 1)
dateWidth := lipgloss.Width(date)
prefixWidth := 4 // "│ ● " or " "
subjectWidth := max(c.width-prefixWidth-dateWidth-2, 10) // 2 for gap
// Truncate subject by display width, avoiding splitting UTF-8 runes.
if lipgloss.Width(subject) > subjectWidth {
var b strings.Builder
currentWidth := 0
ellipsisWidth := lipgloss.Width("...")
for _, r := range subject {
rw := lipgloss.Width(string(r))
if currentWidth+rw+ellipsisWidth > subjectWidth {
break
}
b.WriteRune(r)
currentWidth += rw
}
subject = b.String() + "..."
}
gap := max(c.width-prefixWidth-lipgloss.Width(subject)-dateWidth, 1)

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/content.go
Comment on lines +298 to +300
if len(title) > subjectWidth {
title = title[:subjectWidth-3] + "..."
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

Same UTF-8 issue as the posting subject truncation: byte slicing can split runes and corrupt the output. Switch to rune-aware/display-width-aware truncation to keep rendering safe for non-ASCII titles.

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/tui.go
Comment on lines +271 to +283
func (m model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
key := msg.String()

// Double Ctrl+C to quit
if key == "ctrl+c" {
if m.ctrlCOnce {
m.cancel()
return m, tea.Quit
}
m.ctrlCOnce = true
m.updateHelpBindings()
return m, tea.Tick(2*time.Second, func(time.Time) tea.Msg { return ctrlCResetMsg{} })
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

When m.inThread is true, most key presses never reach m.topicViewport.Update, so viewport-native controls (e.g., pgup/pgdn, home/end, etc.) won’t work, and keys like tab can still change focus rows even though the help text implies a “thread-only” control set. Consider short-circuiting in handleKey: if m.inThread and the key isn’t one of the global controls (ctrl+c, esc/q), pass the key msg directly to m.topicViewport.Update and return that cmd.

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/tui.go
}
return m, nil
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

When m.inThread is true, most key presses never reach m.topicViewport.Update, so viewport-native controls (e.g., pgup/pgdn, home/end, etc.) won’t work, and keys like tab can still change focus rows even though the help text implies a “thread-only” control set. Consider short-circuiting in handleKey: if m.inThread and the key isn’t one of the global controls (ctrl+c, esc/q), pass the key msg directly to m.topicViewport.Update and return that cmd.

Suggested change
// In thread view, delegate all other keys to the topic viewport so that
// viewport-native controls (pgup/pgdn, home/end, etc.) work as expected
// and outer-level focus/section shortcuts are disabled.
if m.inThread {
var cmd tea.Cmd
m.topicViewport, cmd = m.topicViewport.Update(msg)
return m, cmd
}

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/tui.go
Comment on lines +741 to +749
func generateJournalDates(n int) []string {
dates := make([]string, n)
today := time.Now()
for i := range n {
d := today.AddDate(0, 0, -(n - 1 - i))
dates[i] = d.Format("2006-01-02")
}
return dates
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

for i := range n relies on Go 1.22+ “range over integers”. If this repo’s go directive targets < 1.22, this won’t compile. If older Go versions must be supported, replace with a classic indexed loop (for i := 0; i < n; i++ { ... }).

Copilot uses AI. Check for mistakes.
Comment thread internal/tui/tui_test.go
Comment on lines +290 to +292
today := time.Now().Format("2006-01-02")
if dates[6] != today {
t.Errorf("last date = %q, want today %q", dates[6], today)

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

This test can flake around midnight because generateJournalDates and time.Now() are called at different times. A simple stabilization is to capture now := time.Now() once in the test and compare against that, or (more robustly) refactor generateJournalDates to accept a “base time” for deterministic testing.

Suggested change
today := time.Now().Format("2006-01-02")
if dates[6] != today {
t.Errorf("last date = %q, want today %q", dates[6], today)
now := time.Now()
today := now.Format("2006-01-02")
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
if dates[6] != today && dates[6] != yesterday {
t.Errorf("last date = %q, want %q or %q", dates[6], today, yesterday)

Copilot uses AI. Check for mistakes.
Comment thread internal/cmd/help.go
Comment on lines +40 to +52
func customHelpFunc(defaultHelp func(*cobra.Command, []string)) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, args []string) {
if agentFlag {
printAgentHelp(cmd)
return
}
if cmd == cmd.Root() {
renderRootHelp(cmd.OutOrStdout(), cmd)
return
}
renderCommandHelp(cmd)
}
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

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

The defaultHelp parameter is never used, which can confuse readers and suggests an intended fallback path that doesn’t exist. Either remove the parameter (and update the call site), or call defaultHelp(cmd, args) where you want Cobra’s standard help output to be used.

Copilot uses AI. Check for mistakes.

@cubic-dev-ai cubic-dev-ai Bot 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.

4 issues found across 29 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/tui/styles.go">

<violation number="1" location="internal/tui/styles.go:127">
P3: Use a display-width calculation (e.g., lipgloss.Width or runewidth.StringWidth) instead of len() when wrapping/padding error text; len() counts bytes and will misalign the error box for non-ASCII messages.</violation>
</file>

<file name="scripts/release.sh">

<violation number="1" location="scripts/release.sh:73">
P2: The new jq requirement is unused in this script and in the release-check target, so it can block releases on machines that don’t have jq even though nothing here needs it. Remove this check unless a later step actually uses jq.</violation>
</file>

<file name="internal/tui/content.go">

<violation number="1" location="internal/tui/content.go:139">
P2: Use rune-/display-width truncation for subject so Unicode subjects don’t get split or misaligned in the TUI.</violation>

<violation number="2" location="internal/tui/content.go:298">
P2: Use rune-/display-width truncation for recording titles so Unicode titles render safely and align correctly.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread scripts/release.sh
echo " Branch: $CURRENT_BRANCH"
echo " Commit: $LOCAL"
# Verify required tools
if ! command -v jq >/dev/null 2>&1; then

@cubic-dev-ai cubic-dev-ai Bot Mar 31, 2026

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.

P2: The new jq requirement is unused in this script and in the release-check target, so it can block releases on machines that don’t have jq even though nothing here needs it. Remove this check unless a later step actually uses jq.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/release.sh, line 73:

<comment>The new jq requirement is unused in this script and in the release-check target, so it can block releases on machines that don’t have jq even though nothing here needs it. Remove this check unless a later step actually uses jq.</comment>

<file context>
@@ -23,58 +48,72 @@ DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
-echo "  Branch: $CURRENT_BRANCH"
-echo "  Commit: $LOCAL"
+# Verify required tools
+if ! command -v jq >/dev/null 2>&1; then
+  die "jq is required but not found. Install with your package manager."
+fi
</file context>
Fix with Cubic

Comment thread internal/tui/content.go
dateWidth := len(date)
prefixWidth := 4 // "│ ● " or " "
subjectWidth := max(c.width-prefixWidth-dateWidth-2, 10) // 2 for gap
if len(subject) > subjectWidth {

@cubic-dev-ai cubic-dev-ai Bot Mar 31, 2026

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.

P2: Use rune-/display-width truncation for subject so Unicode subjects don’t get split or misaligned in the TUI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/tui/content.go, line 139:

<comment>Use rune-/display-width truncation for subject so Unicode subjects don’t get split or misaligned in the TUI.</comment>

<file context>
@@ -0,0 +1,333 @@
+		dateWidth := len(date)
+		prefixWidth := 4                                         // "│ ● " or "    "
+		subjectWidth := max(c.width-prefixWidth-dateWidth-2, 10) // 2 for gap
+		if len(subject) > subjectWidth {
+			subject = subject[:subjectWidth-3] + "..."
+		}
</file context>
Fix with Cubic

Comment thread internal/tui/content.go
}

subjectWidth := max(c.width-4-len(date)-2, 10)
if len(title) > subjectWidth {

@cubic-dev-ai cubic-dev-ai Bot Mar 31, 2026

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.

P2: Use rune-/display-width truncation for recording titles so Unicode titles render safely and align correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/tui/content.go, line 298:

<comment>Use rune-/display-width truncation for recording titles so Unicode titles render safely and align correctly.</comment>

<file context>
@@ -0,0 +1,333 @@
+		}
+
+		subjectWidth := max(c.width-4-len(date)-2, 10)
+		if len(title) > subjectWidth {
+			title = title[:subjectWidth-3] + "..."
+		}
</file context>
Fix with Cubic

Comment thread internal/tui/styles.go
lines := wrapText(errMsg, maxInner)
innerWidth := 0
for _, l := range lines {
if len(l) > innerWidth {

@cubic-dev-ai cubic-dev-ai Bot Mar 31, 2026

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.

P3: Use a display-width calculation (e.g., lipgloss.Width or runewidth.StringWidth) instead of len() when wrapping/padding error text; len() counts bytes and will misalign the error box for non-ASCII messages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/tui/styles.go, line 127:

<comment>Use a display-width calculation (e.g., lipgloss.Width or runewidth.StringWidth) instead of len() when wrapping/padding error text; len() counts bytes and will misalign the error box for non-ASCII messages.</comment>

<file context>
@@ -1,23 +1,168 @@
+	lines := wrapText(errMsg, maxInner)
+	innerWidth := 0
+	for _, l := range lines {
+		if len(l) > innerWidth {
+			innerWidth = len(l)
+		}
</file context>
Fix with Cubic

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants