diff --git a/openspec/changes/worktree-name-search/proposal.md b/openspec/changes/worktree-name-search/proposal.md new file mode 100644 index 0000000..8a949d3 --- /dev/null +++ b/openspec/changes/worktree-name-search/proposal.md @@ -0,0 +1,21 @@ +## 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. +- Web: a setting of the page the browser refuses to store (storage full or blocked) still applies, and the page shows that saving failed, instead of the handler throwing. + +## 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..6b8f00b --- /dev/null +++ b/openspec/changes/worktree-name-search/specs/web/spec.md @@ -0,0 +1,28 @@ +## 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. When the browser refuses to store a setting of the page, the change SHALL still apply and the page SHALL show that saving failed. + +#### 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 every worktree its checked filter lists, and the first repo shows its text and lists only the matching worktrees and the selected one + +#### 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 the checked filter lists is listed, with the selected one, 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 + +#### Scenario: Refused storage write +- **WHEN** the browser refuses to store the search and the user types text +- **THEN** the list is narrowed by the text and the page shows that saving its settings failed diff --git a/openspec/changes/worktree-name-search/tasks.md b/openspec/changes/worktree-name-search/tasks.md new file mode 100644 index 0000000..4ac3106 --- /dev/null +++ b/openspec/changes/worktree-name-search/tasks.md @@ -0,0 +1,9 @@ +## 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" +- [x] 1.3 The view's settings writes report a refused write on the page; e2e: a refused write still narrows the list and shows the notice + +## 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 ff443f2..d7d0c4e 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) => {