Skip to content

feat(web): make the palette a reliable attention switcher - #1377

Merged
danyaberezun merged 6 commits into
mainfrom
attention-switcher-audit
Nov 14, 2026
Merged

danyaberezun merged 6 commits into
mainfrom
attention-switcher-audit

Conversation

@danyaberezun

@danyaberezun danyaberezun commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The search palette (Mod+K / double Shift) is how the user switches attention, but as a switcher it misled:

  • Order. Inside an urgency level, "Waiting for you" fell back to project order and then to whatever order session states happened to load in. That matched neither recency nor the rail's arrangement.
  • Chats without a project trailed every repository and were subtitled with the container's raw stored name Chats (not localized, no hint that they belong to no project).
  • Which project. A chat row's second line, where the project and workspace are named, was the faintest text in the row.
  • Recent showed only palette-pick frecency, so a chat opened from the rail never appeared in the empty palette unless it needed attention.

Approach

  • Freshness. buildWorkItems emits chats and workspaces newest activity first, so Waiting's stable urgency sort, the @ listing and equal-score ties all read newest first.
    • Activity is the workspace's host-stamped lastActiveAt: agent runs, terminal writes and HEAD moves, at most a minute coarse. It's the signal the topbar switcher and the Chats section already sort by.
    • Equal stamps follow the rail: the Chats container first, then projects in the user's order, then Default, then the newest created workspace.
    • Rejected: a per-chat event time on SessionStateRecord. It would be exact, but needs a contract and host change for the rare tie of two waiting chats in one workspace.
  • Recent = where you have been, by any route (rail, topbar, link, palette), plus anything run from the palette. Newest first; never the place you're in now, never a row Waiting or Actions already shows.
    • initSearchPreferences watches the store's location (the attention center chat tab; in a chat without a project, its one chat even before its layout resolves; else the workspace; else Project Home) and stamps a client-local lastUsed per id.
    • A chat's visit replaces its workspace's stamp, and picking a row the current place already covers (its workspace, from its chat) stamps the place instead, so one place is one row.
    • Visits order Recent only: typed-query ranking still learns from palette picks, so habitual places never outrank what a query has learned.
  • Row location. The chat title stays the main line. The project and workspace in the second line are drawn one colour step brighter, with dim separators. Only the colour changes, per the typography rule.
    • A chat without a project reads No project, in all 7 locales. Host transcript and plan hits use the same label.

Changes

  • apps/web/src/search/workItems.ts: freshness and rail-order sort; located() computes the subtitle emphasis spans from the interpolated values, so a locale template stays free to word itself.
  • apps/web/src/search/learning.ts: optional lastUsed map in the existing learning snapshot, capped like frecency. A snapshot without it reads as empty, so there's no version bump. recordVisit, and recordPick now stamps lastUsed too.
  • apps/web/src/search/places.ts (new): derives the current place from the store. preferences.ts stamps visits; sections.ts builds Recent from lastUsed minus the current place.
  • apps/web/src/search/SearchPalette.tsx: RowTitle's muted lead and the new PaletteItem.subtitleEmphasis share one highlightSpans splitter. A pick reads the shared snapshot, so it can't overwrite a visit stamped while the palette was open.
  • apps/web/src/shell/useSearchPalette.ts: the host-hit project name for the Chats container uses the new label.
  • packages/locales/*: search.item.noProject.
  • apps/web/src/search/SPEC.md: Freshness, Recent, row location, Preferences bound.
  • Migration: Recent starts empty after the upgrade and fills as the user moves around; there's no frecency fallback.
  • Deferred: visits boosting typed-query ranking; exact per-chat event times from the host.

Screenshots

Same scenario, same viewport: one chat without a project and two finished chats in the project folder, all unread.

Before After
Waiting for you before after
After opening the chat without a project from the rail, then the project folder before after

Checklist

  • Fast gates pass: bun run lint, bun run typecheck, bun run test
  • E2E suite passes for app-affecting changes (bun run e2e, or bun run e2e:full when touching agent behavior)
  • Before/after screenshots are included for frontend changes, or marked not applicable
  • Relevant SPEC.md / top-level specs updated to reflect any boundary, contract, or behavior change
  • I have read the Contributing guide and agree to the Code of Conduct

Testing (on the rebased head):

  • bun run lint, bun run typecheck (26/26), bun run test (25/25) → green
  • bun run check:deps, check:boundaries, check:seams, check:spec-surface, check:i18n, check:compiler → OK
  • bun test apps/web/src/search → 80 pass. New tests cover freshness and rail-order ties, the No project label and emphasis spans, visits and lastUsed, Recent ordering and exclusion, and currentPlaceIds.
  • New e2e "a chat without a project waits labelled No project, and a chat opened from the rail is Recent"; bun run e2e e2e/search-palette.spec.ts e2e/chats.spec.ts → 32 passed
  • Full bun run e2e (8 lanes) is not green on the dev machine, on the base commit either. Terminal shells never start (data-failed="true"), with knock-on failures in tool-rails, workspace-actions and voice-input: 35 failures on the unmodified base, 38 on this branch. The extra four (ask-user-question, extension-dialogs, no-webassembly, plan-ask) pass when rerun serially.
  • New e2e repeated: bun run e2e e2e/search-palette.spec.ts -g "No project" --repeat-each=50 → 50 passed (it had flaked 1 in 15 before the project-less chat's place stopped depending on its layout)
  • CI's complete no-agent E2E (shards 1–3) → see the checks on the current head.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

Comment thread apps/web/src/search/preferences.ts Resolved

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun force-pushed the attention-switcher-audit branch from 8922ff8 to 75e0f07 Compare November 14, 2026 19:51

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun merged commit 2cb3dc9 into main Nov 14, 2026
12 checks passed
@danyaberezun
danyaberezun deleted the attention-switcher-audit branch November 14, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant