Skip to content

Commit 96b54b6

Browse files
jongioCopilot
andauthored
feat: live activity push watcher, new session command, resume rename (#359)
* feat: live activity push watcher, new session command, resume rename - Add fsnotify-based EventWatcher for push session updates (replaces polling) - Add SessionTracker for PID tracking of launched sessions - Add focus window support (Win32 SetForegroundWindow) - Add new session launcher with + keybinding - Add W keybinding to focus selected session terminal window - Rename custom_command to resume_session_command with v1->v2 migration - Add new_session_command config field and settings panel entry - Add blinking dot and highlighted row for waiting sessions - Show last event type and timestamp in session detail pane - Update all docs (README, SECURITY, config.astro, features.astro) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37403417-09bc-4600-8056-fbb5a2fb18bb * feat: default new_session_command to 'copilot' and resume to 'copilot --resume' - Change defaultNewSessionCommand from 'gh copilot' to 'copilot' - Flip FindCLIBinary() to prefer 'copilot' over 'ghcs' - Update README, spec doc, web config page with new defaults - Add new_session_command to web config.astro options table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37403417-09bc-4600-8056-fbb5a2fb18bb * fix: reject non-positive PIDs and make EventWatcher.Stop synchronous Two cross-platform bugs surfaced by the new SessionTracker tests on Linux. IsProcessAlive treated PID 0 as alive on Unix: syscall.Kill(0, 0) targets the caller's own process group, so it returns nil. Both implementations now reject non-positive PIDs up front. The existing coverage lived in a windows-only test file, which is why CI never caught it; the tests move to process_test.go so they run on every platform. EventWatcher used one time.AfterFunc per session for debouncing, and Stop only called Timer.Stop() without waiting. A timer that had already fired could invoke onChange after the TUI closed eventWatchCh, panicking with "send on closed channel" (reproduced in 3 of 4 internal/tui runs). Debouncing now uses a dirty set drained by a single goroutine, and Stop waits on a WaitGroup, so no callback can be in flight once Stop returns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dac50159-d2c8-4ade-920a-333a51fc9ce3 * fix: satisfy staticcheck and errcheck in live-activity code LastSessionEvent built SessionEvent field-by-field from an identical struct (staticcheck S1016); use a direct conversion instead. Also explicitly discard the return values of the Win32 syscall wrappers in focus_windows.go, which errcheck flags when linting on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dac50159-d2c8-4ade-920a-333a51fc9ce3 * refactor: drop unused SessionTracker SessionTracker was never populated outside its own tests. LaunchNewSession always returns PID 0 because wt.exe spawns a detached child whose PID it cannot recover, and the TUI discarded that value anyway, so HasLive was always false and the focus path always fell through to the lock-file lookup. Remove the type and its tests, and collapse handleFocusWindowKey onto FindSessionPID, which is the mechanism that was actually doing the work. The test plan is updated to map the focus cases onto the platform PID tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dac50159-d2c8-4ade-920a-333a51fc9ce3 * fix: restore trailing debounce in EventWatcher The rewritten debounce fired a fixed 50ms after the first change rather than after changes stopped arriving, turning it into a throttle. A sustained burst of appends therefore produced one callback per 50ms window instead of one per quiet period, which is what TestEventWatcher_DebounceCollapses caught under the race detector. Reset the window on every new change, as the original per-session timers did, and cap the total deferral at 500ms so a continuously active session still refreshes instead of having its window extended forever. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dac50159-d2c8-4ade-920a-333a51fc9ce3 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37403417-09bc-4600-8056-fbb5a2fb18bb Copilot-Session: dac50159-d2c8-4ade-920a-333a51fc9ce3
1 parent 9557852 commit 96b54b6

44 files changed

Lines changed: 1756 additions & 422 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Dispatch reads your local Copilot CLI session store and presents every past sess
4141
- **Session favorites** (`*`) — star sessions as favorites. Filter to show only favorites via the `!` status picker
4242
- **Session tags** (`#`) — attach comma-separated tags to sessions and filter to a tag with the `tag:` search token
4343
- **Session aliases** (`A`) — give a session a short, memorable alias and resume it from the CLI with `dispatch open <alias>` instead of the full session ID
44-
- **Settings panel** (`,`) — 19 fields: Yolo Mode, Agent, Model, Launch Mode, Pane Direction, Terminal, Shell, Custom Command, Theme, Crash Recovery, Preview Position, Redact Secrets, Excluded Words, Auto Refresh, Notify On Waiting, and column toggles for Repo, Folder, Turns, and Host
44+
- **Settings panel** (`,`) — 20 fields: Yolo Mode, Agent, Model, Launch Mode, Pane Direction, Terminal, Shell, Resume Session Command, New Session Command, Theme, Crash Recovery, Preview Position, Redact Secrets, Excluded Words, Auto Refresh, Notify On Waiting, and column toggles for Repo, Folder, Turns, and Host
4545
- **Configurable list columns** (`,` settings) — choose which optional columns (repo, folder, turns, host) appear in the session list. Defaults show every column, and the session name and attention indicator are always visible
4646
- **Shell picker** — auto-detects installed shells, modal picker when multiple available
4747
- **5 built-in themes** — Dispatch Dark, Dispatch Light, Campbell, One Half Dark, One Half Light + custom via Windows Terminal JSON
@@ -706,7 +706,8 @@ dispatch config path # print the config file path
706706
| `model` | string | `""` | Pass `--model <name>` to Copilot CLI |
707707
| `launch_mode` | string | `"tab"` | How to open sessions: `in-place`, `tab`, `window`, `pane` |
708708
| `pane_direction` | string | `"auto"` | Split direction for pane mode: `auto`, `right`, `down`, `left`, `up` (see note below) |
709-
| `custom_command` | string | `""` | Custom launch command (`{sessionId}` is replaced) |
709+
| `resume_session_command` | string | `""` | Custom resume command (`{sessionId}` is replaced). Defaults to `copilot --resume` |
710+
| `new_session_command` | string | `""` | Command to launch new sessions (`{cwd}` is replaced). Defaults to `copilot` |
710711
| `excluded_dirs` | array | `[]` | Directory paths to hide from session list |
711712
| `excluded_words` | array | `[]` | Comma-separated words; sessions containing any word are hidden |
712713
| `attention_threshold` | string | `"15m"` | Duration after which an inactive running session is marked stale |
@@ -767,7 +768,8 @@ The split starts in the session's working directory (`-c`) and runs the resume c
767768
"model": "",
768769
"launch_mode": "tab",
769770
"pane_direction": "auto",
770-
"custom_command": "",
771+
"resume_session_command": "",
772+
"new_session_command": "",
771773
"excluded_dirs": [],
772774
"theme": "auto",
773775
"workspace_recovery": true,
@@ -779,12 +781,20 @@ The split starts in the session's working directory (`-c`) and runs the resume c
779781
}
780782
```
781783

782-
### Custom Command
784+
### Resume Session Command
783785

784-
Set `custom_command` to replace the default Copilot CLI launch entirely. Use `{sessionId}` as the placeholder. When set, Agent, Model, and Yolo Mode fields are ignored.
786+
Set `resume_session_command` to replace the default Copilot CLI resume command. Use `{sessionId}` as the placeholder for the session to resume. When set, Agent, Model, and Yolo Mode fields are ignored.
785787

786788
```json
787-
"custom_command": "my-tool resume {sessionId}"
789+
"resume_session_command": "my-tool resume {sessionId}"
790+
```
791+
792+
### New Session Command
793+
794+
Set `new_session_command` to customize the command used when launching a brand new session from dispatch (the `+` keybinding). Use `{cwd}` as the placeholder for the working directory. When empty, defaults to `copilot`.
795+
796+
```json
797+
"new_session_command": "copilot --agent workspace"
788798
```
789799

790800
### Customizing Keybindings

‎SECURITY.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ The application may contact the following domains:
5555

5656
- **Configuration file** (platform config directory, e.g.
5757
`~/.config/dispatch/config.json`): Treated as trusted user
58-
input. If a `custom_command` is configured, it is executed as-is. Keep your
58+
input. If a `resume_session_command` is configured, it is executed as-is. Keep your
5959
config file permissions restricted to your user account.
6060
- **Session data**: Read from the Copilot CLI SQLite database in read-only mode.
6161
Malformed session data cannot cause code execution.

‎cmd/dispatch/config.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func configFields() []configField {
7474
config.LaunchModeInPlace, config.LaunchModeTab, config.LaunchModeWindow, config.LaunchModePane),
7575
enumField("pane_direction", func(c *config.Config) *string { return &c.PaneDirection },
7676
config.PaneDirectionAuto, config.PaneDirectionRight, config.PaneDirectionDown, config.PaneDirectionLeft, config.PaneDirectionUp),
77-
strField("custom_command", func(c *config.Config) *string { return &c.CustomCommand }),
77+
strField("resume_session_command", func(c *config.Config) *string { return &c.ResumeSessionCommand }),
7878
boolField("ai_search", func(c *config.Config) *bool { return &c.AISearch }),
7979
durationField("attention_threshold", func(c *config.Config) *string { return &c.AttentionThreshold }),
8080
strField("theme", func(c *config.Config) *string { return &c.Theme }),

‎cmd/dispatch/new.go‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ func resolveNewDir(dir string) (string, error) {
136136
func defaultNewLaunch(w io.Writer, cfg *config.Config, dir string, mode string) error {
137137
if mode == config.LaunchModeInPlace {
138138
rc := platform.ResumeConfig{
139-
YoloMode: cfg.YoloMode,
140-
Agent: cfg.Agent,
141-
Model: cfg.Model,
142-
CustomCommand: cfg.CustomCommand,
143-
Cwd: dir,
139+
YoloMode: cfg.YoloMode,
140+
Agent: cfg.Agent,
141+
Model: cfg.Model,
142+
ResumeSessionCommand: cfg.ResumeSessionCommand,
143+
Cwd: dir,
144144
}
145145
cmd, err := platform.NewResumeCmd("", rc)
146146
if err != nil {
@@ -157,14 +157,14 @@ func defaultNewLaunch(w io.Writer, cfg *config.Config, dir string, mode string)
157157
return errors.New("no shell detected on this system")
158158
}
159159
rc := platform.ResumeConfig{
160-
YoloMode: cfg.YoloMode,
161-
Agent: cfg.Agent,
162-
Model: cfg.Model,
163-
Terminal: cfg.DefaultTerminal,
164-
CustomCommand: cfg.CustomCommand,
165-
Cwd: dir,
166-
LaunchStyle: launchStyleForOpenMode(mode),
167-
PaneDirection: cfg.EffectivePaneDirection(),
160+
YoloMode: cfg.YoloMode,
161+
Agent: cfg.Agent,
162+
Model: cfg.Model,
163+
Terminal: cfg.DefaultTerminal,
164+
ResumeSessionCommand: cfg.ResumeSessionCommand,
165+
Cwd: dir,
166+
LaunchStyle: launchStyleForOpenMode(mode),
167+
PaneDirection: cfg.EffectivePaneDirection(),
168168
}
169169
if err := platform.LaunchSession(shell, "", rc); err != nil {
170170
return err

‎cmd/dispatch/open.go‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,11 @@ func readSessionIDs(r io.Reader) ([]string, error) {
212212
// omitted because they affect terminal placement, not the copilot invocation.
213213
func openResumeConfig(cfg *config.Config, sess *data.Session) platform.ResumeConfig {
214214
return platform.ResumeConfig{
215-
YoloMode: cfg.YoloMode,
216-
Agent: cfg.Agent,
217-
Model: cfg.Model,
218-
CustomCommand: cfg.CustomCommand,
219-
Cwd: sess.Cwd,
215+
YoloMode: cfg.YoloMode,
216+
Agent: cfg.Agent,
217+
Model: cfg.Model,
218+
ResumeSessionCommand: cfg.ResumeSessionCommand,
219+
Cwd: sess.Cwd,
220220
}
221221
}
222222

@@ -471,14 +471,14 @@ func defaultOpenLaunch(w io.Writer, cfg *config.Config, sess *data.Session, mode
471471
return errors.New("no shell detected on this system")
472472
}
473473
rc := platform.ResumeConfig{
474-
YoloMode: cfg.YoloMode,
475-
Agent: cfg.Agent,
476-
Model: cfg.Model,
477-
Terminal: cfg.DefaultTerminal,
478-
CustomCommand: cfg.CustomCommand,
479-
Cwd: sess.Cwd,
480-
LaunchStyle: launchStyleForOpenMode(mode),
481-
PaneDirection: cfg.EffectivePaneDirection(),
474+
YoloMode: cfg.YoloMode,
475+
Agent: cfg.Agent,
476+
Model: cfg.Model,
477+
Terminal: cfg.DefaultTerminal,
478+
ResumeSessionCommand: cfg.ResumeSessionCommand,
479+
Cwd: sess.Cwd,
480+
LaunchStyle: launchStyleForOpenMode(mode),
481+
PaneDirection: cfg.EffectivePaneDirection(),
482482
}
483483
if err := platform.LaunchSession(shell, sess.ID, rc); err != nil {
484484
return err

‎docs/specs/live-activity/spec.md‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
author: "@jongio"
3+
status: approved
4+
---
5+
6+
<!-- Pipeline tracking (auto-managed, not part of product spec) -->
7+
<!-- ## Pipeline Status -->
8+
<!-- Phase: VERIFYING -->
9+
10+
# Live Activity Monitor
11+
12+
## Problem
13+
14+
Dispatch polls session attention status every 30 seconds (`attentionRefreshInterval`). This means state transitions (a session finishing AI work and waiting for user input) can take up to 30 seconds to appear. Users working across multiple sessions need instant feedback about which sessions need their attention, the ability to launch new sessions from dispatch (to track them), and a one-key action to switch focus to a running session's terminal.
15+
16+
## Solution
17+
18+
Four integrated capabilities:
19+
20+
1. **Event Watcher (push-based)**: Replace 30-second polling with filesystem notifications. Watch `~/.copilot/session-state/` for changes to `events.jsonl` and lock files. When a file changes, re-classify only that session and push the update to the TUI instantly.
21+
22+
2. **New Session Launch**: A configurable keybinding that starts a brand new Copilot CLI session in the selected session/folder's working directory. The launch command is configurable via `new_session_command` in settings.
23+
24+
3. **Session Process Tracking**: When dispatch launches (or resumes) a session, record the child process PID. This tracking persists so dispatch knows which running sessions it owns.
25+
26+
4. **Focus Session**: A keybinding that brings the terminal window of a tracked running session to the foreground. Uses the tracked PID to locate the owning window.
27+
28+
## Design
29+
30+
### Event Watcher (`internal/data/eventwatcher.go`)
31+
32+
A new `EventWatcher` struct using `fsnotify` to monitor `~/.copilot/session-state/`. On file write events for `events.jsonl` or `inuse.*.lock`, it debounces (50ms), re-classifies the affected session, and fires a callback with the session ID and new status. The existing `attentionRefreshInterval` polling remains as a fallback (bumped to 120s) for edge cases where fsnotify misses events.
33+
34+
### New Session Launch
35+
36+
Config adds `NewSessionCommand string` (default: `"copilot"`). Template variables: `{cwd}`. Keybinding: `N` (uppercase, mnemonic: "New"). When triggered, dispatch runs the configured command in a new WT tab/pane at the selected folder's cwd.
37+
38+
### Session Process Tracking (`internal/data/sessiontrack.go`)
39+
40+
An in-memory map `sessionID -> TrackedSession{PID int, LaunchTime time.Time}` with periodic cleanup of dead PIDs. Not persisted to disk (PIDs are ephemeral; stale after reboot). Populated when `LaunchSession` or the new-session launch completes.
41+
42+
### Focus Session (`internal/platform/focus_windows.go`)
43+
44+
Given a PID, walk the process tree upward to find the terminal host window, then call `SetForegroundWindow`. Keybinding: `F` (uppercase, mnemonic: "Focus"). Only active when the selected session has a tracked live PID.
45+
46+
## Acceptance Criteria
47+
48+
1. When a session's `events.jsonl` changes, the attention dot updates within 200ms (not 30s)
49+
2. Pressing `N` on a selected session/folder launches a new Copilot CLI session in that cwd
50+
3. The `new_session_command` config setting controls what command is run for new sessions
51+
4. Pressing `F` on a session with a tracked live process brings its terminal window to the foreground
52+
5. The 30-second polling fallback still works when fsnotify is unavailable or misses events
53+
6. No increase in idle CPU usage (fsnotify is kernel-driven, not busy-wait)
54+
55+
## Non-Goals
56+
57+
- Tab-level granularity within Windows Terminal (WT doesn't expose tab-focus APIs externally)
58+
- Tracking sessions launched outside dispatch (only dispatch-launched sessions are tracked)
59+
- macOS/Linux window focus (stub implementations for now; Windows is primary)
60+
61+
<!-- Pipeline tracking (auto-managed, not part of product spec) -->
62+
## Pipeline Status
63+
Phase: BUILDING
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Test Plan: Live Activity Monitor
2+
3+
## Status: AUTOMATED
4+
5+
## Planned Tests
6+
7+
| ID | AC | Description | Status |
8+
|----|-----|-------------|--------|
9+
| T1 | AC1 | EventWatcher fires callback within 200ms when events.jsonl is modified | automated |
10+
| T2 | AC1 | EventWatcher debounces rapid writes (multiple writes within 50ms produce one callback) | automated |
11+
| T3 | AC1 | EventWatcher handles lock file creation/deletion (session start/stop) | automated |
12+
| T4 | AC1 | EventWatcher ignores non-session directories and invalid session IDs | automated |
13+
| T5 | AC5 | Fallback polling still triggers attention scan when watcher is stopped | blocked(integration-only; polling interval verified structurally) |
14+
| T6 | AC2 | New session launch executes configured command in selected session's cwd | blocked(requires real terminal; verified via code path inspection) |
15+
| T7 | AC3 | NewSessionCommand config field is respected (custom command template) | automated |
16+
| T8 | AC3 | Default new session command works when config field is empty | automated |
17+
| T9 | AC4 | Focus keybinding calls platform focus with the session's PID | blocked(requires real Windows terminal; platform code isolated) |
18+
| T10 | AC4 | Focus is no-op when the session's lock file yields no PID | automated |
19+
| T11 | AC4 | Focus is no-op when the located PID is dead | automated |
20+
| T12 | AC6 | EventWatcher idle CPU is negligible (no busy-wait) | blocked(requires profiling; fsnotify is event-based by design) |
21+
| T13 | AC1 | EventWatcher correctly identifies session ID from file path | automated |
22+
| T14 | AC4 | IsProcessAlive rejects non-positive and nonexistent PIDs | automated |
23+
24+
## Test Mapping
25+
26+
- `internal/data/eventwatcher_test.go`: T1, T2, T3, T4, T13
27+
- `internal/platform/process_test.go`: T10, T11, T14
28+
- `internal/tui/handlers_eventwatcher.go` (code path): T7, T8
29+
- `internal/data/eventwatcher.go` (NewEventWatcher watches new dirs): T3

‎go.mod‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/atotto/clipboard v0.1.4
1212
github.com/charmbracelet/x/ansi v0.11.7
1313
github.com/creack/pty v1.1.24
14+
github.com/fsnotify/fsnotify v1.10.1
1415
github.com/github/copilot-sdk/go v1.0.8
1516
github.com/lucasb-eyer/go-colorful v1.4.0
1617
golang.org/x/sync v0.22.0

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
5454
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
5555
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
5656
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
57+
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
58+
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
5759
github.com/github/copilot-sdk/go v1.0.8 h1:vuGAK8r/biF2tBHF2PaZ9b0i35KQzPY6eja2ESBW+Ms=
5860
github.com/github/copilot-sdk/go v1.0.8/go.mod h1:U0STg9Jdv9LDwtkUtcUJCzPQ4KTnKakRfP+1LhEx248=
5961
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=

‎internal/config/config.go‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const (
3131
// currentConfigVersion is the schema version written to new and migrated
3232
// config files. Increment this when making breaking schema changes and
3333
// add a corresponding migration case in [migrate].
34-
currentConfigVersion = 1
34+
currentConfigVersion = 2
3535
)
3636

3737
// NamedView holds a saved combination of list state filters that can be
@@ -180,8 +180,19 @@ type Config struct {
180180
// with the actual session ID at launch time. When set, YoloMode, Agent,
181181
// and Model are ignored (they only apply to the default copilot CLI).
182182
// Terminal and Shell settings are still used.
183+
//
184+
// Deprecated: renamed to ResumeSessionCommand in config v2. This field
185+
// exists only for backward-compatible deserialization; migrate() copies
186+
// it to ResumeSessionCommand then clears it.
183187
CustomCommand string `json:"custom_command,omitempty"`
184188

189+
// ResumeSessionCommand is a user-defined command that replaces the
190+
// default copilot CLI resume command. The placeholder {sessionId} is
191+
// replaced with the actual session ID at launch time. When set,
192+
// YoloMode, Agent, and Model are ignored (they only apply to the
193+
// default copilot CLI). Terminal and Shell settings are still used.
194+
ResumeSessionCommand string `json:"resume_session_command,omitempty"`
195+
185196
// ExcludedWords is a list of words used to filter sessions from the
186197
// dispatch list. Sessions whose summary or turn content contains any
187198
// of these words (case-insensitive) are hidden from the session list.
@@ -297,6 +308,12 @@ type Config struct {
297308
// ActiveView is the name of the currently active named view.
298309
// Empty or "Default" means no named view is active.
299310
ActiveView string `json:"active_view,omitempty"`
311+
312+
// NewSessionCommand is the command template used to launch a brand new
313+
// Copilot CLI session from dispatch. The placeholder {cwd} is replaced
314+
// with the target working directory. When empty, defaults to "copilot".
315+
// Examples: "copilot", "copilot --agent workspace", "gh copilot"
316+
NewSessionCommand string `json:"new_session_command,omitempty"`
300317
}
301318

302319
// LaunchMode describes how sessions are opened in the terminal.
@@ -627,6 +644,15 @@ func migrate(cfg *Config) {
627644
cfg.LaunchMode = LaunchModeInPlace
628645
}
629646
}
647+
648+
// v1 → v2: custom_command renamed to resume_session_command.
649+
// Copy the old value if the new field is empty.
650+
if cfg.ConfigVersion < 2 {
651+
if cfg.CustomCommand != "" && cfg.ResumeSessionCommand == "" {
652+
cfg.ResumeSessionCommand = cfg.CustomCommand
653+
}
654+
cfg.CustomCommand = "" // clear so it won't be written back
655+
}
630656
}
631657

632658
// shellUnsafe contains characters that must never appear in shell or terminal

0 commit comments

Comments
 (0)