Skip to content

feat(tui): add clipboard copy via OSC 52 on observation screens#142

Open
Snakeblack wants to merge 1 commit intoGentleman-Programming:mainfrom
Snakeblack:feat/tui-clipboard-copy
Open

feat(tui): add clipboard copy via OSC 52 on observation screens#142
Snakeblack wants to merge 1 commit intoGentleman-Programming:mainfrom
Snakeblack:feat/tui-clipboard-copy

Conversation

@Snakeblack
Copy link
Copy Markdown

Linked Issue

Closes #141

PR Type

  • New feature

Summary

  • Add c keybinding to copy observation content to clipboard via OSC 52 escape sequences
  • Works on Recent Observations, Observation Detail, Search Results, and Session Detail screens
  • Shows "✓ Copied!" / "✗ Copy failed" visual feedback for 2 seconds

Changes

File Change
internal/tui/model.go Add CopyFeedback field, clipboardCopiedMsg / copyFeedbackClearMsg messages, and copyToClipboard() command using go-osc52/v2
internal/tui/update.go Handle clipboard result and feedback-clear messages in Update(); add "c" key handler in handleRecentKeys, handleObservationDetailKeys, handleSearchResultsKeys, handleSessionDetailKeys
internal/tui/view.go Update help text on 4 screens to show c copy; render copy feedback overlay in View() using existing green/red color tokens

Design Decisions

  • OSC 52 over atotto/clipboard: Works over SSH, no system dependencies (xclip/xsel/pbcopy), and go-osc52/v2 is already a transitive dependency from the Charm ecosystem — zero new deps added
  • Feedback via tea.Tick: Standard Bubbletea pattern. 2-second auto-clear keeps the help area clean
  • Graceful fallback: Terminals that don't support OSC 52 get a friendly "✗ Copy failed" message

Test Plan

  • Unit tests pass locally: go test ./internal/tui/... — 37 tests, all passing
  • Build passes: go build ./...
  • Manually tested clipboard copy on Windows Terminal (OSC 52 supported)
  • Verified feedback appears and auto-clears after 2 seconds
  • Verified Ctrl+V pastes the copied content correctly
  • Verified no key binding conflicts with existing c-less screens

Contributor Checklist

  • Linked an approved issue
  • Commits follow Conventional Commits format
  • Branch follows type/description naming
  • No generated or temporary files committed
  • All tests pass locally

Copilot AI review requested due to automatic review settings April 1, 2026 14:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds OSC 52 clipboard-copy support to the Engram Bubbletea TUI observation-related screens, with short-lived visual feedback after a copy attempt.

Changes:

  • Introduces a clipboard copy command (copyToClipboard) and feedback state/messages on the TUI model.
  • Handles clipboard-result + auto-clear feedback messages in Update(), and adds c keybindings on 4 observation screens.
  • Updates help text and renders copy feedback in the main View() output.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
internal/tui/model.go Adds clipboard feedback fields/messages and an OSC 52 copy command.
internal/tui/update.go Wires c key handling plus feedback and auto-clear tick handling into Update().
internal/tui/view.go Updates help text to include c copy and renders copy feedback.

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

Comment on lines +244 to +248
func copyToClipboard(text string) tea.Cmd {
return func() tea.Msg {
_, err := osc52.New(text).WriteTo(os.Stderr)
return clipboardCopiedMsg{err: err}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

copyToClipboard() writes OSC 52 sequences directly to os.Stderr from within a Bubbletea command. This bypasses Bubbletea’s configured output writer (the program uses stdout by default) and introduces hard-to-test, side-effecting I/O from a goroutine, which can also interleave with the renderer’s output. Consider emitting the OSC 52 sequence through the same output stream as the TUI (or making the writer injectable) so clipboard writes are routed consistently and can be tested deterministically.

Copilot uses AI. Check for mistakes.
Comment on lines 323 to +330
m.PrevScreen = ScreenSearchResults
return m, loadTimeline(m.store, obsID)
}
case "c":
if len(m.SearchResults) > 0 && m.Cursor < len(m.SearchResults) {
r := m.SearchResults[m.Cursor]
return m, copyToClipboard(r.Content)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

New clipboard behavior isn’t covered by the existing TUI key-path tests. internal/tui/update_test.go has explicit assertions for other keys on these screens (e.g. recent enter/t/esc), but there are no tests ensuring that pressing c returns a clipboard command and that clipboardCopiedMsg updates CopyFeedback. Adding tests here would prevent regressions across the four screens that now support copy.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to 102
fbStyle := lipgloss.NewStyle().Bold(true).Foreground(colorGreen)
if strings.HasPrefix(m.CopyFeedback, "✗") {
fbStyle = lipgloss.NewStyle().Bold(true).Foreground(colorRed)
}
content += "\n" + fbStyle.Render(m.CopyFeedback)
}

return appStyle.Render(content)
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The copy feedback color is derived by parsing the rendered text (strings.HasPrefix(m.CopyFeedback, "✗")). This makes styling brittle (changing the message text breaks the color logic) and mixes presentation with state. Consider storing a small structured state (e.g., a bool/enum for success vs failure) alongside the message and selecting the style from that instead.

Suggested change
fbStyle := lipgloss.NewStyle().Bold(true).Foreground(colorGreen)
if strings.HasPrefix(m.CopyFeedback, "✗") {
fbStyle = lipgloss.NewStyle().Bold(true).Foreground(colorRed)
}
content += "\n" + fbStyle.Render(m.CopyFeedback)
}
return appStyle.Render(content)
}
fbStyle := m.copyFeedbackStyle()
content += "\n" + fbStyle.Render(m.CopyFeedback)
}
return appStyle.Render(content)
}
// copyFeedbackStyle determines the style for the current copy feedback message.
// NOTE: This currently infers error vs. success from the message content, but
// the logic is centralized here so it can later be switched to a dedicated
// structured state (e.g., a bool/enum on the model) without touching View.
func (m Model) copyFeedbackStyle() lipgloss.Style {
// Default to success style.
style := lipgloss.NewStyle().Bold(true).Foreground(colorGreen)
// If the feedback indicates a failure, use the error style.
if strings.HasPrefix(m.CopyFeedback, "✗") {
style = lipgloss.NewStyle().Bold(true).Foreground(colorRed)
}
return style
}

Copilot uses AI. Check for mistakes.
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.

feat(tui): add clipboard copy support to observation screens

2 participants