From e8687a3496c69508b2967a39405e519f878f1148 Mon Sep 17 00:00:00 2001 From: Egor Kraev Date: Thu, 8 Oct 2026 14:15:09 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Worktree=20name=20search:=20a=20text=20box?= =?UTF-8?q?=20in=20the=20sidebar=20header=20lists=20only=20worktrees=20who?= =?UTF-8?q?se=20label=20contains=20its=20text,=20ignoring=20case,=20with?= =?UTF-8?q?=20a=20=C3=97=20to=20empty=20it;=20each=20repo=20keeps=20its=20?= =?UTF-8?q?own,=20across=20reloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../changes/worktree-name-search/proposal.md | 20 +++++ .../worktree-name-search/specs/web/spec.md | 24 ++++++ .../changes/worktree-name-search/tasks.md | 8 ++ src/web/main/styles.css | 53 ++++++++++++- src/web/ui/app.tsx | 38 +++++++++ src/web/ui/sidebar.test.ts | 78 ++++++++++++++++++- src/web/ui/sidebar.ts | 39 +++++++--- src/web/ui/view.ts | 23 +++++- test/e2e/contract.ts | 4 + test/e2e/sidebar.spec.ts | 63 +++++++++++++++ 10 files changed, 335 insertions(+), 15 deletions(-) create mode 100644 openspec/changes/worktree-name-search/proposal.md create mode 100644 openspec/changes/worktree-name-search/specs/web/spec.md create mode 100644 openspec/changes/worktree-name-search/tasks.md diff --git a/openspec/changes/worktree-name-search/proposal.md b/openspec/changes/worktree-name-search/proposal.md new file mode 100644 index 0000000..9f52c79 --- /dev/null +++ b/openspec/changes/worktree-name-search/proposal.md @@ -0,0 +1,20 @@ +## Why + +A repo with many worktrees makes the sidebar long; the "Starred" toggle narrows it only to worktrees starred beforehand, so finding one by its branch name means scrolling. + +## What Changes + +- Web: a text box in the sidebar header, between the title and the "Starred" toggle, listing only the worktrees whose label contains its text (trimmed, ignoring case), together with the "Starred" toggle; the selected worktree stays listed. A × beside it, shown while it holds text, empties it. Each repo keeps its own text, stored per browser across reloads (`wtd.searches`). A sidebar too narrow for one header row puts the box on its own row. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `web`: worktree name search. + +## Impact + +- Code: `src/web/ui/{sidebar.ts,view.ts,app.tsx}`, `src/web/main/styles.css`. +- Architecture, wire, upgrade: no change. +- Tests: `src/web/ui/sidebar.test.ts`; `test/e2e/sidebar.spec.ts` (search per repo, persistence, ×, combined with "Starred"). diff --git a/openspec/changes/worktree-name-search/specs/web/spec.md b/openspec/changes/worktree-name-search/specs/web/spec.md new file mode 100644 index 0000000..dc882ca --- /dev/null +++ b/openspec/changes/worktree-name-search/specs/web/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Worktree name search +The sidebar header SHALL hold, between its title and the checked filter's toggle, a text box for the selected repo: while it holds text, the sidebar SHALL list only the worktrees whose label contains that text, trimmed and ignoring case, that the checked filter also lists, and the selected worktree. While it holds text, a control beside it SHALL empty it. Each repo SHALL keep its own text, stored per browser across reloads; a repo without one SHALL list every worktree the checked filter lists. The text SHALL play no part in which worktree is selected without a stored selection. + +#### Scenario: Search narrows the list +- **WHEN** a repo has worktrees on `feature/login`, `fix/Logout` and `docs`, `feature/login` is selected and the user types `LOG` +- **THEN** the sidebar lists `feature/login` and `fix/Logout` only + +#### Scenario: Selected worktree stays listed +- **WHEN** the selected worktree's label does not contain the typed text +- **THEN** it stays listed and selected + +#### Scenario: Search belongs to its repo and persists +- **WHEN** text is typed in one repo, another repo is selected and the page is reloaded +- **THEN** the other repo shows an empty box and all its worktrees, and the first repo shows its text and lists only the matching worktrees + +#### Scenario: Clear control empties the search +- **WHEN** the user activates the clear control +- **THEN** the box is empty and focused, the clear control is gone, and every worktree is listed, also after a reload + +#### Scenario: Search with "checked only" +- **WHEN** the search is `feat` and the checked filter is "checked only" +- **THEN** only checked worktrees whose label contains `feat`, and the selected worktree, are listed diff --git a/openspec/changes/worktree-name-search/tasks.md b/openspec/changes/worktree-name-search/tasks.md new file mode 100644 index 0000000..1657fb2 --- /dev/null +++ b/openspec/changes/worktree-name-search/tasks.md @@ -0,0 +1,8 @@ +## 1. Web + +- [x] 1.1 `searchOf`, `withSearch`, `matchesSearch` and the search in `visibleWorktrees` (not in `defaultWorktree`) with unit tests; `wtd.searches` and `setSearch` in the view; the text box and × in the sidebar header +- [x] 1.2 e2e: the search lists matching worktrees ignoring case, keeps the selected one, belongs to its repo, persists across a reload, × empties it; it applies together with "checked only" + +## 2. Final gates + +- [x] 2.1 `pnpm test` green (incl. Playwright); `pnpm lint` exits 0; `openspec validate worktree-name-search --strict` passes diff --git a/src/web/main/styles.css b/src/web/main/styles.css index 1839816..399cfe2 100644 --- a/src/web/main/styles.css +++ b/src/web/main/styles.css @@ -122,6 +122,7 @@ input:focus-visible, overflow: hidden; background: var(--panel); border-right: 1px solid var(--border); + container-type: inline-size; } /* Over the sidebar's right border, so the terminal layer never covers it. */ @@ -145,13 +146,63 @@ input:focus-visible, .sidebar-header { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; + gap: 6px 10px; min-height: 38px; - padding: 0 10px 0 13px; + padding: 6px 10px 6px 13px; + box-sizing: border-box; border-bottom: 1px solid var(--border); } +/* Too narrow for one row: the search takes its own row below. */ +@container (max-width: 250px) { + .worktree-search { + order: 1; + flex-basis: 100%; + } +} + +.worktree-search { + position: relative; + display: flex; + flex: 1 1 60px; + min-width: 0; +} + +.worktree-search input { + width: 100%; + min-width: 0; + height: 22px; + box-sizing: border-box; + padding: 0 20px 0 7px; + font: inherit; + font-size: 12px; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 5px; +} + +.worktree-search input::placeholder { + color: var(--muted); +} + +.worktree-search button { + position: absolute; + top: 0; + right: 0; + height: 22px; + padding: 0 6px; + line-height: 1; + color: var(--muted); +} + +.worktree-search button:hover { + color: var(--text); +} + .sidebar-title { color: var(--muted); font-size: 11px; diff --git a/src/web/ui/app.tsx b/src/web/ui/app.tsx index f600095..13b7a55 100644 --- a/src/web/ui/app.tsx +++ b/src/web/ui/app.tsx @@ -966,6 +966,43 @@ const SidebarResizer = ({ view }: { view: View }) => { ); }; +/** The selected repo's worktree name search, with a button clearing it. */ +const WorktreeSearch = ({ view }: { view: View }) => { + const input = useRef(null); + const search = view.search.value; + return ( + + { + view.setSearch(event.currentTarget.value); + }} + /> + {search !== '' && ( + + )} + + ); +}; + const Workspace = ({ client, manager, view }: AppProps) => { const tab = view.tab.value; const repo = view.repo.value; @@ -977,6 +1014,7 @@ const Workspace = ({ client, manager, view }: AppProps) => {