diff --git a/internal/tui/app.go b/internal/tui/app.go index ea9b9ec..c9a0cc5 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -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 @@ -2609,6 +2615,8 @@ 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 } @@ -2616,17 +2624,21 @@ func (a *App) handleBulkActionsMenu(key string) (tea.Model, tea.Cmd) { // 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) { diff --git a/internal/tui/interactions_test.go b/internal/tui/interactions_test.go index 9891a61..d6b1c2b 100644 --- a/internal/tui/interactions_test.go +++ b/internal/tui/interactions_test.go @@ -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" @@ -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)) + } +}