feat: copy thread / conversation

This commit is contained in:
2026-08-04 16:26:50 +02:00
parent 63ce0319f7
commit d3f98c6dbe
7 changed files with 350 additions and 4 deletions

24
tui.go
View File

@@ -148,6 +148,7 @@ type App struct {
secondaryLoading bool
err error
lastRefresh time.Time
notice string
pendingZ bool
searching bool
searchQuery string
@@ -175,6 +176,7 @@ type App struct {
prEditUserIndex int
prEditGeneration uint64
cursorOutput *terminalCursorOutput
clipboard textClipboard
foldResolved bool
threadListWidthPercent int
@@ -291,6 +293,7 @@ func NewAppWithSettings(
viewerLabel: firstNonEmpty(settings.ViewerLabel, "login"),
editorMode: settings.EditorMode,
keybindings: settings.KeyBindings,
clipboard: systemTextClipboard{},
readState: state,
drafts: settings.Drafts,
mutations: settings.Mutations,
@@ -981,6 +984,15 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.recordHealth(issue.Component, healthWarning, issue.Message)
}
return m, m.difflet.setState(m.restingDiffletState())
case threadCopiedMsg:
if msg.err != nil {
m.err = fmt.Errorf("copy review thread: %w", msg.err)
m.notice = ""
return m, m.difflet.setState(diffletRecoverableError)
}
m.err = nil
m.notice = "thread copied"
return m, m.difflet.setState(diffletSuccess)
case draftFlushMsg:
if msg.err != nil {
m.recordHealth("draft persistence", healthWarning, msg.err.Error())
@@ -1346,6 +1358,7 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
rawKey := k
m.notice = ""
k = m.keybindings.canonicalMainKey(k, m.screen)
if k == "ctrl+c" || k == "q" {
return m, tea.Quit
@@ -1466,6 +1479,10 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.screen == threadScreen {
m.markCurrentThreadRead()
}
case "y":
if m.screen == threadScreen {
return m, m.copySelectedThread()
}
case "d":
if m.screen == prScreen && len(m.prs) > 0 {
return m, m.openSelectedPR(dashboardScreen)
@@ -2677,6 +2694,7 @@ func (m App) helpBindings() []helpBinding {
{keyLabel(m.keybindings.Threads.ClearFilter), "Clear the active thread filter and show every thread"},
{combinedKeyLabel(m.keybindings.Threads.NextUnread, m.keybindings.Threads.PreviousUnread), "Next / previous new update"},
{keyLabel(m.keybindings.Threads.MarkRead), "Mark the selected thread read"},
{keyLabel(m.keybindings.Threads.Copy), "Copy the selected thread and local AI conversation for an LLM"},
{keyLabel(m.keybindings.Threads.Reply), "Compose a reply to the selected thread"},
{keyLabel(m.keybindings.Threads.Resolve), "Resolve or unresolve the selected thread"},
{keyLabel(m.keybindings.Views.AI), "Open the local AI review or selected-thread discussion menu"},
@@ -3782,10 +3800,11 @@ func (m App) viewThreads() string {
body = lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right)
}
help := fmt.Sprintf(
"%s keys • %s focus • %s move/scroll • %s AI • %s reply • %s resolve • %s dashboard • %s back • %s quit",
"%s keys • %s focus • %s move/scroll • %s copy • %s AI • %s reply • %s resolve • %s dashboard • %s back • %s quit",
primaryKeyLabel(m.keybindings.General.Help),
primaryCombinedKeyLabel(m.keybindings.Navigation.Left, m.keybindings.Navigation.Right),
primaryCombinedKeyLabel(m.keybindings.Navigation.Down, m.keybindings.Navigation.Up),
primaryKeyLabel(m.keybindings.Threads.Copy),
primaryKeyLabel(m.keybindings.Views.AI),
primaryKeyLabel(m.keybindings.Threads.Reply),
primaryKeyLabel(m.keybindings.Threads.Resolve),
@@ -4565,6 +4584,9 @@ func (m App) frame(lines []string, help string) string {
if m.err != nil {
status = badStyle.Render("error: " + truncate(m.err.Error(), max(20, m.width-8)))
}
if status == "" && m.notice != "" {
status = okStyle.Render(m.notice)
}
// Keep an already-rendered screen byte-for-byte stable while a background
// refresh starts. Changing only this footer on a full-height alternate
// screen makes some terminals clear and repaint the entire frame.