Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ Dispatch reads your local Copilot CLI session store and presents every past sess
- **Time range filtering** (`1`–`4`) — 1 hour, 1 day, 7 days, all
- **Preview panel** (`p`) — metadata, chat-style conversation bubbles, checkpoints (up to 5), files (up to 5), refs (up to 5), scroll indicators. Toggle conversation sort order with `o`. Click the session ID row to copy it to clipboard
- **Copy session ID** (`c`) — copy the selected session's ID to the system clipboard. Also available by clicking the ID row in the preview pane
- **Copy resume command** (`Y`) — copy the selected session's full resume command to the system clipboard
- **Copy resume command** (`Y`) — copy the selected session's full resume command to the system clipboard. With a multi-select active, copies one resume command per selected session, one per line
- **Open working directory** (`O`) — open the selected session's working directory in the system file manager (Explorer on Windows, Finder on macOS, the default file manager on Linux)
- **Four launch modes** (`Enter` / `t` / `w` / `e`) — in-place, new tab, new window, split pane (Windows Terminal) with per-session overrides
- **Multi-session open** (`Space` / `L` / `a` / `d`) — select multiple sessions with Space, launch all at once with L, select/deselect all with a/d. Shift+↑/↓ for range selection, Ctrl+click and Shift+click for mouse selection. With a selection active, `h` (hide) and `*` (favorite) apply to every selected session at once
- **Multi-session open** (`Space` / `L` / `a` / `d`) — select multiple sessions with Space, launch all at once with L, select/deselect all with a/d. Shift+↑/↓ for range selection, Ctrl+click and Shift+click for mouse selection. With a selection active, `h` (hide), `*` (favorite), and `Y` (copy resume command) apply to every selected session at once
- **Attention indicators** — colored dots showing real-time session status: working (blue, executing tools), thinking (cyan, generating response), compacting (magenta, context compaction), waiting (purple), active (green), stale (yellow), interrupted (orange ⚡), idle (gray). Jump to next waiting session with `n`, resume interrupted sessions with `R`, filter by status with `!`
- **Host type icons** — sessions display an icon indicating their origin: CLI (desktop), Cloud (cloud), or Actions (gear)
- **Incremental auto-refresh** — the session list auto-refreshes within 2 seconds when the Copilot CLI writes new data (WAL file polling when focused). No manual reindex needed for normal use. Tune the interval or turn it off with `auto_refresh_seconds`
Expand Down
41 changes: 30 additions & 11 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1701,24 +1701,43 @@ func (m Model) handleCopyPath() (tea.Model, tea.Cmd) {
return m, clearStatusAfter(2 * time.Second)
}

// handleCopyResumeCommand copies the selected session's resume command to the
// system clipboard. It mirrors the same launch options used by Dispatch when
// opening sessions so copied commands match the configured workflow.
// handleCopyResumeCommand copies the resume command(s) to the system clipboard.
// When multi-select is active, one resume command per selected session is
// copied, joined by newlines and ordered to match the list. Otherwise only the
// current session's command is copied. Copied commands mirror the same launch
// options Dispatch uses when opening sessions so they match the configured
// workflow.
func (m Model) handleCopyResumeCommand() (tea.Model, tea.Cmd) {
sess, ok := m.sessionList.Selected()
if !ok {
var sessions []data.Session
if sel := m.sessionList.SelectedSessions(); len(sel) > 0 {
sessions = sel
} else if sess, ok := m.sessionList.Selected(); ok {
sessions = []data.Session{sess}
}
if len(sessions) == 0 {
return m, nil
}
cmd, err := platform.BuildResumeCommandString(sess.ID, m.resumeConfigForSession(sess.Cwd))
if err != nil {
m.statusErr = "resume command: " + err.Error()
return m, clearStatusAfter(2 * time.Second)

cmds := make([]string, 0, len(sessions))
for _, sess := range sessions {
cmd, err := platform.BuildResumeCommandString(sess.ID, m.resumeConfigForSession(sess.Cwd))
if err != nil {
m.statusErr = "resume command: " + err.Error()
return m, clearStatusAfter(2 * time.Second)
}
cmds = append(cmds, cmd)
}
if err := clipboardWrite(cmd); err != nil {

if err := clipboardWrite(strings.Join(cmds, "\n")); err != nil {
m.statusErr = "clipboard: " + err.Error()
return m, clearStatusAfter(2 * time.Second)
}
m.statusInfo = statusCopiedResumeCommand

if len(cmds) == 1 {
m.statusInfo = statusCopiedResumeCommand
} else {
m.statusInfo = fmt.Sprintf("Copied %d resume commands ✓", len(cmds))
}
return m, clearStatusAfter(2 * time.Second)
}

Expand Down
41 changes: 41 additions & 0 deletions internal/tui/model_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,47 @@ func TestHandleKey_CopyResumeCommand_ClipboardError(t *testing.T) {
}
}

func TestHandleKey_CopyResumeCommand_MultiSelect(t *testing.T) {
var copied string
orig := clipboardWrite
clipboardWrite = func(text string) error {
copied = text
return nil
}
t.Cleanup(func() { clipboardWrite = orig })

m := newTestModel()
m.cfg.CustomCommand = "copilot --resume {sessionId}"
m.sessionList.SetSessions([]data.Session{
{ID: "s1", Cwd: "/a"},
{ID: "s2", Cwd: "/b"},
{ID: "s3", Cwd: "/c"},
})

// Mark the first and third sessions with Space.
m.sessionList.ToggleSelected()
m.sessionList.MoveDown()
m.sessionList.MoveDown()
m.sessionList.ToggleSelected()

result, cmd := m.Update(runeKeyMsg('Y'))
rm := result.(Model)

want := "copilot --resume s1\ncopilot --resume s3"
if copied != want {
t.Errorf("clipboard text = %q, want %q", copied, want)
}
if rm.statusInfo != "Copied 2 resume commands ✓" {
t.Errorf("statusInfo = %q, want %q", rm.statusInfo, "Copied 2 resume commands ✓")
}
if rm.statusErr != "" {
t.Errorf("statusErr = %q, want empty", rm.statusErr)
}
if cmd == nil {
t.Error("CopyResumeCommand multi-select should return clearStatusAfter cmd")
}
}

// ---------------------------------------------------------------------------
// CopyPreview (y key)
// ---------------------------------------------------------------------------
Expand Down