Skip to content

Commit 5c35d9e

Browse files
committed
Search: David's fix-up round 2, part A (frontend, TDD)
Dialog states made honest: the result list itself shows the spinner plus "Searching..." label while a search is in flight, and lays out "No files match these criteria:" with the active filter set when nothing matches; the status bar stops duplicating either message. Recent-searches strip gains a "Recent searches:" label so it's clear what these are. The two right-edge footer buttons (Go to file, Show all in main window) are always rendered now and disable when their preconditions don't hold, so the layout doesn't shift while typing. ⏎ ownership swaps dynamically: a new `lastDialogEvent` discriminator drives a `deriveEnterAction` helper that decides whether Enter runs a search (after a query or filter edit, or when no results are showing) or opens the cursor result (after results arrive or the cursor moves). The bar reads "Search ⏎" only when Enter runs a search; the footer's Go to file reads "Go to file ⏎" only when Enter opens; exactly one shows the hint at any moment. Shortcut allocation finalized: ⌥A / ⌥F / ⌥R switch mode chips globally inside the dialog; ⌥⏎ shows all in the main window; ⌥C / ⌥V (popover-scoped) replace the old global ⌥F / ⌥D for "Use current folder" / "All folders" inside Search in. The stale global ⌥F/⌥D scope handlers are removed. 22 new Vitest cases (deriveEnterAction's 8-permutation table, search-states content vs status-bar split, recent-searches label, always-on footer buttons) plus 3 updated test files for the new contracts. All 320 search tests green. Still pending in this fix-up: filter list-style popovers (Size, Modified), inline shortcut hint pills on mode chips, header-column alignment in the result list, ResizeObserver-driven path-pill width measurement, "Use current folder" smart fallback via nav history, snapshot-pane keyboard nav (PgUp/PgDn/Home/End/Space/⇧↑↓/⌘A), and context-menu fixes in the snapshot pane (Show in Finder, Open, Open with, Copy path, Copy filename label and payload).
1 parent 3ea1b45 commit 5c35d9e

17 files changed

Lines changed: 930 additions & 127 deletions

apps/desktop/src/lib/search/CLAUDE.md

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,55 @@ There is **no `aiPrompt` state and no `namePattern` state**. M2 deleted both. An
8080

8181
## Keyboard shortcuts (in-dialog, hard-coded)
8282

83-
| Shortcut | Action |
84-
| --------- | ------------------------------------------------------------------- |
85-
| `Enter` | Run search in the active mode (AI in AI mode, manual otherwise) |
86-
| `⌘Enter` | Run AI search regardless of active mode (only when AI is enabled) |
87-
| `⌘N` | Clear all dialog state ("new search") |
88-
| `⌘H` | Toggle the recent-searches popover (fuzzy over the full history) |
89-
| `⌘1` | Switch to AI (AI on) or Filename (AI off) |
90-
| `⌘2` | Switch to Filename (AI on) or Regex (AI off) |
91-
| `⌘3` | Switch to Regex (AI on); no-op when AI is off |
92-
| `⌘4` | Reserved for Content when it ships; not wired now |
93-
| `⌥F` | Set scope to the focused pane's current directory |
94-
| `⌥D` | Clear the scope (search the whole drive) |
95-
| `⌥A` | Show all results in the main window (snapshot opens in active pane) |
96-
| `⌥←` | Navigate the active pane to the cursor row's parent folder |
97-
| `⌥→` | Navigate the active pane to the cursor row's path (descend back) |
98-
| `` / `` | Move the cursor through the results list (loops top<->bottom) |
99-
| `` / `` | When focus is on a mode chip: move between chips (skip Content) |
100-
| `Tab` | Trapped within the dialog; cycles through interactive elements |
101-
| `Escape` | Close the dialog |
83+
Final round-2 allocation. ⏎ has dynamic ownership (see D8 below).
84+
85+
| Shortcut | Action |
86+
| --------- | ----------------------------------------------------------------- |
87+
| `Enter` | Dispatched via `enterAction`: "go-to-file" or "run-search" (D8) |
88+
| `⌥⏎` | Show all results in the main window (replaces round-1's ⌥A) |
89+
| `⌘Enter` | Run AI search regardless of active mode (only when AI is enabled) |
90+
| `⌘N` | Clear all dialog state ("new search") |
91+
| `⌘H` | Toggle the recent-searches popover (fuzzy over the full history) |
92+
| `⌘1` | Switch to AI (AI on) or Filename (AI off) |
93+
| `⌘2` | Switch to Filename (AI on) or Regex (AI off) |
94+
| `⌘3` | Switch to Regex (AI on); no-op when AI is off |
95+
| `⌘4` | Reserved for Content when it ships; not wired now |
96+
| `⌥A` | Mode chip: AI (global inside the dialog; only when AI is enabled) |
97+
| `⌥F` | Mode chip: Filename (global) |
98+
| `⌥R` | Mode chip: Regex (global) |
99+
| `⌥C` | Inside Search-in popover only: Use current folder |
100+
| `⌥V` | Inside Search-in popover only: All folders |
101+
| `⌥←` | Navigate the active pane to the cursor row's parent folder |
102+
| `⌥→` | Navigate the active pane to the cursor row's path (descend back) |
103+
| `` / `` | Move the cursor through the results list (loops top<->bottom) |
104+
| `` / `` | When focus is on a mode chip: move between chips (skip Content) |
105+
| `Tab` | Trapped within the dialog; cycles through interactive elements |
106+
| `Escape` | Close the dialog |
107+
108+
### Round 2 D8: `` ownership swap
109+
110+
`search-state.svelte.ts` carries `lastDialogEvent: LastDialogEvent` (one of `opened`, `results-arrived`, `cursor-moved`,
111+
`query-edited`, `filter-edited`). The pure helper `deriveEnterAction({ lastEvent, resultsCount })` returns
112+
`'go-to-file' | 'run-search'`:
113+
114+
- `'go-to-file'` when there are results AND the last event was `results-arrived` or `cursor-moved` (the user just got a
115+
list back or is browsing it). Pressing ⏎ opens the cursor row in the active pane.
116+
- `'run-search'` otherwise (zero results, freshly opened, query/filter just edited). Pressing ⏎ runs the search.
117+
118+
The bar's Search button reads `Search ⏎` only when `enterAction === 'run-search'`; the footer's `Go to file` button
119+
reads `Go to file ⏎` only when `enterAction === 'go-to-file'`. Exactly one of them surfaces the hint at any time. Tests
120+
in `enter-action.test.ts` pin the eight-permutation table.
121+
122+
### Round 2 D9: scope shortcuts moved inside the popover
123+
124+
Round 1's global `⌥F` / `⌥D` are gone. `⌥F` is now the Filename mode chip globally. The scope actions live as `⌥C` (Use
125+
current folder) and `⌥V` (All folders), active ONLY while the Search-in popover is open. They're wired via a top-level
126+
`<svelte:window>` in `SearchFilterChips.svelte` that gates on `openChip === 'scope'`.
127+
128+
### Round 2 D6: footer buttons always visible
129+
130+
Both `Go to file` and `Show all in main window` render unconditionally; when there are no results (or the index isn't
131+
ready) they render disabled instead of hidden, so the layout stays still while the user types.
102132

103133
The Content chip is visible-disabled with a "Coming soon" tooltip. It has **no** shortcut. Wiring a shortcut to a
104134
disabled control is hostile UX (either silent no-op or a popup on every press); reserving `⌘4` is the better contract.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* D5: the recent-searches strip carries a "Recent searches:" label prefix so
3+
* the user can read what those chips are without needing context.
4+
*/
5+
import { describe, expect, it } from 'vitest'
6+
import { mount, tick } from 'svelte'
7+
import RecentSearchesFooter from './RecentSearchesFooter.svelte'
8+
import type { HistoryEntry } from '$lib/tauri-commands'
9+
10+
function entry(query: string): HistoryEntry {
11+
return {
12+
id: query,
13+
timestamp: 0,
14+
mode: 'filename',
15+
query,
16+
filters: {},
17+
scope: '',
18+
caseSensitive: false,
19+
excludeSystemDirs: true,
20+
resultCount: 0,
21+
} as HistoryEntry
22+
}
23+
24+
describe('RecentSearchesFooter D5: label', () => {
25+
it('renders a "Recent searches:" label when there are entries', async () => {
26+
const target = document.createElement('div')
27+
document.body.appendChild(target)
28+
mount(RecentSearchesFooter, {
29+
target,
30+
props: {
31+
entries: [entry('*.pdf'), entry('*.jpg')],
32+
disabled: false,
33+
onPick: () => {},
34+
onRemove: () => {},
35+
onOpenAll: () => {},
36+
},
37+
})
38+
await tick()
39+
expect(target.textContent ?? '').toContain('Recent searches:')
40+
})
41+
})

apps/desktop/src/lib/search/RecentSearchesFooter.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040

4141
{#if visible.length > 0}
4242
<div class="recent-footer" role="region" aria-label="Recent searches">
43+
<!-- D5: explicit label so the user can read what these chips are. -->
44+
<span class="recent-label">Recent searches:</span>
4345
{#each visible as entry (entry.id)}
4446
<button
4547
type="button"
@@ -84,6 +86,14 @@
8486
scrollbar-width: thin;
8587
}
8688
89+
/* D5: leading label so the user reads the strip as "Recent searches: …". */
90+
.recent-label {
91+
font-size: var(--font-size-sm);
92+
color: var(--color-text-tertiary);
93+
white-space: nowrap;
94+
margin-right: var(--spacing-xxs);
95+
}
96+
8797
.recent-chip,
8898
.all-searches {
8999
display: inline-flex;

apps/desktop/src/lib/search/SearchBar.svelte

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
aiHighlight: boolean
3333
/** True when the bar should show the "Press Enter to search" hint. Owned by the parent. */
3434
showRunHint?: boolean
35+
/**
36+
* D8: when true, the Search button surfaces the `⏎` shortcut hint. The dialog
37+
* owns the ⏎ ownership swap; when this is false, the hint moves to the
38+
* footer's "Go to file" button.
39+
*/
40+
showEnterHint?: boolean
3541
onInput: (value: string) => void
3642
/** Click handler for the ⏎ run button. Equivalent to pressing Enter in the input. */
3743
onRun: () => void
@@ -49,6 +55,7 @@
4955
disabled,
5056
aiHighlight,
5157
showRunHint = false,
58+
showEnterHint = true,
5259
onInput,
5360
onRun,
5461
onCompositionStart,
@@ -103,6 +110,8 @@
103110
{#if showRunHint}
104111
<span class="run-hint" aria-hidden="true">Press Enter to search</span>
105112
{/if}
113+
<!-- D8: button reads "Search ⏎" when ⏎ owns the run action; just "Search" when
114+
the footer's Go-to-file owns ⏎. Exactly one of the two surfaces the hint. -->
106115
<button
107116
type="button"
108117
class="run-button"
@@ -112,6 +121,8 @@
112121
aria-label={runTitle}
113122
>
114123
<IconCornerDownLeft />
124+
<span class="run-label">Search</span>
125+
{#if showEnterHint}<span class="run-enter-hint" aria-hidden="true">⏎</span>{/if}
115126
</button>
116127
</div>
117128

@@ -166,14 +177,27 @@
166177
flex-shrink: 0;
167178
display: inline-flex;
168179
align-items: center;
180+
gap: var(--spacing-xxs);
169181
justify-content: center;
170-
padding: var(--spacing-xxs) var(--spacing-xs);
182+
padding: var(--spacing-xxs) var(--spacing-sm);
171183
background: transparent;
172184
border: 1px solid var(--color-border-subtle);
173185
border-radius: var(--radius-sm);
174186
color: var(--color-text-secondary);
175187
cursor: default;
176188
line-height: 1;
189+
font-size: var(--font-size-sm);
190+
}
191+
192+
.run-label {
193+
line-height: 1;
194+
}
195+
196+
.run-enter-hint {
197+
font-family: var(--font-mono);
198+
font-size: var(--font-size-xs);
199+
color: var(--color-text-tertiary);
200+
opacity: 0.8;
177201
}
178202
179203
.run-button:hover:not(:disabled) {

0 commit comments

Comments
 (0)