diff --git a/CHANGELOG.md b/CHANGELOG.md index 604d2cd..bdc619f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ### Added - **Configurable auto-refresh** — set `auto_refresh_seconds` (also in the settings panel) to tune the session-list poll interval, or set it to `0` to turn polling off and refresh only with `r` or reindex - **Machine-readable doctor output** — `dispatch doctor --json` prints diagnostics as a single JSON object so scripts and CI can parse them instead of scraping text +- **Search query argument** — pass a search string on the command line (`dispatch auth` or `dispatch fix auth bug`) to launch the TUI with the search box pre-filled and the list already filtered ## [v0.13.0] — 2026-06-30 diff --git a/README.md b/README.md index ef6cc76..e2ef92a 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,13 @@ The installer also creates a `disp` alias automatically. dispatch ``` +Pass a search term to open with the search box pre-filled and the list already filtered: + +```sh +dispatch auth +dispatch fix auth bug +``` + ### Example Workflow 1. Run `dispatch` (or `disp`) in your terminal diff --git a/cmd/dispatch/cli.go b/cmd/dispatch/cli.go index 8722832..4f228b9 100644 --- a/cmd/dispatch/cli.go +++ b/cmd/dispatch/cli.go @@ -21,10 +21,13 @@ import ( // handleArgs processes CLI arguments and executes early-exit subcommands // (help, version, update, clear-cache, reindex). It returns done=true when -// the caller should exit without starting the TUI. When --demo is among -// the arguments, cleanup is non-nil and the caller must defer it. Errors -// indicate a failing subcommand; the error message is already printed to -// stderr. +// the caller should exit without starting the TUI. A single leading token +// that is not a known subcommand and does not start with "-" (or several such +// tokens) is treated as an initial search query and returned in query; the +// caller seeds the TUI search bar with it. When --demo is among the +// arguments, cleanup is non-nil and the caller must defer it. Errors indicate +// a failing subcommand or an unknown flag; the error message is already +// printed to stderr. // // Function variables (below) allow test substitution of external calls. var ( @@ -34,79 +37,80 @@ var ( configResetFn = config.Reset ) -func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.UpdateInfo) (done bool, cleanup func(), err error) { +func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.UpdateInfo) (done bool, cleanup func(), query string, err error) { + var queryParts []string for _, arg := range args { switch arg { case "--help", "-h", "help": printUsage() showUpdateNotification(origStderr, updateCh) - return true, cleanup, nil + return true, cleanup, "", nil case "--version", "-v", "version": fmt.Println(version.Version) showUpdateNotification(origStderr, updateCh) - return true, cleanup, nil + return true, cleanup, "", nil case "update": if uErr := runUpdateFn(context.Background(), version.Version); uErr != nil { fmt.Fprintf(os.Stderr, "update: %v\n", uErr) - return true, cleanup, uErr + return true, cleanup, "", uErr } - return true, cleanup, nil + return true, cleanup, "", nil case "completion": if len(args) < 2 { err := errors.New("completion requires a shell: bash, zsh, or powershell") fmt.Fprintf(os.Stderr, "completion: %v\n", err) - return true, cleanup, err + return true, cleanup, "", err } if cErr := runCompletion(os.Stdout, args[1]); cErr != nil { fmt.Fprintf(os.Stderr, "completion: %v\n", cErr) - return true, cleanup, cErr + return true, cleanup, "", cErr } - return true, cleanup, nil + return true, cleanup, "", nil case "doctor": if slices.Contains(args, "--json") { if jErr := runDoctorJSON(os.Stdout); jErr != nil { fmt.Fprintf(os.Stderr, "doctor: %v\n", jErr) - return true, cleanup, jErr + return true, cleanup, "", jErr } } else { runDoctor(os.Stdout) } showUpdateNotification(origStderr, updateCh) - return true, cleanup, nil + return true, cleanup, "", nil case "open": if oErr := runOpen(os.Stdout, args); oErr != nil { fmt.Fprintf(os.Stderr, "open: %v\n", oErr) - return true, cleanup, oErr + return true, cleanup, "", oErr } - return true, cleanup, nil + return true, cleanup, "", nil case "stats": if sErr := runStats(os.Stdout, args); sErr != nil { fmt.Fprintf(os.Stderr, "stats: %v\n", sErr) - return true, cleanup, sErr + return true, cleanup, "", sErr } - return true, cleanup, nil + return true, cleanup, "", nil case "--demo": c, demoErr := setupDemo() if demoErr != nil { fmt.Fprintf(os.Stderr, "demo: %v\n", demoErr) - return true, cleanup, demoErr + return true, cleanup, "", demoErr } cleanup = c case "--clear-cache": if cErr := configResetFn(); cErr != nil { fmt.Fprintf(os.Stderr, "clear-cache: %v\n", cErr) - return true, cleanup, cErr + return true, cleanup, "", cErr } fmt.Println("Config reset to defaults.") - return true, cleanup, nil + return true, cleanup, "", nil case "--reindex": fmt.Println("Reindexing session store via Copilot CLI…") @@ -118,11 +122,11 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd fmt.Println("Copilot CLI not found, running index maintenance…") if mErr := maintainFn(context.Background()); mErr != nil { fmt.Fprintf(os.Stderr, "reindex: %v\n", mErr) - return true, cleanup, mErr + return true, cleanup, "", mErr } } else { fmt.Fprintf(os.Stderr, "reindex: %v\n", rErr) - return true, cleanup, rErr + return true, cleanup, "", rErr } } // Post-reindex maintenance (WAL checkpoint + FTS5 optimize). @@ -130,15 +134,23 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd fmt.Fprintf(os.Stderr, "warning: post-reindex maintenance: %v\n", mErr) } fmt.Println("Done.") - return true, cleanup, nil + return true, cleanup, "", nil default: - fmt.Fprintf(os.Stderr, "unknown flag: %s\n", arg) - printUsage() - return true, cleanup, fmt.Errorf("unknown flag: %s", arg) + // A leading "-" marks an unknown flag, which is still an error. + // Anything else is collected as part of an initial search query. + if strings.HasPrefix(arg, "-") { + fmt.Fprintf(os.Stderr, "unknown flag: %s\n", arg) + printUsage() + return true, cleanup, "", fmt.Errorf("unknown flag: %s", arg) + } + queryParts = append(queryParts, arg) } } - return false, cleanup, nil + if len(queryParts) > 0 { + query = strings.Join(queryParts, " ") + } + return false, cleanup, query, nil } func runCompletion(w io.Writer, shell string) error { diff --git a/cmd/dispatch/cli_test.go b/cmd/dispatch/cli_test.go index 039dddd..b83aedb 100644 --- a/cmd/dispatch/cli_test.go +++ b/cmd/dispatch/cli_test.go @@ -23,7 +23,7 @@ func TestHandleArgs_Help(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil // no update available - done, cleanup, err := handleArgs([]string{"--help"}, io.Discard, ch) + done, cleanup, _, err := handleArgs([]string{"--help"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -39,7 +39,7 @@ func TestHandleArgs_HelpShort(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"-h"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"-h"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for -h; got done=%v, err=%v", done, err) } @@ -49,7 +49,7 @@ func TestHandleArgs_HelpCommand(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"help"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"help"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for help; got done=%v, err=%v", done, err) } @@ -59,7 +59,7 @@ func TestHandleArgs_Version(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"--version"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--version"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for --version; got done=%v, err=%v", done, err) } @@ -69,7 +69,7 @@ func TestHandleArgs_VersionShort(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"-v"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"-v"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for -v; got done=%v, err=%v", done, err) } @@ -79,7 +79,7 @@ func TestHandleArgs_VersionCommand(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"version"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"version"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for version; got done=%v, err=%v", done, err) } @@ -121,7 +121,7 @@ func TestRunCompletion_UnsupportedShell(t *testing.T) { func TestHandleArgs_Completion(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, cleanup, err := handleArgs([]string{"completion", "bash"}, io.Discard, ch) + done, cleanup, _, err := handleArgs([]string{"completion", "bash"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -136,7 +136,7 @@ func TestHandleArgs_Completion(t *testing.T) { func TestHandleArgs_CompletionMissingShell(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"completion"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"completion"}, io.Discard, ch) if err == nil { t.Fatal("expected error for missing shell") } @@ -234,7 +234,7 @@ func TestHandleArgs_DoctorJSON(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, _, err := handleArgs([]string{"doctor", "--json"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"doctor", "--json"}, io.Discard, ch) if err != nil || !done { t.Errorf("expected done=true, no error for doctor --json; got done=%v, err=%v", done, err) } @@ -244,7 +244,7 @@ func TestHandleArgs_Doctor(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil - done, cleanup, err := handleArgs([]string{"doctor"}, io.Discard, ch) + done, cleanup, _, err := handleArgs([]string{"doctor"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -259,7 +259,7 @@ func TestHandleArgs_Doctor(t *testing.T) { func TestHandleArgs_UnknownFlag(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--unknown"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--unknown"}, io.Discard, ch) if err == nil { t.Error("expected error for unknown flag") } @@ -274,7 +274,7 @@ func TestHandleArgs_UnknownFlag(t *testing.T) { func TestHandleArgs_NoArgs(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, cleanup, err := handleArgs(nil, io.Discard, ch) + done, cleanup, _, err := handleArgs(nil, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -286,10 +286,58 @@ func TestHandleArgs_NoArgs(t *testing.T) { } } +func TestHandleArgs_SingleQuery(t *testing.T) { + ch := make(chan *update.UpdateInfo, 1) + + done, cleanup, query, err := handleArgs([]string{"auth"}, io.Discard, ch) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if done { + t.Error("expected done=false for a search query") + } + if cleanup != nil { + t.Error("expected cleanup=nil for a search query") + } + if query != "auth" { + t.Errorf("query = %q, want %q", query, "auth") + } +} + +func TestHandleArgs_MultiWordQuery(t *testing.T) { + ch := make(chan *update.UpdateInfo, 1) + + done, _, query, err := handleArgs([]string{"fix", "auth", "bug"}, io.Discard, ch) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if done { + t.Error("expected done=false for a multi-word query") + } + if query != "fix auth bug" { + t.Errorf("query = %q, want %q", query, "fix auth bug") + } +} + +func TestHandleArgs_QueryDoesNotShadowSubcommands(t *testing.T) { + ch := make(chan *update.UpdateInfo, 1) + ch <- nil + + // A known subcommand must still short-circuit even though it is a bare + // non-flag token that could otherwise look like a query. + done, _, query, err := handleArgs([]string{"version"}, io.Discard, ch) + if err != nil || !done { + t.Errorf("expected done=true, no error for version; got done=%v, err=%v", done, err) + } + if query != "" { + t.Errorf("query should be empty for a subcommand, got %q", query) + } +} + func TestHandleArgs_ClearCache(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--clear-cache"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--clear-cache"}, io.Discard, ch) // config.Reset may succeed or fail depending on environment. // Either way, done should be true. if !done { @@ -322,7 +370,7 @@ func TestHandleArgs_DemoFromRepoRoot(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, cleanup, err := handleArgs([]string{"--demo"}, io.Discard, ch) + done, cleanup, _, err := handleArgs([]string{"--demo"}, io.Discard, ch) if err != nil { t.Fatalf("--demo from repo root should succeed: %v", err) } @@ -344,7 +392,7 @@ func TestHandleArgs_DemoNotFound(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--demo"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--demo"}, io.Discard, ch) if err == nil { t.Error("expected error when demo DB not found") } @@ -360,7 +408,7 @@ func TestHandleArgs_HelpWithUpdate(t *testing.T) { LatestVersion: "2.0.0", } - done, _, err := handleArgs([]string{"--help"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--help"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -376,7 +424,7 @@ func TestHandleArgs_VersionWithUpdate(t *testing.T) { LatestVersion: "2.0.0", } - done, _, err := handleArgs([]string{"--version"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--version"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -400,7 +448,7 @@ func TestHandleArgs_Reindex_CopilotNotFound(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -419,7 +467,7 @@ func TestHandleArgs_Reindex_OtherError(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) if err == nil { t.Error("expected error for non-CopilotNotFound reindex failure") } @@ -443,7 +491,7 @@ func TestHandleArgs_Reindex_MaintainError(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) if err == nil { t.Error("expected error when maintain fails") } @@ -468,7 +516,7 @@ func TestHandleArgs_Reindex_Success(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -484,7 +532,7 @@ func TestHandleArgs_UpdateSuccess(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"update"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"update"}, io.Discard, ch) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -500,7 +548,7 @@ func TestHandleArgs_UpdateError(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"update"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"update"}, io.Discard, ch) if err == nil { t.Error("expected error for failed update") } @@ -516,7 +564,7 @@ func TestHandleArgs_ClearCacheError(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--clear-cache"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--clear-cache"}, io.Discard, ch) if err == nil { t.Error("expected error for failed config reset") } @@ -550,7 +598,7 @@ func TestHandleArgs_Reindex_PostMaintainWarning(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) - done, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"--reindex"}, io.Discard, ch) // The post-reindex maintain warning is non-fatal, so handleArgs should // still succeed. if err != nil { diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index 8da48dd..d49f766 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -33,7 +33,7 @@ func main() { defer origStderr.Close() //nolint:errcheck // best-effort cleanup } - done, demoCleanup, err := handleArgs(os.Args[1:], origStderr, updateCh) + done, demoCleanup, initialQuery, err := handleArgs(os.Args[1:], origStderr, updateCh) if demoCleanup != nil { defer demoCleanup() } @@ -67,7 +67,7 @@ func main() { } p := tea.NewProgram( - tui.NewModel(), + tui.NewModelWithQuery(initialQuery), ) if _, err := p.Run(); err != nil { @@ -85,6 +85,7 @@ func printUsage() { Usage: dispatch Launch the interactive TUI + dispatch [query] Launch the TUI with the search box pre-filled dispatch [command] Commands: diff --git a/cmd/dispatch/open_test.go b/cmd/dispatch/open_test.go index 95b3399..b9daa1a 100644 --- a/cmd/dispatch/open_test.go +++ b/cmd/dispatch/open_test.go @@ -174,7 +174,7 @@ func TestHandleArgs_OpenMissingID(t *testing.T) { ch := make(chan *update.UpdateInfo, 1) ch <- nil // parseOpenArgs fails before touching config or the store, so this is safe. - done, _, err := handleArgs([]string{"open"}, io.Discard, ch) + done, _, _, err := handleArgs([]string{"open"}, io.Discard, ch) if !done { t.Error("expected done=true for open") } diff --git a/cmd/dispatch/stats_test.go b/cmd/dispatch/stats_test.go index b27548d..e388f4a 100644 --- a/cmd/dispatch/stats_test.go +++ b/cmd/dispatch/stats_test.go @@ -209,7 +209,7 @@ func TestHandleArgsStats(t *testing.T) { withStatsList(t, func(data.FilterOptions) ([]data.Session, error) { return sampleSessions(), nil }) - done, _, err := handleArgs([]string{"stats", "--json"}, &bytes.Buffer{}, nil) + done, _, _, err := handleArgs([]string{"stats", "--json"}, &bytes.Buffer{}, nil) if !done { t.Error("expected done=true for stats") } diff --git a/internal/tui/handlers.go b/internal/tui/handlers.go index 16fde39..cd39011 100644 --- a/internal/tui/handlers.go +++ b/internal/tui/handlers.go @@ -61,8 +61,16 @@ func (m Model) handleSpinnerTick(msg spinner.TickMsg) (Model, tea.Cmd) { func (m Model) handleStoreOpened(msg storeOpenedMsg) (Model, tea.Cmd) { m.store = msg.store m.state = stateSessionList + // Apply a command-line search query before building the load command so + // the first load is already filtered. + var extra []tea.Cmd + if m.initialQuery != "" { + extra = m.applyInitialQuery(m.initialQuery) + m.initialQuery = "" + } // Quick scan first (lock files only), then full scan follows. - return m, tea.Batch(m.loadSessionsCmd(), m.scanAttentionQuickCmd()) + cmds := append([]tea.Cmd{m.loadSessionsCmd(), m.scanAttentionQuickCmd()}, extra...) + return m, tea.Batch(cmds...) } func (m Model) handleStoreError(msg storeErrorMsg) (Model, tea.Cmd) { //nolint:unparam diff --git a/internal/tui/model.go b/internal/tui/model.go index a1f3517..8c6267f 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -344,6 +344,10 @@ type Model struct { workStatus workStatusState searchFilter SearchFilter // structured tokens parsed from search bar input + // initialQuery is a search string passed on the command line. It is + // applied once, when the session store first opens, then cleared. + initialQuery string + // Launch mode requested when showing the shell picker. pendingLaunchMode string @@ -497,8 +501,34 @@ func NewModel() Model { return m } -// resolveTheme applies a user-chosen color scheme. -// +// NewModelWithQuery creates the root Model and seeds an initial search query +// that is applied once the session store opens. An empty query behaves +// exactly like NewModel. +func NewModelWithQuery(query string) Model { + m := NewModel() + m.initialQuery = query + return m +} + +// applyInitialQuery seeds the search bar with a command-line query and puts +// the model into the same search state as if the user had typed it: the +// search bar is focused and populated, structured tokens are parsed, and a +// quick search runs immediately with a deep search scheduled to follow. It +// returns the commands the caller should run (focus blink and the deep-search +// timer). +func (m *Model) applyInitialQuery(query string) []tea.Cmd { + focusCmd := m.searchBar.Focus() + m.searchBar.SetValue(query) + m.search.lastRawInput = query + m.searchFilter = ParseSearchTokens(query) + m.applySearchTokens() + m.filter.DeepSearch = false + m.search.deepSearchVersion++ + m.search.deepSearchPending = true + m.searchBar.SetSearching(true) + return []tea.Cmd{focusCmd, m.scheduleDeepSearch(m.search.deepSearchVersion)} +} + // When the config field is empty or "auto" we keep the legacy // defaults from styles.init(). The correct light/dark variant // is applied later when tea.BackgroundColorMsg is received diff --git a/internal/tui/model_search_test.go b/internal/tui/model_search_test.go index dc1390d..e6a5a7e 100644 --- a/internal/tui/model_search_test.go +++ b/internal/tui/model_search_test.go @@ -46,6 +46,65 @@ func enterKeyMsg() tea.KeyPressMsg { // --- Tests ------------------------------------------------------------------- +func TestApplyInitialQuerySeedsSearchState(t *testing.T) { + m := newTestModel() + + cmds := m.applyInitialQuery("repo:dispatch auth") + + if got := m.searchBar.Value(); got != "repo:dispatch auth" { + t.Errorf("search bar value = %q, want %q", got, "repo:dispatch auth") + } + if !m.searchBar.Focused() { + t.Error("search bar should be focused after applying an initial query") + } + if m.filter.Query != "auth" { + t.Errorf("filter.Query = %q, want %q", m.filter.Query, "auth") + } + if m.filter.Repository != "dispatch" { + t.Errorf("filter.Repository = %q, want %q", m.filter.Repository, "dispatch") + } + if !m.search.deepSearchPending { + t.Error("deep search should be pending after applying an initial query") + } + if len(cmds) == 0 { + t.Error("applyInitialQuery should return commands (focus + deep-search timer)") + } +} + +func TestNewModelWithQuerySetsInitialQuery(t *testing.T) { + m := NewModelWithQuery("hello world") + defer m.closeStore() + + if m.initialQuery != "hello world" { + t.Errorf("initialQuery = %q, want %q", m.initialQuery, "hello world") + } +} + +func TestStoreOpenedAppliesInitialQuery(t *testing.T) { + m := newTestModel() + m.state = stateLoading + m.initialQuery = "seattle" + + result, cmd := m.Update(storeOpenedMsg{store: nil}) + rm := result.(Model) + + if rm.state != stateSessionList { + t.Errorf("state = %v, want stateSessionList", rm.state) + } + if rm.filter.Query != "seattle" { + t.Errorf("filter.Query = %q, want %q", rm.filter.Query, "seattle") + } + if rm.initialQuery != "" { + t.Errorf("initialQuery should be cleared after applying, got %q", rm.initialQuery) + } + if !rm.searchBar.Focused() { + t.Error("search bar should be focused after an initial query is applied") + } + if cmd == nil { + t.Error("storeOpenedMsg with an initial query should return commands") + } +} + func TestEscapeFromSearchPreservesQuery(t *testing.T) { m := newTestModel()