Search: operators as autocompleted pills and natural dates #61

Closed
opened 2026-09-24 15:53:02 +00:00 by kayg · 6 comments
Owner

Owner decision S5 (DESIGN §32, "this is awesome"): operators #tag, type:photo|note|file|event|task|log|bookmark, date:, in:<folder>, is:todo|done|shared|favorite, from:/with:<person>, ext:pdf, size:>10mb, has:photo|file|note, and natural dates (last week, in march, 2025, yesterday). Operators chain; plain words still search. In the field, typing an operator opens an autocomplete list and turns into a removable pill; pills are keyboard-editable (Backspace removes, arrows move). Parser shared by server and client (one grammar, tests with many cursed inputs).

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js docs/search.md (read-only at /home/kayg/Developer/calternal.js) — its palette UX, > command mode, ranking weights, a11y combobox pattern and recents carry over.
  • Existing code: crates/calternal-search (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web.
  • Owner rules: search must be ultra fast (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work.
  • Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Owner decision S5 (DESIGN §32, "this is awesome"): operators `#tag`, `type:photo|note|file|event|task|log|bookmark`, `date:`, `in:<folder>`, `is:todo|done|shared|favorite`, `from:`/`with:<person>`, `ext:pdf`, `size:>10mb`, `has:photo|file|note`, and natural dates (`last week`, `in march`, `2025`, `yesterday`). Operators chain; plain words still search. In the field, typing an operator opens an **autocomplete list** and turns into a **removable pill**; pills are keyboard-editable (Backspace removes, arrows move). Parser shared by server and client (one grammar, tests with many cursed inputs). ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js `docs/search.md` (read-only at /home/kayg/Developer/calternal.js) — its palette UX, `>` command mode, ranking weights, a11y combobox pattern and recents carry over. - Existing code: `crates/calternal-search` (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web. - Owner rules: search must be **ultra fast** (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work. - Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Author
Owner

Starting the search backend job for issues #58, #61, and #62. Branch: job/search-backend; base SHA: 55a2752a4c4c7a087fce55868871a8576fea31a7. I am reading the current index, plugin interfaces, and API contract before implementation.

Starting the search backend job for issues #58, #61, and #62. Branch: `job/search-backend`; base SHA: `55a2752a4c4c7a087fce55868871a8576fea31a7`. I am reading the current index, plugin interfaces, and API contract before implementation.
Author
Owner

The shared parser test measured search parser latency: p50=27 us p95=45 us (1000 parses) on this worktree. It accepts the issue grammar and returns the API DTO from GET /api/v1/search/parse; quoted values and malformed operator values have parser tests. Full workspace and live-server gates are still pending. The 16 ms first-keystroke target belongs to the client provider and is outside this backend job’s file ownership.

The shared parser test measured `search parser latency: p50=27 us p95=45 us (1000 parses)` on this worktree. It accepts the issue grammar and returns the API DTO from `GET /api/v1/search/parse`; quoted values and malformed operator values have parser tests. Full workspace and live-server gates are still pending. The 16 ms first-keystroke target belongs to the client provider and is outside this backend job’s file ownership.
Author
Owner

Finished on branch job/search-backend at head 9e3cb6ed6b677a0af990955821f4aa96b24b3866.

Added the shared bounded search grammar and cross-client vectors for tag, type, date, folder, state, people, extension, size, and content-kind filters. The API exposes parsed terms and filters at /api/v1/search/parse; the API client contract is generated and checked. Parser latency measured search parser latency: p50=16 us p95=32 us (1000 parses).

The search UI autocomplete and removable-pill interaction remain for the web UI job. Relative dates resolve against UTC in the backend because the API has no user time-zone setting.

Gates: cargo fmt --check passed (no output); cargo clippy --all-targets -- -D warnings passed (Finished dev profile [unoptimized + debuginfo] target(s) in 6.29s); cargo test --workspace passed; bash packages/api-client/check-generated.sh passed; bash tests/adversarial/run.sh passed with ==== FINDINGS 0.

Finished on branch `job/search-backend` at head `9e3cb6ed6b677a0af990955821f4aa96b24b3866`. Added the shared bounded search grammar and cross-client vectors for tag, type, date, folder, state, people, extension, size, and content-kind filters. The API exposes parsed terms and filters at `/api/v1/search/parse`; the API client contract is generated and checked. Parser latency measured `search parser latency: p50=16 us p95=32 us (1000 parses)`. The search UI autocomplete and removable-pill interaction remain for the web UI job. Relative dates resolve against UTC in the backend because the API has no user time-zone setting. Gates: `cargo fmt --check` passed (no output); `cargo clippy --all-targets -- -D warnings` passed (`Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.29s`); `cargo test --workspace` passed; `bash packages/api-client/check-generated.sh` passed; `bash tests/adversarial/run.sh` passed with `==== FINDINGS 0`.
Author
Owner

Starting the search window UI job for #57, #61, #63 and #64 (one job, one worktree). Branch: job/search-ui; base SHA: 8c216759f01d704e69ecf37ef8293f422abf5552.

Plan: extend the existing ⌘K dialog and registry in apps/web (no second palette) into one floating search window with pills, previews and an in-place expanded view; add Files ⌘F and Calendar contextual search; add a saved-search CRUD API (JSON files through calternal-fs) and smart folders. I will post findings and a finish comment here.

Starting the search window UI job for #57, #61, #63 and #64 (one job, one worktree). Branch: `job/search-ui`; base SHA: `8c216759f01d704e69ecf37ef8293f422abf5552`. Plan: extend the existing ⌘K dialog and registry in apps/web (no second palette) into one floating search window with pills, previews and an in-place expanded view; add Files ⌘F and Calendar contextual search; add a saved-search CRUD API (JSON files through `calternal-fs`) and smart folders. I will post findings and a finish comment here.
Author
Owner

Finished the UI half of #61 on branch job/search-ui at a8d2f644b41c0ebdd2eeff2bf4cfca6c6a3cfa5c (not pushed, not merged).

  • Typing #… or op:… opens an autocomplete list with real values: the user's tags, the Home's folders (two levels), the instance's people, and the grammar's fixed values (type, is, has, date phrases, ext, size). ↑/↓ move, ↩ or ⇥ accept, esc closes only the list. A word that starts an operator name (ty → type:) offers the operator, but that row is not pre-selected, so ↩ still searches.
  • A complete operator followed by a space becomes a pill. Natural dates that the server reads as filters (today, 2025, last week, in march) become date pills too, so the field shows what the server will do.
  • Pills: ⌫ at the start of the text removes the last pill; ← walks into the pills, ⌫ or Delete removes the selected one, ↩ puts it back into the text for editing. Each pill also has a remove button. Screen readers hear every add, remove and selection.
  • The raw query string is the only query state (URL, saved searches, server). apps/web/src/lib/search/query.ts mirrors the server lexer (quotes, escapes) and parse_filter to decide what becomes a pill; GET /api/v1/search/parse stays the authority. Its error shows under the field in plain words ("One filter has a value that search does not know. Pick one from the list."). An operator that is still being typed (type:no, in:) is held back from the query, so the results and the error do not flicker while typing.
  • Tests: query.test.ts (lexer, pills, natural dates, validation, suggestions, pending operators) and the e2e pill steps.
Finished the UI half of #61 on branch `job/search-ui` at `a8d2f644b41c0ebdd2eeff2bf4cfca6c6a3cfa5c` (not pushed, not merged). - Typing `#…` or `op:…` opens an autocomplete list with real values: the user's tags, the Home's folders (two levels), the instance's people, and the grammar's fixed values (type, is, has, date phrases, ext, size). ↑/↓ move, ↩ or ⇥ accept, esc closes only the list. A word that starts an operator name (`ty` → `type:`) offers the operator, but that row is not pre-selected, so ↩ still searches. - A complete operator followed by a space becomes a pill. Natural dates that the server reads as filters (`today`, `2025`, `last week`, `in march`) become date pills too, so the field shows what the server will do. - Pills: ⌫ at the start of the text removes the last pill; ← walks into the pills, ⌫ or Delete removes the selected one, ↩ puts it back into the text for editing. Each pill also has a remove button. Screen readers hear every add, remove and selection. - The raw query string is the only query state (URL, saved searches, server). `apps/web/src/lib/search/query.ts` mirrors the server lexer (quotes, escapes) and `parse_filter` to decide what becomes a pill; `GET /api/v1/search/parse` stays the authority. Its error shows under the field in plain words ("One filter has a value that search does not know. Pick one from the list."). An operator that is still being typed (`type:no`, `in:`) is held back from the query, so the results and the error do not flicker while typing. - Tests: `query.test.ts` (lexer, pills, natural dates, validation, suggestions, pending operators) and the e2e pill steps.
Author
Owner

Completed on dev in 717fe20efb (Merge job/search-ui: floating search window, pills, previews, saved searches, contextual search (#57, #61, #63, #64)).

Completed on dev in 717fe20efbb647e9754adfb86b5ddd7875b60f73 (Merge job/search-ui: floating search window, pills, previews, saved searches, contextual search (#57, #61, #63, #64)).
kayg closed this issue 2026-10-01 05:08:55 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#61
No description provided.