Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2523,6 +2523,12 @@ func (a *App) handleActionsMenu(key string) (tea.Model, tea.Cmd) {
return a.openURLMenuFromItems(extract.SessionURLs(sess.FilePath), "session")
case akm.Files:
return a.openURLMenuFromItems(extract.SessionFilePaths(sess.FilePath), "session files")
case akm.Changes:
changes := extract.SessionChanges(sess.FilePath)
items, cmap := changeItemsFromSlice(changes)
a.urlChangeMap = cmap
a.initDiffViewport()
return a.openURLMenuFromItems(items, "session changes")
case akm.Tags:
a.tagMenu = true
a.tagSessID = sess.ID
Expand Down Expand Up @@ -2609,24 +2615,30 @@ func (a *App) handleBulkActionsMenu(key string) (tea.Model, tea.Cmd) {
return a.openBulkURLMenu(selected, false)
case akm.Files:
return a.openBulkURLMenu(selected, true)
case akm.Changes:
return a.openBulkChangesMenu(selected)
}
return a, nil
}

// openBulkURLMenu merges URLs or file paths from multiple sessions into the URL menu.
func (a *App) openBulkChangesMenu(selected []session.Session) (tea.Model, tea.Cmd) {
seen := make(map[string]bool)
var merged []extract.Item
var merged []extract.ChangeItem
for _, s := range selected {
items := sessionChangeItems(s.FilePath)
for _, item := range items {
if !seen[item.URL] {
seen[item.URL] = true
merged = append(merged, item)
for _, ch := range extract.SessionChanges(s.FilePath) {
url := ch.Item.URL
if seen[url] {
continue
}
seen[url] = true
merged = append(merged, ch)
}
}
return a.openURLMenuFromItems(merged, fmt.Sprintf("%d sessions changes", len(selected)))
items, cmap := changeItemsFromSlice(merged)
a.urlChangeMap = cmap
a.initDiffViewport()
return a.openURLMenuFromItems(items, fmt.Sprintf("%d sessions changes", len(selected)))
}

func (a *App) openBulkURLMenu(selected []session.Session, files bool) (tea.Model, tea.Cmd) {
Expand Down
64 changes: 64 additions & 0 deletions internal/tui/interactions_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package tui

import (
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/sendbird/ccx/internal/session"
)

func writeChangeSession(t *testing.T, name string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name+".jsonl")
body := `{"type":"assistant","timestamp":"2025-01-01T00:00:00Z","message":{"role":"assistant","content":[{"type":"tool_use","name":"Edit","input":{"file_path":"/tmp/` + name + `.go","old_string":"a","new_string":"b"}}]}}` + "\n"
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write change session: %v", err)
}
return path
}

func TestConversationActionMenuUsesKeymapBindings(t *testing.T) {
app := newTestApp(fakeSessions())
app.keymap.Actions.URLs = "U"
Expand Down Expand Up @@ -94,3 +107,54 @@ func TestHandleConvActionsMenuUsesConfigurableChangeBinding(t *testing.T) {
t.Fatalf("expected change scope label, got %q", app.urlScope)
}
}

func TestHandleActionsMenuOpensSessionChanges(t *testing.T) {
path := writeChangeSession(t, "single")
sessions := []session.Session{{
ID: "aaa", ShortID: "aaa",
FilePath: path,
ProjectPath: "/tmp/proj-a", ProjectName: "proj-a",
ModTime: time.Now(), MsgCount: 1,
}}
app := newTestApp(sessions)
app.actionsSess = sessions[0]
app.actionsMenu = true
app.keymap.Actions.Changes = "g"

m, _ := app.handleActionsMenu("g")
app = m.(*App)
if !app.urlMenu {
t.Fatal("expected actions menu to open URL menu for session changes")
}
if !strings.Contains(app.urlScope, "changes") {
t.Fatalf("expected changes scope, got %q", app.urlScope)
}
if len(app.urlChangeMap) == 0 {
t.Fatal("expected change map populated for diff preview")
}
}

func TestHandleBulkActionsMenuOpensBulkChanges(t *testing.T) {
pathA := writeChangeSession(t, "bulk-a")
pathB := writeChangeSession(t, "bulk-b")
sessions := []session.Session{
{ID: "aaa", ShortID: "aaa", FilePath: pathA, ProjectPath: "/tmp/proj-a", ProjectName: "proj-a"},
{ID: "bbb", ShortID: "bbb", FilePath: pathB, ProjectPath: "/tmp/proj-b", ProjectName: "proj-b"},
}
app := newTestApp(sessions)
app.selectedSet = map[string]bool{"aaa": true, "bbb": true}
app.actionsMenu = true
app.keymap.Actions.Changes = "g"

m, _ := app.handleActionsMenu("g")
app = m.(*App)
if !app.urlMenu {
t.Fatal("expected bulk actions menu to open URL menu for changes")
}
if !strings.Contains(app.urlScope, "changes") {
t.Fatalf("expected bulk changes scope, got %q", app.urlScope)
}
if len(app.urlChangeMap) < 2 {
t.Fatalf("expected change map populated for both sessions, got %d", len(app.urlChangeMap))
}
}
Loading