From eda71c6b856343219160487bcdcd7be6fa8abf42 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 15 May 2026 12:23:40 -0700 Subject: [PATCH] Improve GitHub list search --- docs/github-list-search.md | 57 +++++++++++++++++++ src/renderer/src/components/TaskPage.tsx | 56 ++++++++++++++++++ .../github-project/ProjectViewWrapper.tsx | 53 +++++++++++++++-- src/shared/github-project-types.ts | 3 +- 4 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 docs/github-list-search.md diff --git a/docs/github-list-search.md b/docs/github-list-search.md new file mode 100644 index 00000000000..41c1dbdbf87 --- /dev/null +++ b/docs/github-list-search.md @@ -0,0 +1,57 @@ +# GitHub List Search + +## What the code does today + +- `TaskPage` has GitHub Items search input with Enter-to-apply and X-to-clear; it does not claim Cmd/Ctrl+F. +- `ProjectViewWrapper` has a local input (`ProjectSearchInput`) with Enter/blur apply and Esc restore+blur. +- Project X button is **reset to saved view filter**, not clear-to-empty. +- `queryOverride` semantics are: + - `undefined`: use `selectedView.filter` + - `''`: explicit empty filter for this fetch + - any other string: override filter for this fetch +- Shared type docs currently claim `''` and `undefined` are equivalent; that is incorrect. + +## Critical corrections + +1. Shortcut behavior is missing, not partially implemented. +- Add explicit Cmd/Ctrl+F capture in both GitHub Items and Projects modes. +- Use platform check (`navigator.userAgent.includes('Mac') ? metaKey : ctrlKey`) and never hardcode `metaKey`. +- Do not capture while dialogs/modals or editable overlays are active. + +2. Project clear behavior in this doc was wrong. +- Current code resets to saved filter; GitHub-like clear should apply `queryOverride: ''`. +- Keep Esc as the “revert to last applied” interaction. + +3. `queryOverride` contract in shared types is wrong. +- Update `GetProjectViewTableArgs.queryOverride` docs to match main-process behavior and cache key behavior. + +## Edge cases this must explicitly handle + +- IME composition: keep Enter suppression for composing text in GitHub Items and Projects. +- Repeated Cmd/Ctrl+F: always focus the active search box and select all text. +- Mode gating: never steal find when task source is not GitHub, or GitHub mode is not the visible mode. +- View switch race: do not let late responses overwrite the visible search state for a newer view. +- External mutations and multi-window drift: local optimistic edits and per-window cache are non-authoritative; refresh must remain available and predictable. + +## Consistency and concurrency notes + +- Renderer cache TTL for project tables is 60s; user-initiated apply already forces fetch (`force: true`), which is correct. +- Inflight dedupe is per-renderer process only. Separate windows can race and show temporarily different snapshots. +- No push invalidation exists for GitHub-side external changes; consistency is pull-based (refresh/fetch). +- `queryOverride` is part of cache keys; this is required so saved-filter and empty-filter results do not clobber each other. + +## Feasibility check + +- “One call” claims are not valid for most flows: + - `getProjectViewTable` can require multiple GraphQL pages for views, fields, and items. + - Discovery and view resolution are paginated. +- “Free” claims are not valid: + - Every apply/refresh consumes GitHub API budget; forced fetch intentionally bypasses fresh cache. + +## Required doc-aligned implementation outcomes + +1. Cmd/Ctrl+F focuses/selects GitHub Items search when GitHub Items mode is visible. +2. Cmd/Ctrl+F focuses/selects Project search when Project view mode is visible. +3. Project clear button applies empty override (`''`) immediately. +4. Esc in Project search restores last applied query and blurs. +5. Shared type comment for `queryOverride` matches runtime semantics. diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 7abc2f431ad..5197859ea39 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -869,6 +869,7 @@ export default function TaskPage(): React.JSX.Element { const [taskSearchInput, setTaskSearchInput] = useState(initialTaskQuery) const [appliedTaskSearch, setAppliedTaskSearch] = useState(initialTaskQuery) + const taskSearchInputRef = useRef(null) const [activeTaskPreset, setActiveTaskPreset] = useState( defaultTaskViewPreset ) @@ -1645,6 +1646,59 @@ export default function TaskPage(): React.JSX.Element { [handleApplyTaskSearch] ) + useEffect(() => { + if ( + taskSource !== 'github' || + githubMode !== 'items' || + dialogWorkItem || + drawerLinearIssue || + newIssueOpen || + newLinearIssueOpen || + activeModal !== 'none' + ) { + return + } + + const onKeyDown = (event: KeyboardEvent): void => { + const isMac = navigator.userAgent.includes('Mac') + const modifierPressed = isMac ? event.metaKey : event.ctrlKey + if (!modifierPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'f') { + return + } + + const input = taskSearchInputRef.current + if (!input) { + return + } + const target = event.target + if ( + target instanceof HTMLElement && + target !== input && + (target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target.isContentEditable) + ) { + return + } + + event.preventDefault() + event.stopPropagation() + input.focus() + input.select() + } + + window.addEventListener('keydown', onKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) + }, [ + activeModal, + dialogWorkItem, + drawerLinearIssue, + githubMode, + newIssueOpen, + newLinearIssueOpen, + taskSource + ]) + const openComposerForItem = useCallback( (item: GitHubWorkItem): void => { const linkedWorkItem: LinkedWorkItemSummary = { @@ -2285,6 +2339,8 @@ export default function TaskPage(): React.JSX.Element {
(initial) + const inputRef = useRef(null) const applied = appliedOverride !== undefined ? appliedOverride : viewFilter const dirty = value !== applied @@ -808,14 +809,55 @@ function ProjectSearchInput({ onApply(next === viewFilter ? undefined : next) } + useEffect(() => { + const onKeyDown = (event: KeyboardEvent): void => { + const isMac = navigator.userAgent.includes('Mac') + const modifierPressed = isMac ? event.metaKey : event.ctrlKey + if (!modifierPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'f') { + return + } + if (document.querySelector('[role="dialog"]')) { + return + } + + const input = inputRef.current + if (!input) { + return + } + const target = event.target + if ( + target instanceof HTMLElement && + target !== input && + (target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target.isContentEditable) + ) { + return + } + + event.preventDefault() + event.stopPropagation() + input.focus() + input.select() + } + + window.addEventListener('keydown', onKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) + }, []) + return (
setValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { + if (e.nativeEvent.isComposing) { + return + } e.preventDefault() apply(value) } else if (e.key === 'Escape') { @@ -835,13 +877,14 @@ function ProjectSearchInput({ dirty && 'border-amber-500/50' )} /> - {value && value !== viewFilter ? ( + {value ? (