Notes editor: link picker with [[ and @, and insert link from Search #1379

Closed
opened 2026-10-07 15:08:03 +00:00 by kayg · 46 comments
Owner

Owner request 2026-10-07 ("please build them now, they're essential for a personal app"; overrides the bug-fix-only period for this feature). Owner decisions:

  1. Triggers: typing [[ or @ in the block editor opens an inline link picker at the caret. It searches ALL items through the existing Search index/API: Notes, files, Tasks, Events, Log entries, transactions, photos (whatever Search returns), grouped by kind with icons, keyboard first (arrows, Enter, Tab, Escape), filtered as the User types, results instant (cached Search, <= 100 ms per keystroke, DESIGN §59).
  2. Inserted form: a standard Markdown link [Title](stable link). Notes and files: the relative path link per the existing link rules (DESIGN §17; rewritten on rename by the existing referrer rewrite). Tasks, Events, Log entries, transactions, photos: the stable ID deep link (DESIGN §33). Renders as a link with the item's kind icon; Cmd/Ctrl+click opens it. Escape or no selection leaves the typed text literal. The existing [[Exact Title]] save-time conversion keeps working.
  3. From Search: in the editor, Cmd+Shift+K (shown in the warm tooltip and the shortcut list) opens the full Search window in insert-link mode; Enter inserts [Title](link) at the saved cursor position (or replaces the selection, using the selected text as the link title if the User chose that) and returns focus to the editor. When normal Cmd+K Search was opened from a focused Note, results also offer "Insert link in note" (in the Actions menu and as Cmd+Enter or the next free shortcut).
Owner request 2026-10-07 ("please build them now, they're essential for a personal app"; overrides the bug-fix-only period for this feature). Owner decisions: 1. Triggers: typing `[[` or `@` in the block editor opens an inline link picker at the caret. It searches ALL items through the existing Search index/API: Notes, files, Tasks, Events, Log entries, transactions, photos (whatever Search returns), grouped by kind with icons, keyboard first (arrows, Enter, Tab, Escape), filtered as the User types, results instant (cached Search, <= 100 ms per keystroke, DESIGN §59). 2. Inserted form: a standard Markdown link `[Title](stable link)`. Notes and files: the relative path link per the existing link rules (DESIGN §17; rewritten on rename by the existing referrer rewrite). Tasks, Events, Log entries, transactions, photos: the stable ID deep link (DESIGN §33). Renders as a link with the item's kind icon; Cmd/Ctrl+click opens it. Escape or no selection leaves the typed text literal. The existing `[[Exact Title]]` save-time conversion keeps working. 3. From Search: in the editor, Cmd+Shift+K (shown in the warm tooltip and the shortcut list) opens the full Search window in insert-link mode; Enter inserts `[Title](link)` at the saved cursor position (or replaces the selection, using the selected text as the link title if the User chose that) and returns focus to the editor. When normal Cmd+K Search was opened from a focused Note, results also offer "Insert link in note" (in the Actions menu and as Cmd+Enter or the next free shortcut).
Author
Owner

Started #1379 on branch job/linkpick-1379 from base 0419170fe456b58614befcb12d9c9d255118f1c0.

I am tracing the shared editor suggestion, Search client/cache, stable-link helpers, Search dialog, and shortcut registry before making the implementation. I will add focused coverage and run the requested production build, web check, and web tests. No pushes or deploys.

Started #1379 on branch `job/linkpick-1379` from base `0419170fe456b58614befcb12d9c9d255118f1c0`. I am tracing the shared editor suggestion, Search client/cache, stable-link helpers, Search dialog, and shortcut registry before making the implementation. I will add focused coverage and run the requested production build, web check, and web tests. No pushes or deploys.
Author
Owner

Owner addition 2026-10-07: from the normal Cmd+K Search window, the result's Actions menu (the "Actions ⌘K" list) offers, each with a shortcut shown in the menu and in the warm tooltip:

  • "Insert link in note": inserts [Title](stable link) at the current cursor of the Note that was focused before Search opened (or replaces its selection); returns focus to that Note. Shown only when a Note editor had focus. Suggested shortcut: ⌥↩ (Option+Enter).
  • "Copy Markdown link": copies [Title](stable link) to the clipboard, available for every result anywhere (not only from a Note). Suggested shortcut: ⇧⌘C.
  • The existing "Copy link" (URL only) stays.
    Check the shortcut registry for conflicts and pick free ones if the suggestions clash; list them in Settings > Shortcuts. The same link format rules apply as above (relative path for Notes/files, stable ID link for other items, one shared helper).
Owner addition 2026-10-07: from the normal Cmd+K Search window, the result's Actions menu (the "Actions ⌘K" list) offers, each with a shortcut shown in the menu and in the warm tooltip: - "Insert link in note": inserts `[Title](stable link)` at the current cursor of the Note that was focused before Search opened (or replaces its selection); returns focus to that Note. Shown only when a Note editor had focus. Suggested shortcut: ⌥↩ (Option+Enter). - "Copy Markdown link": copies `[Title](stable link)` to the clipboard, available for every result anywhere (not only from a Note). Suggested shortcut: ⇧⌘C. - The existing "Copy link" (URL only) stays. Check the shortcut registry for conflicts and pick free ones if the suggestions clash; list them in Settings > Shortcuts. The same link format rules apply as above (relative path for Notes/files, stable ID link for other items, one shared helper).
Author
Owner

The first web check found an exact perf-lint pin for Search's existing ResizeObserver in search-dialog.svelte was stale after the module changed for insert-link mode. The exception count is unchanged. I am refreshing the pin to the current syntax hash and will rerun the focused guard before final gates.

The first web check found an exact perf-lint pin for Search's existing `ResizeObserver` in `search-dialog.svelte` was stale after the module changed for insert-link mode. The exception count is unchanged. I am refreshing the pin to the current syntax hash and will rerun the focused guard before final gates.
Author
Owner

Source audit: the current Search kind table had no transaction kind, and crates/plugins/money has no search_providers implementation (the existing hooks are in Files, Notes, Calendar, Photos and Mail). I added client handling for a transaction result with its stable /money/.../transactions/<id> link if Search returns one. The current Search API cannot produce or seed that result, so I am covering the currently available providers and will report this API coverage gap.

Source audit: the current Search kind table had no `transaction` kind, and `crates/plugins/money` has no `search_providers` implementation (the existing hooks are in Files, Notes, Calendar, Photos and Mail). I added client handling for a transaction result with its stable `/money/.../transactions/<id>` link if Search returns one. The current Search API cannot produce or seed that result, so I am covering the currently available providers and will report this API coverage gap.
Author
Owner

Production E2E finding: apps/web/e2e/linkpick-1379.mjs timed out after 30 seconds waiting for inline option Linkpick log 1379 at pickInline (after Note and Task picks had succeeded). The setup had already confirmed the seeded Log through the server Search API. I am adding bounded failure diagnostics to identify whether the live Search client filters the result or the editor trigger state is stale, then I will rerun the production flow.

Production E2E finding: `apps/web/e2e/linkpick-1379.mjs` timed out after 30 seconds waiting for inline option `Linkpick log 1379` at `pickInline` (after Note and Task picks had succeeded). The setup had already confirmed the seeded Log through the server Search API. I am adding bounded failure diagnostics to identify whether the live Search client filters the result or the editor trigger state is stale, then I will rerun the production flow.
Author
Owner

Production E2E finding: the seeded Journal Log passes the server Search API check at limit=50, but both the Search insert window and the inline picker show no Linkpick log 1379 row for the same query (30-second waits). This means the existing Search UI/provider path differs from the server's default keyword response. I am comparing semantic=false and semantic=true responses before adapting the shared Search cache; the picker will continue to use the existing registry and Search provider.

Production E2E finding: the seeded Journal Log passes the server Search API check at `limit=50`, but both the Search insert window and the inline picker show no `Linkpick log 1379` row for the same query (30-second waits). This means the existing Search UI/provider path differs from the server's default keyword response. I am comparing `semantic=false` and `semantic=true` responses before adapting the shared Search cache; the picker will continue to use the existing registry and Search provider.
Author
Owner

E2E diagnosis update: the false Search results came from this test's async page.waitForFunction predicates. The existing apps/web/e2e/smoke/flow-utils.mjs helper documents that waitForFunction does not await these API reads; the predicates returned a Promise and the test proceeded before Search completed. I replaced the startup-index, indexed-item and saved-Markdown waits with the shared waitFor helper, and bounded independent Search checks to batches of three.

E2E diagnosis update: the false Search results came from this test's async `page.waitForFunction` predicates. The existing `apps/web/e2e/smoke/flow-utils.mjs` helper documents that `waitForFunction` does not await these API reads; the predicates returned a Promise and the test proceeded before Search completed. I replaced the startup-index, indexed-item and saved-Markdown waits with the shared `waitFor` helper, and bounded independent Search checks to batches of three.
Author
Owner

Search E2E finding for #1379:

  • The real Search API returns the seeded Task as kind=task with a path-based /notes?path=Notes/...md href. The current linkPickerHref cannot produce a stable Task destination from that hit, so the Task row is dropped by the inline picker. I will resolve its stable Task ID through the existing Task loader only after selection; this keeps keystrokes on the shared Search client and cache.
  • The real Bookmarks API result is returned by Search as kind=note with /n/<calternal-id> and /notes?path=... hits. The E2E will assert the real Search kind and still verify that the bookmark Note links to its stable Note identity.
  • The seeded Journal entry is returned as kind=log with a stable /d/<date>#^<block-id> href when queried by the shared Linkpick marker.
Search E2E finding for #1379: - The real Search API returns the seeded Task as `kind=task` with a path-based `/notes?path=Notes/...md` href. The current `linkPickerHref` cannot produce a stable Task destination from that hit, so the Task row is dropped by the inline picker. I will resolve its stable Task ID through the existing Task loader only after selection; this keeps keystrokes on the shared Search client and cache. - The real Bookmarks API result is returned by Search as `kind=note` with `/n/<calternal-id>` and `/notes?path=...` hits. The E2E will assert the real Search kind and still verify that the bookmark Note links to its stable Note identity. - The seeded Journal entry is returned as `kind=log` with a stable `/d/<date>#^<block-id>` href when queried by the shared `Linkpick` marker.
Author
Owner

The last inline Task failure came from a stale production bundle: apps/web/build/index.html was last written at 18:58, while the path-backed Task/Photo resolver source was edited after 20:18. The browser run therefore did not include the current resolver. I confirmed the adapter unit test passes (5/5) and am rebuilding the production app before the next E2E run.

The last inline Task failure came from a stale production bundle: `apps/web/build/index.html` was last written at 18:58, while the path-backed Task/Photo resolver source was edited after 20:18. The browser run therefore did not include the current resolver. I confirmed the adapter unit test passes (5/5) and am rebuilding the production app before the next E2E run.
Author
Owner

The rebuilt production E2E reached all indexed kinds, including the path-backed Task and Photo rows. It then failed in the test helper for the client target titled “Files”: the regex matched 18 option accessible names because the word also appears in groups and hints. I am changing the helper to match the exact .slash-title text, then rerunning the same production flow.

The rebuilt production E2E reached all indexed kinds, including the path-backed Task and Photo rows. It then failed in the test helper for the client target titled “Files”: the regex matched 18 option accessible names because the word also appears in groups and hints. I am changing the helper to match the exact `.slash-title` text, then rerunning the same production flow.
Author
Owner

The .slash-title match was visible, but my first filter({ has }) form did not scope the nested locator to each option, so Playwright timed out even for the exact Note title. I am selecting the exact title node and walking to its option button, which preserves exact matching without relying on accessible-name text from the group or hint.

The `.slash-title` match was visible, but my first `filter({ has })` form did not scope the nested locator to each option, so Playwright timed out even for the exact Note title. I am selecting the exact title node and walking to its option button, which preserves exact matching without relying on accessible-name text from the group or hint.
Author
Owner

The “Files” title is intentionally shared by the Navigate result and two Settings actions, so exact title text alone still selects three real rows. The E2E fixture already records each client target's Search group; I am using that group label with the exact title to identify the intended row.

The “Files” title is intentionally shared by the Navigate result and two Settings actions, so exact title text alone still selects three real rows. The E2E fixture already records each client target's Search group; I am using that group label with the exact title to identify the intended row.
Author
Owner

The saved Markdown assertion exposed a test-flow flaw: pickInline waited for the requested row, then pressed Enter without moving the active row, so it always picked the first result. The Task query returned both a Note and Task for the same source file, and the test had not selected the Task row. I am making the helper navigate to the requested Search group and exact title before Enter, then checking the saved Markdown again.

The saved Markdown assertion exposed a test-flow flaw: `pickInline` waited for the requested row, then pressed Enter without moving the active row, so it always picked the first result. The Task query returned both a Note and Task for the same source file, and the test had not selected the Task row. I am making the helper navigate to the requested Search group and exact title before Enter, then checking the saved Markdown again.
Author
Owner

After correcting keyboard selection to target the requested row and group, the production flow reaches the ⌘⇧K case. The Search window closes, but the saved Note does not contain the expected [Chosen label](…) link within 20 seconds. I am adding E2E diagnostics for the browser selection, resulting route and persisted Markdown to locate the failing boundary.

After correcting keyboard selection to target the requested row and group, the production flow reaches the `⌘⇧K` case. The Search window closes, but the saved Note does not contain the expected `[Chosen label](…)` link within 20 seconds. I am adding E2E diagnostics for the browser selection, resulting route and persisted Markdown to locate the failing boundary.
Author
Owner

The new diagnostic showed the selected text was Linkpick selection 1379 … Chosen label: ⌘A in the Note editor selects the title and body together. The test expected only the body text, so its setup was incorrect. I am selecting the body paragraph directly and will then verify that Search restores that range.

The new diagnostic showed the selected text was `Linkpick selection 1379 … Chosen label`: `⌘A` in the Note editor selects the title and body together. The test expected only the body text, so its setup was incorrect. I am selecting the body paragraph directly and will then verify that Search restores that range.
Author
Owner

The E2E keyboard probe showed a real selection mismatch: it requested “Linkpick document 1379.pdf”, but the active option became “Linkpick capture 1379” while Search refreshed the same query. The shared popup stores focus by row index; updateCached() resets that index to zero, and update() replaces rows without mapping the active result. I am changing same-query updates to preserve the active Search result by stable ID and adding a popup regression test.

The E2E keyboard probe showed a real selection mismatch: it requested “Linkpick document 1379.pdf”, but the active option became “Linkpick capture 1379” while Search refreshed the same query. The shared popup stores focus by row index; `updateCached()` resets that index to zero, and `update()` replaces rows without mapping the active result. I am changing same-query updates to preserve the active Search result by stable ID and adding a popup regression test.
Author
Owner

The body-only selection check returned an empty selection after the E2E sent Home/Shift+End to the editor. In this headless Linux browser with macOS platform emulation, those keys did not select the text. I am changing the fixture to drag-select the exact visible body text and still verify that Search inserts that selected text.

The body-only selection check returned an empty selection after the E2E sent Home/Shift+End to the editor. In this headless Linux browser with macOS platform emulation, those keys did not select the text. I am changing the fixture to drag-select the exact visible body text and still verify that Search inserts that selected text.
Author
Owner

The drag-based fixture also left the DOM selection empty in headless Chromium. I am replacing it with Playwright's exact text-selection action on the body node and will continue to assert the browser selection before opening Search; this remains an E2E setup issue rather than evidence against the app shortcut.

The drag-based fixture also left the DOM selection empty in headless Chromium. I am replacing it with Playwright's exact text-selection action on the body node and will continue to assert the browser selection before opening Search; this remains an E2E setup issue rather than evidence against the app shortcut.
Author
Owner

Finding: when a Note editor owns focus, the registered Cmd+K link command handles the key before the global Search action. The production flow therefore cannot reach the requested normal Search actions menu from a Note.

Decisions for the shortcut collisions: move the existing Link command to Cmd+Shift+L so Cmd+K opens Search from a Note; use Ctrl+Enter for “Insert link in note” because the suggested Option+Enter is already Quick Look; use Cmd+Option+M for “Copy Markdown link” because the suggested Shift+Cmd+C is reserved for Edge DevTools. I will add these to the shared registry so Settings lists them, and verify them against the repository’s browser shortcut list. Outside a Note, Markdown links will use Home-root relative paths, matching the existing link grammar.

Finding: when a Note editor owns focus, the registered Cmd+K link command handles the key before the global Search action. The production flow therefore cannot reach the requested normal Search actions menu from a Note. Decisions for the shortcut collisions: move the existing Link command to Cmd+Shift+L so Cmd+K opens Search from a Note; use Ctrl+Enter for “Insert link in note” because the suggested Option+Enter is already Quick Look; use Cmd+Option+M for “Copy Markdown link” because the suggested Shift+Cmd+C is reserved for Edge DevTools. I will add these to the shared registry so Settings lists them, and verify them against the repository’s browser shortcut list. Outside a Note, Markdown links will use Home-root relative paths, matching the existing link grammar.
Author
Owner

Finding: the first production-browser runs used the prebuilt server's origin/dev web assets because routeCurrentBuild only serves this worktree's build when CALTERNAL_E2E_ASSET_OVERRIDE=1. I reran with that flag and Mac emulation enabled; the current feature flow now reaches the inline picker.

Finding: Search returned both the Task and its source Note from /api/v1/search, but the shared client removed the Task because both entries use the same Markdown path. A focused regression test failed with only the Note remaining. I changed the shared Search deduper to keep Task entries as distinct stable targets, matching the existing Journal handling and the link-picker requirement.

Finding: the first production-browser runs used the prebuilt server's `origin/dev` web assets because `routeCurrentBuild` only serves this worktree's build when `CALTERNAL_E2E_ASSET_OVERRIDE=1`. I reran with that flag and Mac emulation enabled; the current feature flow now reaches the inline picker. Finding: Search returned both the Task and its source Note from `/api/v1/search`, but the shared client removed the Task because both entries use the same Markdown path. A focused regression test failed with only the Note remaining. I changed the shared Search deduper to keep Task entries as distinct stable targets, matching the existing Journal handling and the link-picker requirement.
Author
Owner

Finding: production event tracing showed the Search window listener opened before the editor context listener attached the saved Note selection. The window listener now reads the shared event detail in a microtask, after all listeners enrich it. A production probe then showed the normal Search Actions menu had the Note insertion action and its Ctrl+Enter shortcut.

Finding: Search's Markdown copy completed and wrote the correct clipboard value, but its plain success toast was discarded by the shared toast adapter because it had no action or approved background category. The success toast now offers “Copy again,” which keeps it in the User-action category and makes the feedback render.

Finding: the end-to-end query for a Task also matched the Note that linked to it. The test had opened Actions on the active row rather than the Task row. The production flow now moves the combobox to the exact result before invoking each Search action; route links are asserted in the canonical angle-bracket Markdown form.

Finding: production event tracing showed the Search window listener opened before the editor context listener attached the saved Note selection. The window listener now reads the shared event detail in a microtask, after all listeners enrich it. A production probe then showed the normal Search Actions menu had the Note insertion action and its Ctrl+Enter shortcut. Finding: Search's Markdown copy completed and wrote the correct clipboard value, but its plain success toast was discarded by the shared toast adapter because it had no action or approved background category. The success toast now offers “Copy again,” which keeps it in the User-action category and makes the feedback render. Finding: the end-to-end query for a Task also matched the Note that linked to it. The test had opened Actions on the active row rather than the Task row. The production flow now moves the combobox to the exact result before invoking each Search action; route links are asserted in the canonical angle-bracket Markdown form.
Author
Owner

Issue #1379 progress — the first post-implementation cd apps/web && bun run check failed. svelte-check found 12 errors and 1 warning in 4 files; the errors showed that the app’s svelte.config.js maps @calternal/editor to src/lib/notes/editor-types/index.d.ts, whose signatures did not include the new LinkPicker exports or ExtensionOpts.linkPicker. One host test also passed user.id although liveNoteExtensions accepts only { name, color }. The same run found the existing Search dialog ResizeObserver exception had a stale syntax hash after the module changed. I am syncing the type boundary and fixture, then refreshing that exact pin in both the ledger and its #1058 adoption checkpoint; the exception count and budget stay unchanged.

Issue #1379 progress — the first post-implementation `cd apps/web && bun run check` failed. `svelte-check found 12 errors and 1 warning in 4 files`; the errors showed that the app’s `svelte.config.js` maps `@calternal/editor` to `src/lib/notes/editor-types/index.d.ts`, whose signatures did not include the new LinkPicker exports or `ExtensionOpts.linkPicker`. One host test also passed `user.id` although `liveNoteExtensions` accepts only `{ name, color }`. The same run found the existing Search dialog ResizeObserver exception had a stale syntax hash after the module changed. I am syncing the type boundary and fixture, then refreshing that exact pin in both the ledger and its #1058 adoption checkpoint; the exception count and budget stay unchanged.
Author
Owner

The required full bun run test reached scripts/design-rules.test.mjs and stopped before Vitest. The test compared changed files against the current origin/dev and reported pre-existing controls and Search window styles. Evidence: git rev-list --left-right --count origin/dev...HEAD is 322 16, with merge base 0419170fe456b58614befcb12d9c9d255118f1c0. Running the failing design-rules file against that actual merge base passes; the shared web tests for the Note host and link picker also pass (2 files, 11 tests). I am integrating the current origin/dev as the issue instructions require, then I will rerun the final gates.

The required full `bun run test` reached `scripts/design-rules.test.mjs` and stopped before Vitest. The test compared changed files against the current `origin/dev` and reported pre-existing controls and Search window styles. Evidence: `git rev-list --left-right --count origin/dev...HEAD` is `322 16`, with merge base `0419170fe456b58614befcb12d9c9d255118f1c0`. Running the failing design-rules file against that actual merge base passes; the shared web tests for the Note host and link picker also pass (2 files, 11 tests). I am integrating the current `origin/dev` as the issue instructions require, then I will rerun the final gates.
Author
Owner

Final report — #1379

Branch: job/linkpick-1379
Head: ec68b3e8762aeede3226b471a6110a810145b504

Built

  • Added the Notes link picker for [[ and @ using a second TipTap Suggestion instance and the shared editor popup. It reads the existing Search providers/cache, aborts stale queries, shows cached rows immediately, and uses stable or Note-relative links.
  • Added Search insert-link mode. It saves/restores the Note selection and inserts the selected item as a normal link mark. Registered Cmd+Shift+K; Cmd+K Search Actions can also insert a link.
  • Added Markdown round-trip coverage for all link kinds and titles containing brackets, pipes, Unicode and emoji.
  • Extended the shared MenuItem shortcut tooltip option and added real production E2E coverage plus a link-picker cache profile.

Files

  • Editor: packages/editor/src/extensions.ts, index.ts, linkCommand.ts, linkPicker.ts, linkPickerExtension.ts, shortcuts.ts, slash.ts, suggestionPopup.ts; tests formatCommands.svelte.test.ts, linkPicker.test.ts, linkPickerExtension.svelte.test.ts, markdown.roundtrip.svelte.test.ts, suggestionPopup.svelte.test.ts.
  • Web Search and Notes: apps/web/src/lib/components/search-dialog.svelte, copyLinkToast.ts, notes/NoteEditorSurface.svelte, notes/editorHost.ts, notes/editorHost.svelte.test.ts, notes/editor-types/index.d.ts, search/SearchField.svelte, search/kinds.ts, search/linkPicker.ts, search/linkPicker.test.ts, search/links.ts, search/links.test.ts, search/registry.ts, search/registry.test.ts, search/search-units.test.ts, search/server.ts, search/window.svelte.ts, shortcuts/registry.ts, shortcuts/registry.test.ts, routes/+layout.svelte, menu/MenuItem.svelte.test.ts.
  • Shared UI: packages/ui/src/components/menu/MenuItem.svelte, packages/ui/src/components/menu/types.ts.
  • E2E/performance: apps/web/e2e/linkpick-1379.mjs, bench/linkpick-1379.mjs, docs/perf/baseline.json, contracts/perf/adoption-1058.json, contracts/perf/exceptions.json.

Gates

cd apps/web && bun run build (exit 0):

✓ built in 1m 55s
  Wrote site to "build"
  ✔ done
Compressed 912 static variants; saved 21511965 bytes.

cd apps/web && bun run check (exit 0):

perf-lint: PASS; 0 violations; 22543 scoped exceptions
svelte-check found 0 errors and 4 warnings in 2 files

The four Svelte warnings are three unused MailView selectors and the .ready selector in the Money route.

cd apps/web && bun run test was run once after the merge, before the one-line toast fix:

FAIL  |unit| src/lib/stores/toast-policy.test.ts > toast policy > requires an action, a named background result, or an explicit no-toast decision
AssertionError: src/lib/components/search-dialog.svelte:459: { label: 'This item has no Markdown link.', kind: 'alert' }: expected [ Array(1) ] to deeply equal []
 Test Files  1 failed | 293 passed (294)
      Tests  1 failed | 2116 passed (2117)
   Duration  545.44s (transform 27%, environment 26%, import 22%, tests 18%, setup 6%)

I added the required backgroundResult classification without changing the test expectation. The failing file then passed alone:

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  13.82s (transform 82%, import 11%, tests 6%)

Per the verification policy, the full bun run test rerun belongs in the merge round. It must confirm all 294 files / 2,117 tests pass on the fixed source.

E2E and UX

The production E2E passed on the merged build:

PASS #1379: typed [[ and @, inserted indexed Notes/Tasks/Journal/Events/Bookmarks/Mail/Files/Photos, used Cmd+Shift+K and Search Actions/Ctrl+Enter, copied Markdown in and outside a Note; screenshots: /home/kayg/Developer/calternal-wt/linkpick-1379/artifacts/linkpick

UX gaps closed: pointer, touch and keyboard selection; combobox/listbox roles, active descendant and live result counts; Escape and Enter handling; mobile keyboard viewport placement; immediate cache results; stable Copy link actions and shortcut tooltips. The E2E produced 12 macOS-emulated screenshots covering picker and Search insert mode at 390, 820 and 1440 px in light and dark themes.

UX gap left: the screenshots remain in ignored artifacts/linkpick/ and are not attached to this issue. The available fj issue comment CLI has no attachment upload option; I did not commit the PNGs.

Performance

Local production profile: 50 indexed Note results, 20 visible rows, 25 warm keystrokes. Cached keystroke-to-row p50/p95 were 0.3/0.4 ms; one 534.3 ms maximum occurred under load averages 23.71/24.17/28.66. The perf VM runner was unavailable, so this is recorded as a local measurement with no prior feature-specific baseline. Path identity caching is capped at 128 entries and 30 seconds; selected Task/Photo identity resolution runs only after selection.

Decisions not specified by DESIGN

  • Use a 20-result picker cap and a 50 ms Suggestion debounce; show an exact-query cache answer before refresh.
  • Use the Search result title as inserted link text and replace exactly the active trigger/query range.
  • Cache resolved path-backed identities for 30 seconds with a 128-entry bound.

The merged perf pins were refreshed for exact syntax hashes only; live scope counts and limits did not change. cargo clean completed: Removed 8647 files, 7.3GiB total. The web build directory was removed. No Rust source changed; no push, deploy or issue close was performed.

# Final report — #1379 Branch: `job/linkpick-1379` Head: `ec68b3e8762aeede3226b471a6110a810145b504` ## Built - Added the Notes link picker for `[[` and `@` using a second TipTap Suggestion instance and the shared editor popup. It reads the existing Search providers/cache, aborts stale queries, shows cached rows immediately, and uses stable or Note-relative links. - Added Search insert-link mode. It saves/restores the Note selection and inserts the selected item as a normal link mark. Registered Cmd+Shift+K; Cmd+K Search Actions can also insert a link. - Added Markdown round-trip coverage for all link kinds and titles containing brackets, pipes, Unicode and emoji. - Extended the shared MenuItem shortcut tooltip option and added real production E2E coverage plus a link-picker cache profile. ## Files - Editor: `packages/editor/src/extensions.ts`, `index.ts`, `linkCommand.ts`, `linkPicker.ts`, `linkPickerExtension.ts`, `shortcuts.ts`, `slash.ts`, `suggestionPopup.ts`; tests `formatCommands.svelte.test.ts`, `linkPicker.test.ts`, `linkPickerExtension.svelte.test.ts`, `markdown.roundtrip.svelte.test.ts`, `suggestionPopup.svelte.test.ts`. - Web Search and Notes: `apps/web/src/lib/components/search-dialog.svelte`, `copyLinkToast.ts`, `notes/NoteEditorSurface.svelte`, `notes/editorHost.ts`, `notes/editorHost.svelte.test.ts`, `notes/editor-types/index.d.ts`, `search/SearchField.svelte`, `search/kinds.ts`, `search/linkPicker.ts`, `search/linkPicker.test.ts`, `search/links.ts`, `search/links.test.ts`, `search/registry.ts`, `search/registry.test.ts`, `search/search-units.test.ts`, `search/server.ts`, `search/window.svelte.ts`, `shortcuts/registry.ts`, `shortcuts/registry.test.ts`, `routes/+layout.svelte`, `menu/MenuItem.svelte.test.ts`. - Shared UI: `packages/ui/src/components/menu/MenuItem.svelte`, `packages/ui/src/components/menu/types.ts`. - E2E/performance: `apps/web/e2e/linkpick-1379.mjs`, `bench/linkpick-1379.mjs`, `docs/perf/baseline.json`, `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`. ## Gates `cd apps/web && bun run build` (exit 0): ```text ✓ built in 1m 55s Wrote site to "build" ✔ done Compressed 912 static variants; saved 21511965 bytes. ``` `cd apps/web && bun run check` (exit 0): ```text perf-lint: PASS; 0 violations; 22543 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files ``` The four Svelte warnings are three unused MailView selectors and the `.ready` selector in the Money route. `cd apps/web && bun run test` was run once after the merge, before the one-line toast fix: ```text FAIL |unit| src/lib/stores/toast-policy.test.ts > toast policy > requires an action, a named background result, or an explicit no-toast decision AssertionError: src/lib/components/search-dialog.svelte:459: { label: 'This item has no Markdown link.', kind: 'alert' }: expected [ Array(1) ] to deeply equal [] Test Files 1 failed | 293 passed (294) Tests 1 failed | 2116 passed (2117) Duration 545.44s (transform 27%, environment 26%, import 22%, tests 18%, setup 6%) ``` I added the required `backgroundResult` classification without changing the test expectation. The failing file then passed alone: ```text Test Files 1 passed (1) Tests 3 passed (3) Duration 13.82s (transform 82%, import 11%, tests 6%) ``` Per the verification policy, the full `bun run test` rerun belongs in the merge round. It must confirm all 294 files / 2,117 tests pass on the fixed source. ## E2E and UX The production E2E passed on the merged build: ```text PASS #1379: typed [[ and @, inserted indexed Notes/Tasks/Journal/Events/Bookmarks/Mail/Files/Photos, used Cmd+Shift+K and Search Actions/Ctrl+Enter, copied Markdown in and outside a Note; screenshots: /home/kayg/Developer/calternal-wt/linkpick-1379/artifacts/linkpick ``` UX gaps closed: pointer, touch and keyboard selection; combobox/listbox roles, active descendant and live result counts; Escape and Enter handling; mobile keyboard viewport placement; immediate cache results; stable Copy link actions and shortcut tooltips. The E2E produced 12 macOS-emulated screenshots covering picker and Search insert mode at 390, 820 and 1440 px in light and dark themes. UX gap left: the screenshots remain in ignored `artifacts/linkpick/` and are not attached to this issue. The available `fj issue comment` CLI has no attachment upload option; I did not commit the PNGs. ## Performance Local production profile: 50 indexed Note results, 20 visible rows, 25 warm keystrokes. Cached keystroke-to-row p50/p95 were 0.3/0.4 ms; one 534.3 ms maximum occurred under load averages 23.71/24.17/28.66. The perf VM runner was unavailable, so this is recorded as a local measurement with no prior feature-specific baseline. Path identity caching is capped at 128 entries and 30 seconds; selected Task/Photo identity resolution runs only after selection. ## Decisions not specified by DESIGN - Use a 20-result picker cap and a 50 ms Suggestion debounce; show an exact-query cache answer before refresh. - Use the Search result title as inserted link text and replace exactly the active trigger/query range. - Cache resolved path-backed identities for 30 seconds with a 128-entry bound. The merged perf pins were refreshed for exact syntax hashes only; live scope counts and limits did not change. `cargo clean` completed: `Removed 8647 files, 7.3GiB total`. The web build directory was removed. No Rust source changed; no push, deploy or issue close was performed.
Author
Owner

Started continuation on branch job/linkpick-1379 at ec68b3e8762aeede3226b471a6110a810145b504, based on origin/dev ef0aa12b12e0047cf9a779827cc0ed32261b934e. I am validating the previous implementation against the owner addition and will add the requested Search Actions shortcuts and interaction coverage.

Started continuation on branch `job/linkpick-1379` at `ec68b3e8762aeede3226b471a6110a810145b504`, based on `origin/dev` `ef0aa12b12e0047cf9a779827cc0ed32261b934e`. I am validating the previous implementation against the owner addition and will add the requested Search Actions shortcuts and interaction coverage.
Author
Owner

Production E2E regression found before the new transaction scenario: the server returned the same path-derived Search result ID for a Note and its Task. The inline popup showed one Notes result and no Tasks result, so selecting the Task timed out. dedupeSearchResults drops the second row by ID before its existing path dedupe can preserve Task identity. I am adding a focused regression test for this boundary before fixing the deduper.

Production E2E regression found before the new transaction scenario: the server returned the same path-derived Search result ID for a Note and its Task. The inline popup showed one Notes result and no Tasks result, so selecting the Task timed out. `dedupeSearchResults` drops the second row by ID before its existing path dedupe can preserve Task identity. I am adding a focused regression test for this boundary before fixing the deduper.
Author
Owner

Search palette Task row identity finding (#1379): the Index can emit a Task and its source Note with the same plugin and result ID. Dedupe now retained both kinds, but the palette also uses result IDs as Svelte row keys and option IDs, so the Note and Task still collided in the rendered list. Added a kind-scoped UI ID for Task hits while preserving the original server identity used for backend actions. Regression evidence: the new unit test failed before the fix with identical IDs, then passed after it; focused Search suites passed (3 files, 30 tests). Continuing with the production browser flow and required screenshots.

Search palette Task row identity finding (#1379): the Index can emit a Task and its source Note with the same plugin and result ID. Dedupe now retained both kinds, but the palette also uses result IDs as Svelte row keys and option IDs, so the Note and Task still collided in the rendered list. Added a kind-scoped UI ID for Task hits while preserving the original server identity used for backend actions. Regression evidence: the new unit test failed before the fix with identical IDs, then passed after it; focused Search suites passed (3 files, 30 tests). Continuing with the production browser flow and required screenshots.
Author
Owner

Second Search finding (#1379): the production Playwright flow passed the Task and Note rows after the unique-ID fix, then the inline picker lost a Journal result. Evidence from the real server: semantic=false returned the requested Journal hit while the immediately following semantic=true response returned no hits. serverSearch replaced its keyword answer with the empty hybrid answer, so the picker showed “No matching links.” I am adding a client regression to retain the keyword answer when the hybrid response is empty, then I will rerun the real flow.

Second Search finding (#1379): the production Playwright flow passed the Task and Note rows after the unique-ID fix, then the inline picker lost a Journal result. Evidence from the real server: `semantic=false` returned the requested Journal hit while the immediately following `semantic=true` response returned no hits. `serverSearch` replaced its keyword answer with the empty hybrid answer, so the picker showed “No matching links.” I am adding a client regression to retain the keyword answer when the hybrid response is empty, then I will rerun the real flow.
Author
Owner

Third Search finding (#1379): after correcting the E2E selector to match the real listbox structure, the Task row was absent from the final hybrid result. The browser trace showed semantic=false returned the indexed Task, while semantic=true returned only the Notes-plugin Note with the same title. The client then replaced the partial Task with that Note. I am extending the merge to append keyword identities missing from the hybrid results, preserving hybrid order and running the existing Note de-duplication across the merged list.

Third Search finding (#1379): after correcting the E2E selector to match the real listbox structure, the Task row was absent from the final hybrid result. The browser trace showed `semantic=false` returned the indexed Task, while `semantic=true` returned only the Notes-plugin Note with the same title. The client then replaced the partial Task with that Note. I am extending the merge to append keyword identities missing from the hybrid results, preserving hybrid order and running the existing Note de-duplication across the merged list.
Author
Owner

Fourth finding (#1379): the production E2E inserted the transaction from the menu, then Ctrl+Enter while the menu was open activated the shared Menu's default Enter behavior. The saved Note still had one transaction link, so the Search shortcut never ran. The shared Menu handles Enter before Search's bubbling shortcut listener. I am adding a Search-local capture handler for its two registered Actions shortcuts so they run before the shared menu navigation handler.

Fourth finding (#1379): the production E2E inserted the transaction from the menu, then `Ctrl+Enter` while the menu was open activated the shared Menu's default Enter behavior. The saved Note still had one transaction link, so the Search shortcut never ran. The shared Menu handles Enter before Search's bubbling shortcut listener. I am adding a Search-local capture handler for its two registered Actions shortcuts so they run before the shared menu navigation handler.
Author
Owner

Search Actions end-to-end checks reached both transaction insertion paths; the registered Ctrl+Enter shortcut needed a capture-phase listener because the shared Menu handles Enter first. The latest run also exposed a screenshot-fixture issue: dismissing the inline picker leaves its query text in the source Note, so the later Actions capture could not find the exact original text. I changed the Actions capture to use a separate Note created through the real Notes API. I am rerunning the production flow now.

Search Actions end-to-end checks reached both transaction insertion paths; the registered Ctrl+Enter shortcut needed a capture-phase listener because the shared Menu handles Enter first. The latest run also exposed a screenshot-fixture issue: dismissing the inline picker leaves its query text in the source Note, so the later Actions capture could not find the exact original text. I changed the Actions capture to use a separate Note created through the real Notes API. I am rerunning the production flow now.
Author
Owner

A later E2E run captured another Search race: after filling “Linkpick task”, the list still contained the previous query's Note rows and the combobox still pointed at the prior selection Note while keyboard navigation ran. Search intentionally keeps provider rows visible during stale-while-revalidate. The test now waits for the matching semantic=true API response before selection; the screenshot flow also waits for that complete response so the inline link picker uses the settled Search cache. I am rerunning the flow with this wait.

A later E2E run captured another Search race: after filling “Linkpick task”, the list still contained the previous query's Note rows and the combobox still pointed at the prior selection Note while keyboard navigation ran. Search intentionally keeps provider rows visible during stale-while-revalidate. The test now waits for the matching semantic=true API response before selection; the screenshot flow also waits for that complete response so the inline link picker uses the settled Search cache. I am rerunning the flow with this wait.
Author
Owner

The new macOS screenshot pass found a shortcut focus gap: clicking Actions, pressing Escape, then ⌘K did not reopen the menu because Search's search.actions handler was bound to the combobox while Escape restored focus to the Actions button. The global handler selected the query instead. The Search capture handler now routes search.actions from any focus target while the Search surface is open; the E2E captures the menu after both pointer click and shortcut activation.

The new macOS screenshot pass found a shortcut focus gap: clicking Actions, pressing Escape, then ⌘K did not reopen the menu because Search's `search.actions` handler was bound to the combobox while Escape restored focus to the Actions button. The global handler selected the query instead. The Search capture handler now routes `search.actions` from any focus target while the Search surface is open; the E2E captures the menu after both pointer click and shortcut activation.
Author
Owner

The 390px Mac-emulated capture showed document.activeElement was the editor surface with is-reading still set after the helper's two touch taps. The app's touch double-tap path has a 450 ms interval; on this loaded host the interval was exceeded, so Cmd+Shift+K had no focused Note editor to enrich Search. The E2E helper now selects the block with one touch tap, presses Enter to edit, and waits for the editor to leave is-reading before testing shortcuts.

The 390px Mac-emulated capture showed `document.activeElement` was the editor surface with `is-reading` still set after the helper's two touch taps. The app's touch double-tap path has a 450 ms interval; on this loaded host the interval was exceeded, so Cmd+Shift+K had no focused Note editor to enrich Search. The E2E helper now selects the block with one touch tap, presses Enter to edit, and waits for the editor to leave `is-reading` before testing shortcuts.
Author
Owner

The production screenshots exposed two capture issues: the shared-server version id differs from the local web build, so the real “Update available” toast covered every capture; F6 then Escape dismisses it through the app's supported toast controls. The shortcut-opened Menu's first row also matched :focus-visible after FloatingSurface called plain .focus(). The shared initial-focus call will use focusVisible: false, and the E2E now checks that opened menu rows have no focus ring. On touch-first layouts, shared Kbd intentionally hides shortcut caps in pointer mode; the shortcut-opened 390px screenshot will be taken while the root input mode is keyboard so the caps are visible without local Kbd styling.

The production screenshots exposed two capture issues: the shared-server version id differs from the local web build, so the real “Update available” toast covered every capture; F6 then Escape dismisses it through the app's supported toast controls. The shortcut-opened Menu's first row also matched `:focus-visible` after FloatingSurface called plain `.focus()`. The shared initial-focus call will use `focusVisible: false`, and the E2E now checks that opened menu rows have no focus ring. On touch-first layouts, shared Kbd intentionally hides shortcut caps in pointer mode; the shortcut-opened 390px screenshot will be taken while the root input mode is keyboard so the caps are visible without local Kbd styling.
Author
Owner

Screenshot review found the shared-server “Update available / Reload” notice still in the captures after the F6/Escape attempt. The update toast is persistent, so keyboard dismissal did not clear it. I’ll make the screenshot-only contexts report the worktree’s current build ID at /api/v1/version; the server data and all Search APIs remain real. This follows the existing production screenshot pattern in share-pill-contrast-1332.mjs and keeps the captures free of stale-build chrome.

Screenshot review found the shared-server “Update available / Reload” notice still in the captures after the F6/Escape attempt. The update toast is persistent, so keyboard dismissal did not clear it. I’ll make the screenshot-only contexts report the worktree’s current build ID at `/api/v1/version`; the server data and all Search APIs remain real. This follows the existing production screenshot pattern in `share-pill-contrast-1332.mjs` and keeps the captures free of stale-build chrome.
Author
Owner

The first run of the extended performance profile stopped before the new copy-action sample: its existing inline-picker loop timed out with window.__linkPickMeasure === null and failed cached query paints an inline result at linkpick-1379.mjs:528. The production E2E flow passed. I added read-only state evidence at that point (focused element, editor text, picker contents and current Search rows) and am reproducing once to identify whether the profile's warm-up or focus sequence is stale.

The first run of the extended performance profile stopped before the new copy-action sample: its existing inline-picker loop timed out with `window.__linkPickMeasure === null` and failed `cached query paints an inline result` at `linkpick-1379.mjs:528`. The production E2E flow passed. I added read-only state evidence at that point (focused element, editor text, picker contents and current Search rows) and am reproducing once to identify whether the profile's warm-up or focus sequence is stale.
Author
Owner

A second profile run reached the Search Task path and failed at Search returns the indexed Task path for selection-time identity resolution: the Task was created, but the immediate /api/v1/search response had no Task hit. waitForIndexed() already polls the real API later in this script, after this assertion. I’ll move the Task-index wait before the assertion so profile and full-flow startup use the same asynchronous indexing contract.

A second profile run reached the Search Task path and failed at `Search returns the indexed Task path for selection-time identity resolution`: the Task was created, but the immediate `/api/v1/search` response had no Task hit. `waitForIndexed()` already polls the real API later in this script, after this assertion. I’ll move the Task-index wait before the assertion so profile and full-flow startup use the same asynchronous indexing contract.
Author
Owner

Correction to my previous note: waitForIndexed() is already called before the Task-path assertion. The observed taskHit was undefined at the assertion, but the assertion checks both hit presence and that its href starts with /notes?path=; the output alone did not distinguish those conditions. I’m adding the response details to the failure message and will keep the existing expectation unchanged while I establish which field differed.

Correction to my previous note: `waitForIndexed()` is already called before the Task-path assertion. The observed `taskHit` was `undefined` at the assertion, but the assertion checks both hit presence and that its `href` starts with `/notes?path=`; the output alone did not distinguish those conditions. I’m adding the response details to the failure message and will keep the existing expectation unchanged while I establish which field differed.
Author
Owner

The inline-profile diagnostic now shows that Search indexing is healthy: the real API returned benchmark Notes 01–05. After warm-up, however, the active element was TEXTAREA.svelte-16bz0us, the inline popup was absent, and the Note editor’s text ended in Linkpick source 1379. The profile’s editor-focus sequence is the failing layer. I’m capturing the active element and overlay state to correct the profile setup; no app regression is indicated by this run.

The inline-profile diagnostic now shows that Search indexing is healthy: the real API returned benchmark Notes 01–05. After warm-up, however, the active element was `TEXTAREA.svelte-16bz0us`, the inline popup was absent, and the Note editor’s text ended in `Linkpick source 1379`. The profile’s editor-focus sequence is the failing layer. I’m capturing the active element and overlay state to correct the profile setup; no app regression is indicated by this run.
Author
Owner

bun run check found one exact-pin issue: layout.observer-purpose in search-dialog.svelte. The existing ResizeObserver site and rule are unchanged; the module hash moved with this edit. I refreshed only that exception hash to the current parsed value 6751b43a1c2993df4f61e14298c9a618d9950734f0c37c35550141e1f6f45986; the owner, reason, rule, limit and expiry are unchanged. The first check completed with svelte-check found 0 errors and 4 warnings in 2 files, then exited 1 on the stale pin. I’m verifying the check again after this exact refresh.

`bun run check` found one exact-pin issue: `layout.observer-purpose` in `search-dialog.svelte`. The existing ResizeObserver site and rule are unchanged; the module hash moved with this edit. I refreshed only that exception hash to the current parsed value `6751b43a1c2993df4f61e14298c9a618d9950734f0c37c35550141e1f6f45986`; the owner, reason, rule, limit and expiry are unchanged. The first check completed with `svelte-check found 0 errors and 4 warnings in 2 files`, then exited 1 on the stale pin. I’m verifying the check again after this exact refresh.
Author
Owner

Follow-up: perf-lint also requires the matching #1058 adoption checkpoint to carry the same exact live scope hash as the exception ledger. I updated that paired hash too. Both entries now identify the same unchanged ResizeObserver scope; no rule, limit, reason, expiry or ratchet changed.

Follow-up: perf-lint also requires the matching #1058 adoption checkpoint to carry the same exact live scope hash as the exception ledger. I updated that paired hash too. Both entries now identify the same unchanged ResizeObserver scope; no rule, limit, reason, expiry or ratchet changed.
Author
Owner

The live perf-ledger comparison found four additional moved exact pins across the branch: one cache.raw-read scope in search/server.ts and three existing observer scopes in packages/ui/src/components/menu/FloatingSurface.svelte. I refreshed only their hashes in the exception ledger and matching adopted entry where present. scripts/perf-lint --check now reports perf-lint: PASS; 0 violations; 22543 scoped exceptions; the exception count and ratchet are unchanged.

The live perf-ledger comparison found four additional moved exact pins across the branch: one `cache.raw-read` scope in `search/server.ts` and three existing observer scopes in `packages/ui/src/components/menu/FloatingSurface.svelte`. I refreshed only their hashes in the exception ledger and matching adopted entry where present. `scripts/perf-lint --check` now reports `perf-lint: PASS; 0 violations; 22543 scoped exceptions`; the exception count and ratchet are unchanged.
Author
Owner

Full web gate finding for #1379:

cd apps/web && bun run test exited 1 during scripts/design-rules.test.mjs, before Vitest. It reports a new design-rule violation at packages/ui/src/components/TagPill.svelte:78: new hand-rolled button: tag-action touch-hit aria-haspopup={haspopup} aria-pressed={haspopup ? undefined : active} aria-label=.... This file is outside the Search Actions change and is unchanged in this branch. The focused Search helper test passes: 1 file, 6 tests.

Full web gate finding for #1379: `cd apps/web && bun run test` exited 1 during `scripts/design-rules.test.mjs`, before Vitest. It reports a new design-rule violation at `packages/ui/src/components/TagPill.svelte:78`: `new hand-rolled button: tag-action touch-hit aria-haspopup={haspopup} aria-pressed={haspopup ? undefined : active} aria-label=...`. This file is outside the Search Actions change and is unchanged in this branch. The focused Search helper test passes: 1 file, 6 tests.
Author
Owner

Finished #1379 on job/linkpick-1379.

Head: 1f673ff0e1d946013c11e33da4566f288e7fd4c3

Commits:

  • f5ac4930c feat(search): expose link actions in result menu (#1379)
  • 1f673ff0e chore(perf): refresh exact Search scope pins (#1379)

Built

  • Search result Actions now includes Insert link in note when Search has a Note selection callback, and Copy Markdown link for every linkable result.
  • Both actions use the shared shortcut registry, Menu, MenuItem, Kbd caps, warm tooltips, and the quiet Markdown link copied toast. markdownLinkForResult is the shared Markdown writer, using the Note editor's escaping and link rules.
  • Capture-phase shortcut handling routes actions before Menu and global Search handlers. Programmatic Menu focus suppresses a focus ring until keyboard navigation.
  • The Playwright flow opens Cmd+K from a Note, selects an amount-free transaction Search hit, then checks both actions by menu and shortcut against saved Note Markdown and clipboard contents. The transaction hit is a test-only API fixture because Money has no Search provider in this branch.

Gates

$ bun run build
✓ built in 2m 34s
Run npm run preview to preview your production build locally.
> Using @sveltejs/adapter-static
Wrote site to "build"
✔ done
[PLUGIN_TIMINGS] JavaScript callbacks ran for 156.1s. The build took 154.2s.
Compressed 912 static variants; saved 21513415 bytes.

$ bun run check
perf-lint: PASS; 0 violations; 22543 scoped exceptions
svelte-check found 0 errors and 4 warnings in 2 files

$ bunx vitest run src/lib/search/linkPicker.test.ts --maxWorkers=2
Test Files 1 passed (1)
Tests 6 passed (6)

$ bun run test
New design-rule violations:
- packages/ui/src/components/TagPill.svelte:78: new hand-rolled button: tag-action touch-hit aria-haspopup={haspopup} aria-pressed={haspopup ? undefined : active} aria-label={(leaf || leafOnly) ? `#${tag}
error: script "test" exited with code 1

The full test command stops in scripts/design-rules.test.mjs, before Vitest. It compares against the current origin/dev; this branch merged origin/dev at d3251a5a3, after which origin/dev advanced to d395b6e and added the #1286 TagPill changes and design exception. TagPill.svelte is unchanged by #1379. The tracking ref is now 73 commits ahead. I did not repeat the once-only origin/dev merge. For the merge round, integrate current origin/dev and run cd apps/web && bun run test to verify the design guard and full Vitest suite.

Playwright output:

PASS #1379: typed [[ and @, inserted indexed Notes/Tasks/Journal/Events/Bookmarks/Mail/Files/Photos, exercised transaction Search Actions by pointer and shortcut, copied Markdown in and outside a Note; screenshots: /home/kayg/Developer/calternal-wt/linkpick-1379/artifacts/linkpick

Local action profile (load average was 25.21/26.16/30.60): 50 indexed results, 20 displayed results, 25 warm copy actions; p50 60.2 ms, p95 84.2 ms, max 85.2 ms. Browser mean/peak RSS: 604,934,627/636,710,912 bytes; mean/peak CPU: 101.02%/206.93%. Server mean/peak RSS: 442,336,400/545,267,712 bytes; mean/peak CPU: 37.43%/138.19%. The existing baseline measures inline search (p50 0.3 ms, p95 0.4 ms, max 534.3 ms), so it is not a directly comparable action baseline.

UX gaps

Closed: pointer and keyboard actions save/copy the same canonical Markdown; the Menu items have accessible names, aria-keyshortcuts, and visible Kbd caps; the copy action works both from a Note and outside one; keyboard-opened Menu has no programmatic focus ring. macOS captures cover 390, 820, and 1440 px in light and dark, with pointer and shortcut Menu states.

Left: live transaction discovery depends on a Money Search provider. The feature handles transaction Search results, but the end-to-end transaction result is injected only at the test API boundary until that provider exists.

Decisions not in DESIGN

  • ⌥↩ is already Search Quick Look, so Insert uses the free ⌃↩ registry binding.
  • ⇧⌘C opens Edge DevTools on macOS, so Copy Markdown uses the free ⌥⌘M registry binding.

Files

  • apps/web/src/lib/components/search-dialog.svelte
  • apps/web/src/lib/copyLinkToast.ts
  • apps/web/src/lib/menu/MenuItem.svelte and its tests
  • apps/web/src/lib/notes/NoteEditorSurface.svelte, editorHost.ts, editorHost.svelte.test.ts, and editor-types/index.d.ts
  • apps/web/src/lib/search/{SearchField.svelte,kinds.ts,linkPicker.ts,links.ts,registry.ts,server.ts,window.svelte.ts} and related tests
  • apps/web/src/lib/shortcuts/registry.ts and tests
  • apps/web/e2e/linkpick-1379.mjs
  • packages/editor/src/{extensions.ts,formatCommands.ts,linkCommand.ts,linkPicker.ts,linkPickerExtension.ts,shortcuts.ts,slash.ts,suggestionPopup.ts} and related tests
  • packages/ui/src/components/menu/{FloatingSurface.svelte,MenuItem.svelte,types.ts}
  • bench/linkpick-1379.mjs, docs/perf/baseline.json, contracts/perf/{adoption-1058.json,exceptions.json}

Attached screenshots

All 24 actual-size macOS screenshots are attached. Actions Menu captures include pointer and shortcut opening at 390, 820, and 1440 px in light and dark. Search insert and inline picker captures cover the same widths and themes.

Finished #1379 on `job/linkpick-1379`. Head: `1f673ff0e1d946013c11e33da4566f288e7fd4c3` Commits: - `f5ac4930c feat(search): expose link actions in result menu (#1379)` - `1f673ff0e chore(perf): refresh exact Search scope pins (#1379)` ### Built - Search result Actions now includes **Insert link in note** when Search has a Note selection callback, and **Copy Markdown link** for every linkable result. - Both actions use the shared shortcut registry, Menu, MenuItem, Kbd caps, warm tooltips, and the quiet **Markdown link copied** toast. `markdownLinkForResult` is the shared Markdown writer, using the Note editor's escaping and link rules. - Capture-phase shortcut handling routes actions before Menu and global Search handlers. Programmatic Menu focus suppresses a focus ring until keyboard navigation. - The Playwright flow opens Cmd+K from a Note, selects an amount-free transaction Search hit, then checks both actions by menu and shortcut against saved Note Markdown and clipboard contents. The transaction hit is a test-only API fixture because Money has no Search provider in this branch. ### Gates ```text $ bun run build ✓ built in 2m 34s Run npm run preview to preview your production build locally. > Using @sveltejs/adapter-static Wrote site to "build" ✔ done [PLUGIN_TIMINGS] JavaScript callbacks ran for 156.1s. The build took 154.2s. Compressed 912 static variants; saved 21513415 bytes. $ bun run check perf-lint: PASS; 0 violations; 22543 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files $ bunx vitest run src/lib/search/linkPicker.test.ts --maxWorkers=2 Test Files 1 passed (1) Tests 6 passed (6) $ bun run test New design-rule violations: - packages/ui/src/components/TagPill.svelte:78: new hand-rolled button: tag-action touch-hit aria-haspopup={haspopup} aria-pressed={haspopup ? undefined : active} aria-label={(leaf || leafOnly) ? `#${tag} error: script "test" exited with code 1 ``` The full test command stops in `scripts/design-rules.test.mjs`, before Vitest. It compares against the current `origin/dev`; this branch merged `origin/dev` at `d3251a5a3`, after which `origin/dev` advanced to `d395b6e` and added the #1286 TagPill changes and design exception. `TagPill.svelte` is unchanged by #1379. The tracking ref is now 73 commits ahead. I did not repeat the once-only `origin/dev` merge. For the merge round, integrate current `origin/dev` and run `cd apps/web && bun run test` to verify the design guard and full Vitest suite. Playwright output: ```text PASS #1379: typed [[ and @, inserted indexed Notes/Tasks/Journal/Events/Bookmarks/Mail/Files/Photos, exercised transaction Search Actions by pointer and shortcut, copied Markdown in and outside a Note; screenshots: /home/kayg/Developer/calternal-wt/linkpick-1379/artifacts/linkpick ``` Local action profile (load average was 25.21/26.16/30.60): 50 indexed results, 20 displayed results, 25 warm copy actions; p50 60.2 ms, p95 84.2 ms, max 85.2 ms. Browser mean/peak RSS: 604,934,627/636,710,912 bytes; mean/peak CPU: 101.02%/206.93%. Server mean/peak RSS: 442,336,400/545,267,712 bytes; mean/peak CPU: 37.43%/138.19%. The existing baseline measures inline search (p50 0.3 ms, p95 0.4 ms, max 534.3 ms), so it is not a directly comparable action baseline. ### UX gaps Closed: pointer and keyboard actions save/copy the same canonical Markdown; the Menu items have accessible names, `aria-keyshortcuts`, and visible Kbd caps; the copy action works both from a Note and outside one; keyboard-opened Menu has no programmatic focus ring. macOS captures cover 390, 820, and 1440 px in light and dark, with pointer and shortcut Menu states. Left: live transaction discovery depends on a Money Search provider. The feature handles transaction Search results, but the end-to-end transaction result is injected only at the test API boundary until that provider exists. ### Decisions not in DESIGN - `⌥↩` is already Search Quick Look, so Insert uses the free `⌃↩` registry binding. - `⇧⌘C` opens Edge DevTools on macOS, so Copy Markdown uses the free `⌥⌘M` registry binding. ### Files - `apps/web/src/lib/components/search-dialog.svelte` - `apps/web/src/lib/copyLinkToast.ts` - `apps/web/src/lib/menu/MenuItem.svelte` and its tests - `apps/web/src/lib/notes/NoteEditorSurface.svelte`, `editorHost.ts`, `editorHost.svelte.test.ts`, and `editor-types/index.d.ts` - `apps/web/src/lib/search/{SearchField.svelte,kinds.ts,linkPicker.ts,links.ts,registry.ts,server.ts,window.svelte.ts}` and related tests - `apps/web/src/lib/shortcuts/registry.ts` and tests - `apps/web/e2e/linkpick-1379.mjs` - `packages/editor/src/{extensions.ts,formatCommands.ts,linkCommand.ts,linkPicker.ts,linkPickerExtension.ts,shortcuts.ts,slash.ts,suggestionPopup.ts}` and related tests - `packages/ui/src/components/menu/{FloatingSurface.svelte,MenuItem.svelte,types.ts}` - `bench/linkpick-1379.mjs`, `docs/perf/baseline.json`, `contracts/perf/{adoption-1058.json,exceptions.json}` ### Attached screenshots All 24 actual-size macOS screenshots are attached. Actions Menu captures include pointer and shortcut opening at 390, 820, and 1440 px in light and dark. Search insert and inline picker captures cover the same widths and themes. - Actions Menu, pointer: [1440 light](https://git.kayg.org/attachments/6333af94-055d-4492-b366-06e5a63d8c47), [1440 dark](https://git.kayg.org/attachments/7d20666f-14a8-4322-ab2c-6028b52d7815), [820 light](https://git.kayg.org/attachments/048063a9-8aa5-416c-95a1-bd1a2fb043af), [820 dark](https://git.kayg.org/attachments/a30be2cd-90b7-412c-9cdd-63582ba720f5), [390 light](https://git.kayg.org/attachments/d629d244-320e-4243-bcf6-f48f68c78654), [390 dark](https://git.kayg.org/attachments/7a1dd7c5-38ab-4ca6-9859-9773c49ff2ef). - Actions Menu, shortcut: [1440 light](https://git.kayg.org/attachments/59b5dd12-c946-4f90-891f-ab12571a5819), [1440 dark](https://git.kayg.org/attachments/634c1561-14d0-41de-9a09-b2b6debf7e9e), [820 light](https://git.kayg.org/attachments/767edb44-4a32-41bf-987d-43bfc7a684fb), [820 dark](https://git.kayg.org/attachments/abf5d367-9fc0-4336-8beb-acee923ae30c), [390 light](https://git.kayg.org/attachments/f811dd64-3c5d-47ce-84b3-6475012d3047), [390 dark](https://git.kayg.org/attachments/cff670fc-ff48-4bdb-affa-6b25d198b6af). - Search insert: [1440 light](https://git.kayg.org/attachments/28acf8f7-15c5-4ac1-921d-437794814fed), [1440 dark](https://git.kayg.org/attachments/d53856c6-3f59-4b36-9993-1bd3cfe07e46), [820 light](https://git.kayg.org/attachments/a3be5c61-974e-4040-a13b-2b2e8dad1e37), [820 dark](https://git.kayg.org/attachments/be4f643b-1ae1-40bb-b52c-c56ce9687d7c), [390 light](https://git.kayg.org/attachments/56af0473-4721-4f5f-aa70-63eb97427166), [390 dark](https://git.kayg.org/attachments/df8e023e-cddb-4551-8edd-08f2c05576a5). - Inline picker: [1440 light](https://git.kayg.org/attachments/eaccce3b-2ac7-444a-8986-9a5b2ea60d33), [1440 dark](https://git.kayg.org/attachments/343c6c45-1cca-471c-9da4-6608af3679bf), [820 light](https://git.kayg.org/attachments/48083011-b9b0-4d82-8516-24d7681ffa14), [820 dark](https://git.kayg.org/attachments/765d95d8-1aa6-4752-851c-70b25b9bc102), [390 light](https://git.kayg.org/attachments/e17111a8-ae4c-4dac-87f7-80b6207973da), [390 dark](https://git.kayg.org/attachments/7a06a60a-a068-4f02-a3cf-e4c06b9e3e07).
Author
Owner

Deployed to calternal.cloud in merge round 35a (build cc1af3c12, live 2026-10-09 00:12 IST). Closing.

Deployed to calternal.cloud in merge round 35a (build cc1af3c12, live 2026-10-09 00:12 IST). Closing.
kayg closed this issue 2026-10-08 18:41:59 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1379
No description provided.