68c94dfa3a486c888a3a9dd3547695080e8d976e
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
68c94dfa3a |
feat: refine workbench chrome (#761)
* docs: define product and design direction * style: establish restrained workbench primitives * refactor: declutter board and task topbars * style: align dockview panel chrome * fix: align task topbar vcs control sizing * refactor: surface integrations navigation * fix: repair integrations and task creation workflow * refactor: include linear in integrations navigation * style: restore familiar workbench tones * fix: add dockview tab breathing room * fix: show configured integration shortcuts * fix: hover open integrations menu * fix: stabilize integrations hover menu * fix: align home and dockview topbars * fix: normalize home topbar controls * fix: compact home topbar height * fix: simplify home chrome and split buttons * fix: move integrations to left chrome * fix: restore home stats shortcut * fix: add dockview tab breathing room * fix: border home stats button * fix: streamline dockview topbar actions * fix: align task split button seam * fix: address review feedback * fix: restore topbar e2e affordances * fix: stabilize workflow e2e isolation * fix: add task topbar action overflow * fix: address PR review feedback |
||
|
|
046ac5c1b6 |
feat(web): add Ctrl+F search to session, plan, and terminal panels (#686)
* feat(backend): add session-scoped message search endpoint
Adds SearchMessages across repository/service/handler layers and a new
WebSocket action "message.search". Returns lightweight hits with a
60-char snippet around the first match. Session-scoped only: results
never leak across sessions. Uses dialect-aware LIKE/ILIKE so it works
on SQLite today and Postgres later.
* feat(web): add shared panel search bar and Ctrl+F hook
Introduces the reusable PanelSearchBar overlay and usePanelSearch hook
used by the per-panel search features. The hook listens on the window
and fires only when focus is within the panel root, preventDefaults the
browser find-in-page, and wires Esc to close. PanelRoot becomes a
forwardRef so panel components can expose their root element to the
hook. Adds SHORTCUTS.FIND_IN_PANEL plus shared highlight/flash styles.
* feat(web): add Ctrl+F search to terminal panels
Mounts the xterm addon-search on both passthrough and shell terminals,
exposing next/prev plus case-sensitivity and regex toggles. Invalid
regex surfaces an inline error without firing the search. Because xterm
consumes keystrokes before React, the Ctrl/Cmd+F intercept is added to
attachCustomKeyEventHandler so the shortcut opens the bar from inside
the terminal. Results counter comes from onDidChangeResults; decorations
clear on close.
* feat(web): add Ctrl+F search to plan panel
Adds a custom TipTap extension that builds a ProseMirror DecorationSet
over the document and exposes setPlanSearchQuery / planSearchNext /
planSearchPrev / clearPlanSearch commands. The current match gets an
extra class and scrolls into view. Match count is read from the plugin
state via transaction subscription, so edits to the document update the
counter automatically. Find-only — no replace.
* feat(web): add Ctrl+F search to session chat panel
Wires the new backend message.search action into a debounced client
hook and renders a hits dropdown under the search bar, with the query
highlighted in each snippet. Clicking a hit scrolls the message into
view and flashes it. When the target message is older than what is
currently loaded, the hook loops through loadOlder pages until the
message renders, making search work across lazy-loaded history without
needing to scroll-and-wait manually.
* fix(web): lazy-load xterm addon-search to avoid SSR crash
addon-search is published as a UMD bundle that references `self` at
module load. Statically importing it breaks the Next.js static
prerender of /. Deferring the import to an effect keeps the module out
of the SSR graph without changing runtime behaviour.
* fix(web): propagate plan editor via state so plan-search hook re-runs
The plan-search hook subscribed to editor transactions inside a
useEffect that depended on a ref object. Because ref.current changes do
not trigger effect re-runs, if the Editor instance was not yet ready
when the hook first mounted, the transaction handler never attached and
the match counter stayed at 0 / 0. Tracking the editor via state (in
addition to the ref) causes the deps to change when the Editor becomes
available, so the handler attaches and the counter updates.
* feat(web): allow overriding message list renderer via ?renderer= query
Exposes the "native" / "virtuoso" choice at runtime so E2E coverage can
exercise both paths without a redeploy or code change. Defaults to the
existing "native" strategy when no override is present.
* test(web): add Playwright coverage for panel-scoped Ctrl+F search
Adds an e2e/tests/search/ suite with five specs:
- panel-search-shared: cross-panel contract (Ctrl+F opens, autofocus,
Esc closes, close button, state reset on reopen) parametrised over
session, plan, and terminal.
- terminal-search: xterm addon-search integration — key intercept
without leaking "f" to the PTY, case + regex toggles (incl. invalid
regex error), Esc does not reach the shell, backspace adjusts count.
- plan-search: TipTap plugin — highlight count, exactly one current
match, counter, Enter / Shift+Enter wrapping, close clears
decorations, no case/regex toggles in plan bar.
- session-search: WebSocket-backed hits list, "No matches" state,
click-to-flash, cross-session isolation, close, and virtuoso renderer
coverage via the new ?renderer= override.
- panel-focus-routing: bar mounts inside the focused panel only.
Three tests capture GIFs for the PR description via prCapture when
CAPTURE_PR_ASSETS is set (one per panel). Helpers live in
e2e/helpers/panel-search.ts and e2e/helpers/seed-session-messages.ts;
SessionPage gains search-bar locators.
* fix(web): prevent plan-search update loop on spurious editor transactions
TipTap fires `transaction` on every cursor move/click. The match-info
handler allocated a fresh `{current, total}` object each time, so React
always saw new identity, re-rendered, and could cascade with downstream
effects (e.g. `rehydrateCommentMarks`) into a "Maximum update depth
exceeded" loop. Use a functional setState with equality bail-out so the
handler only schedules a render when the counter actually changes.
* fix(web): retry terminal search once xterm addon-search finishes loading
The SearchAddon is dynamic-imported and was stored in a ref. The
query→findNext effect called `addonRef.current?.findNext(...)` — a
silent no-op if the addon wasn't loaded yet — and its deps didn't
include addon readiness, so a query typed before the import resolved
was dropped forever. Track the addon via state so the search effect
re-runs when it becomes available (and re-binds cleanly if the terminal
is ever re-created). Also deduplicate match-info updates via functional
setState + equality check.
* fix(web): route Ctrl+F to the last-clicked panel when focus is ambient
The chat panel's message list has no focus-capturing child, so clicking
on a message left `document.activeElement === <body>` and the existing
`isFocusWithin` gate silently fell through to the browser's native find.
Fix with two complementary mechanisms:
- `usePanelSearch`: track the most-recently-clicked panel via a
window-level pointerdown capture listener. When focus is ambient
(body/html), that panel claims Ctrl+F. Window-level capture dodges
any descendant `stopPropagation` that would skip a container-level
listener.
- Chat panel root: tabIndex=-1 + onMouseDown that focuses the root when
the click target isn't already focusable. Normal path matches how
plan/terminal already work (TipTap/xterm capture focus on click).
* feat(web): show agent profile name and logo in session search hits
The hit list showed uppercase "AGENT"/"USER" with vivid blue/green
colors that clashed with the app's muted metadata aesthetic. Replace
with the actual author identity:
- Agent hits render the profile name (falling back to "Agent") with
the profile's <AgentLogo> inline — same mechanism session tabs use.
- User hits render "You" with a subtle `text-primary/80` tint that
echoes the user bubble's `bg-primary/30`, keeping a light color
differentiator without reintroducing loud blue/green.
Added `useSessionAgentIdentity` (plus a `useSessionAgentProfile` helper)
that reads from the store via two primitive selectors — the id, then
the profile — and composes the final object inside `useMemo`. A naive
single selector returning `{label, name}` triggers the "getSnapshot
should be cached" infinite loop in `useSyncExternalStore`.
* style: apply prettier formatting to search files
* fix(web): inline resolveStrategy in useMemo to satisfy react-hooks/use-memo
* fix(backend): escape LIKE metacharacters in message search
User query of `%` previously built LIKE pattern `%%` which matched every
message in the session; `file_name` also matched `filename`, `file name`,
etc. Escape the three LIKE metacharacters (`\`, `%`, `_`) before wrapping
the pattern, and declare `ESCAPE '\'` on the clause. Add regression tests
covering wildcard literals and the literal-percent happy path.
* fix(backend): slice search snippet in rune space to keep UTF-8 valid
`buildSnippet` indexed and sliced on byte offsets, so content containing
multi-byte characters (emoji, CJK, accented letters) could be cut
mid-rune — `encoding/json` then replaced the invalid bytes with `\ufffd`
in the websocket response. Switch to rune slicing and per-rune
case-folding via `unicode.ToLower`; this also avoids the
`strings.ToLower` byte-length-change pitfall (e.g. Turkish İ, German ß).
Add a handler test covering emoji, CJK, and large multi-byte content.
* fix(web): cancel backfill loop when user clicks a newer session hit
`setActiveHit` awaits up to 40 `loadOlder()` iterations when the target
message isn't in the DOM. If the user clicked a second hit while those
awaits were in flight, the in-progress loop kept running and eventually
scrolled to the original target, overriding the user's intent. Gate the
loop on a generation counter so only the most recent call's backfill
drives the scroll.
* fix(web): gate plan-search scroll side-effect on dispatch
TipTap invokes commands with `dispatch === undefined` during `can()`
checks to test applicability (e.g. before enabling a menu item). The
next/prev commands called `scrollToMatch` outside the `if (dispatch)`
guard, so a dry-run invocation would scroll the editor to a match the
user didn't navigate to. Move the scroll inside the dispatch block.
* fix(web): address review nits on search UX
- formatTime now checks Number.isNaN(d.getTime()) before formatting;
Invalid Date doesn't throw, so the catch branch was unreachable and
hits with a malformed created_at rendered the literal "Invalid Date".
- prefers-reduced-motion now silences the .search-flash animation
alongside the other custom keyframe animations.
- planScript escapes `title` symmetrically with `content` so callers
passing arbitrary strings don't produce malformed JSON.
* fix(web): fold description into opts to keep seedTask under param limit
`seedTask` had 6 parameters, violating the repo's 5-parameter cap and
failing CI lint. Fold `description` into the existing `opts` object and
update all callsites.
* test(web): add unit tests for useSessionSearch + remove redundant ternary
Cover the non-trivial hook logic called out in review: debounced fetch
coalescing, request-ID cancellation for superseded queries, null
sessionId short-circuit, and close() resets. Also simplify the
`matches.length ? 0 : 0` ternary flagged as a dead branch — both sides
were already 0.
* fix(web): exclude PanelRoot (tabindex=-1) from the focus short-circuit
`target.closest("[tabindex]")` was matching PanelRoot itself (which has
`tabIndex={-1}` so it can receive programmatic focus), causing the guard
to early-return and the `panelRef.focus()` call to never fire — defeating
the Ctrl+F focus-routing that this handler was added for.
Narrow the selector to `[tabindex]:not([tabindex='-1'])` so it only
short-circuits on genuinely interactive elements.
* fix(web): read isReadOnlyMode from live ref in shell-terminal key handler
`attachCustomKeyEventHandler` is called once and has no dispose hook, so
the inner handler keeps its initial closure forever. Other values
(sessionId, send, onFindInPanel) are read via `stateRef` so they stay
current, but `isReadOnlyMode` was captured by closure — if the prop ever
toggled mid-session, the Cmd+Arrow→Home/End path would continue firing
against a read-only terminal. Include it in the state ref for
consistency.
* fix(web): flatten doubly-nested seedTask opts in virtuoso spec; drop unused MODIFIER
The regex-based transform in
|
||
|
|
44811da702 | feat: web e2e tests (#304) | ||
|
|
6b57e5a01f | feat: vscode integration (#256) | ||
|
|
61ef224ec7 | style: fix format issues (#255) | ||
|
|
633c4e0e55 | feat: improve changes panel (#234) | ||
|
|
813bfd1215 | feat: dockview new ui (#219) |