Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b2c658f
chore(porch): 920 init pir
amrmelsayed May 29, 2026
517c4f4
[PIR #920] Plan draft
amrmelsayed May 29, 2026
8b41213
chore(porch): 920 plan-approval gate-requested
amrmelsayed May 29, 2026
6be2dc6
[PIR #920] Plan revised: Option B dedicated search endpoint
amrmelsayed May 30, 2026
6196d55
[PIR #920] Plan: functional Status dropdown (Decision 2 = a)
amrmelsayed May 30, 2026
ab22a6d
chore(porch): 920 plan-approval gate-approved
amrmelsayed May 30, 2026
e663f26
chore(porch): 920 implement phase-transition
amrmelsayed May 30, 2026
443c7a6
[PIR #920] Add GET /api/backlog-search endpoint + body-bearing data path
amrmelsayed May 30, 2026
c9a5a97
[PIR #920] Add Search Backlog editor-tab webview panel + command
amrmelsayed May 30, 2026
4ab9806
[PIR #920] Test searchBacklog filter/sort + formatAge helpers
amrmelsayed May 30, 2026
b659b6e
[PIR #920] Update builder thread: implement complete
amrmelsayed May 30, 2026
521c5f6
chore(porch): 920 dev-approval gate-requested
amrmelsayed May 30, 2026
4fdb619
[PIR #920] Extract webview markup/script to backlog-search.template.ts
amrmelsayed May 31, 2026
1ce709e
[PIR #920] Add inline 'reference in architect' action to search rows
amrmelsayed May 31, 2026
65ec47c
[PIR #920] Replace parameterized issue-list with dedicated issue-sear…
amrmelsayed May 31, 2026
997006f
[PIR #920] Update builder thread: dev-approval feedback rounds
amrmelsayed May 31, 2026
b5da139
[PIR #920] Rename backlog-search API to issue-search for resource-nam…
amrmelsayed Jun 1, 2026
4845f0e
[PIR #920] Update builder thread: issue-search rename
amrmelsayed Jun 1, 2026
aea41cf
Merge remote-tracking branch 'origin/main' into builder/pir-920
amrmelsayed Jun 1, 2026
cf06e4a
chore(porch): 920 dev-approval gate-approved
amrmelsayed Jun 1, 2026
749c706
chore(porch): 920 review phase-transition
amrmelsayed Jun 1, 2026
633a0bc
[PIR #920] Review + retrospective
amrmelsayed Jun 1, 2026
e8354e9
chore(porch): 920 record PR #957
amrmelsayed Jun 1, 2026
6af2ac2
chore(porch): 920 review build-complete
amrmelsayed Jun 1, 2026
b0ff468
[PIR #920] Fix stale-criteria desync (Codex review) + clamp regressio…
amrmelsayed Jun 1, 2026
58c43bb
chore(porch): 920 pr gate-requested
amrmelsayed Jun 1, 2026
b5cc2ba
chore(porch): 920 pr gate-approved
amrmelsayed Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions codev/plans/920-vscode-editor-tab-webview-for-.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# PIR Plan: VSCode editor-tab webview for rich backlog search

> Issue #920 Β· area/vscode Β· supersedes #906 Β· complements #918 (Quick Pick) and the shipped sidebar tree (#809 + #811).

## Understanding

The Backlog sidebar tree answers "what's on my plate" but offers no real search. #918 adds a fast single-pick Quick Pick. This issue adds the **deep-dive** surface: a persistent `WebviewPanel` (full editor tab, opened from a search icon in the Backlog view's title bar) for exploratory triage β€” scan, filter (Area / Assignee / Author), free-text search across **title + body**, sort by columns, refine without re-opening, with a match-count footer. Single instance, theme-aware via CSS variables only, row-click opens the issue via the existing `codev.viewBacklogIssue`.

### The one architectural fact that drives the plan

**The backlog data pipeline carries no issue body today.** The chain is:

- `issue-list` forge concept β€” `gh issue list --json number,title,url,labels,createdAt,author,assignees` (`packages/codev/scripts/forge/github/issue-list.sh`) β€” **no `body`**
- β†’ `IssueListItem` (`packages/codev/src/lib/forge-contracts.ts:32`) β€” no `body`
- β†’ `deriveBacklog` (`packages/codev/src/agent-farm/servers/overview.ts:800`) β†’ `BacklogItem` β†’ `OverviewBacklogItem` (`packages/types/src/api.ts:210`) β€” no `body`
- β†’ `/api/overview` β†’ `TowerClient.getOverview` β†’ `OverviewCache` (vscode) β€” no `body`

The acceptance criteria require substring search over **title AND body**. So body must be sourced through Tower (the extension never shells out to `gh` directly β€” it only talks to Tower's HTTP API). **Resolved (Decision 1): a dedicated `GET /api/backlog-search` endpoint supplies body on a fresh fetch, leaving `/api/overview` untouched.**

Everything else (table, dropdowns, sort, footer, singleton panel, theming) is local to a new vscode webview and well-supported by existing patterns (`view-issue.ts` for lifecycle/refresh; `backlog-filter.ts` for vitest-tested pure helpers; existing `view/title` menu wiring for the icon).

## Design Decisions

### Decision 1 β€” Where does `body` come from? **(DECIDED: Option B β€” dedicated search API)**

A **dedicated `GET /api/backlog-search` Tower endpoint** supplies the searchable dataset. It does a **fresh fetch each time it's hit** (no 30 s overview cache reuse) and returns the open backlog **with full, un-truncated `body`**. `/api/overview` and `OverviewBacklogItem` are left **completely unchanged** β€” the web dashboard and sidebar tree see no payload growth, and there are no Option-A truncation/cache hacks.

Concretely:
- `issue-list` forge concept gains `body` in its `--json` fields (`packages/codev/scripts/forge/github/issue-list.sh`); `IssueListItem.body?: string` (`forge-contracts.ts`). Optional field β€” GitHub-populated; other forges degrade to title-only.
- The new endpoint runs its **own** issue fetch (bypassing the shared 30 s `issueCache`, so it's always fresh) and projects the open backlog via the existing `deriveBacklog` logic plus `body`.
- A new response type (`BacklogSearchItem` = the searchable record incl. `body`) + `TowerClient.searchBacklog(...)` method. **`OverviewBacklogItem` does NOT gain `body`** β€” body lives only on the search path.

**Filtering runs host-side, not per-keystroke server-side.** The endpoint is hit **once when the panel opens** (and on manual refresh / `OverviewCache.onDidChange`), returning the in-scope dataset (with body); the actual substring + scope + sort filtering happens in the extension host via pure vitest-tested helpers, so live typing is instant with no network per keystroke. (A fresh `gh` fetch on every debounced keystroke would add ~1 s latency β€” unacceptable for live search.) Body crosses to the host but **never to the webview** β€” the host posts back only display rows (`#`, title, area, assignee, age).

### Decision 2 β€” Status: Open / Closed / All **(DECIDED: functional)**

The query row carries a **functional `Status: Open / Closed / All` dropdown**, defaulting to **Open**. The `/api/backlog-search` endpoint takes a `state` param (`open` | `closed` | `all`) that maps to `gh issue list --state <state>`. Notes:
- **Open** (default) reproduces the sidebar's set: open issues with no active PR. **Closed / All** lift that PR-exclusion filter (a closed issue typically *has* a merged PR β€” excluding them would make Closed near-empty), so for `closed`/`all` the endpoint returns the raw issue set for that state, projected the same way (minus the `prLinkedIssues`/builder exclusions that only make sense for "available work").
- Changing Status re-hits the endpoint (it's the one criterion that changes the server-side fetch, vs. the host-side text/scope/sort filters). The other controls stay host-side and instant.

### Decision 3 β€” Command name (collision with #918)

#918 (Quick Pick) has no code yet; `codev.searchBacklog` is unclaimed but is the natural name for the muscle-memory fast path. **Recommendation:** name this panel's command **`codev.openBacklogSearch`** (title "Codev: Search Backlog") and leave `codev.searchBacklog` for #918. Gate confirms.

### Decision 4 β€” Age column format

**Recommendation:** compact relative age derived from `createdAt` β€” `3d`, `2w`, `5mo`, `1y` (no "ago" suffix in a dense table; full ISO date in the cell tooltip). Sort on the underlying timestamp, not the formatted string.

### Decision 5 β€” Free-text semantics & a Comments column

- **Recommendation:** text query = pure case-insensitive substring over `title + body`. Scopes (Area/Assignee/Author) AND together and AND with the text query. Typing `area/vscode` in the text box matches it as a **substring** (in title/body) β€” it does **not** secretly drive the Area dropdown (keeps semantics one obvious thing; use the dropdown to filter by area). No fuzzy matching (that's Quick Pick's job).
- **No Comments-count column** in v1 β€” the backlog data carries no comment count and adding one widens the data change for marginal value. Columns: `#`, `Title`, `Area`, `Assignee`, `Age`.

## Proposed Change

### Architecture

1. **Dedicated search endpoint supplies body** (per Decision 1, Option B). `GET /api/backlog-search` does its own fresh issue fetch (with `body`, bypassing the 30 s overview cache), projects the open backlog via `deriveBacklog` + `body`, returns `BacklogSearchItem[]`. `/api/overview` and `OverviewBacklogItem` are untouched. The panel calls it via `TowerClient.searchBacklog(...)`.
2. **Filtering runs host-side in pure, vitest-tested helpers** in `backlog-filter.ts` β€” matches the issue's stated test plan. The endpoint is hit to fetch the body-enriched dataset on panel open, on refresh / `OverviewCache.onDidChange`, **and whenever the Status dropdown changes** (`state` is the one criterion the server resolves β€” see Decision 2). The host then filters/sorts the fetched dataset in-memory per the text/Area/Assignee/Author/sort criteria. The webview is a thin view: it renders controls + table, **debounces (~150 ms) and posts the current criteria** to the extension host, and renders the rows the host posts back. Body never ships to the webview β€” only matched display rows cross the boundary.
3. **Singleton `WebviewPanel`** owned by a `BacklogSearchPanel` class. `createOrShow` focuses the existing panel if open, else creates one in `ViewColumn.Beside` with `enableScripts` + a strict CSP (nonce'd inline script, `localResourceRoots` scoped). HTML/CSS/JS **inlined as a template string** (no runtime file read β†’ no esbuild copy-asset step). CSS variables only (`--vscode-*`).
4. **Message protocol** (typed): webview→host `{type:'search', criteria}` and `{type:'open', id}`; host→webview `{type:'results', rows, footer}`. `open` runs `vscode.commands.executeCommand('codev.viewBacklogIssue', id)` — identical to a sidebar row click.
5. **Live data:** the panel subscribes to `OverviewCache.onDidChange` and re-runs the current criteria so results stay fresh while open; disposes the subscription with the panel.

### Empty-state / cap behavior

Empty query + scopes β†’ all in-scope matches. Empty query + empty scopes β†’ everything, **capped at 200** with a "Load more" affordance (the underlying issue-list is already `--limit 200`, so this is effectively the whole open backlog; the cap + footer note keep the contract explicit if the limit ever rises).

## Files to Change

**Search endpoint + body source (server):**
- `packages/codev/scripts/forge/github/issue-list.sh` β€” add `body` to `--json` fields (optional; GitHub-populated).
- `packages/codev/src/lib/forge-contracts.ts:32` β€” `IssueListItem.body?: string`.
- `packages/types/src/api.ts` β€” new `BacklogSearchItem` (open-backlog record incl. `body`) and the `/api/backlog-search` response type. **`OverviewBacklogItem` unchanged.**
- `packages/codev/src/agent-farm/servers/overview.ts` β€” a body-enriched projection (reuse `deriveBacklog`; carry `body`) and a **fresh** issue fetch that bypasses the 30 s `issueCache`.
- `packages/codev/src/agent-farm/servers/tower-routes.ts:152` β€” register `GET /api/backlog-search` (`handleBacklogSearch`), takes `workspace` (+ `state` param iff Decision 2 = functional Status).
- `packages/core/src/tower-client.ts:314` β€” new `searchBacklog(workspacePath, …)` method (sibling of `getOverview`).

**VSCode panel (new):**
- `packages/vscode/src/webviews/backlog-search-panel.ts` β€” `BacklogSearchPanel` (singleton lifecycle, CSP/nonce HTML, message routing, fetch-on-open + `OverviewCache.onDidChange` refresh subscription).
- `packages/vscode/src/webviews/backlog-search.html.ts` *(or inlined in the panel)* β€” HTML/CSS/JS template, CSS-variables-only.

**VSCode wiring:**
- `packages/vscode/src/views/backlog-filter.ts` β€” add `searchBacklog(items, criteria)` + `formatAge(createdAt)` pure helpers (multi-dimension: text/area/assignee/author + sort), alongside existing `filterMine`.
- `packages/vscode/src/extension.ts` β€” register `codev.openBacklogSearch`; pass `OverviewCache` + `ConnectionManager` to the panel.
- `packages/vscode/package.json` β€” declare `codev.openBacklogSearch` (icon `$(search)`), add a `view/title` entry `when: view == codev.backlog` (group `navigation` alongside the eye/refresh icons).

**Tests:**
- `packages/vscode/src/test/` (or existing `backlog.test.ts` sibling) β€” vitest unit tests for `searchBacklog` (each scope, AND-composition, substring case-insensitivity, body match, sort directions, empty-query passthrough, 200 cap) and `formatAge`.

## Risks & Alternatives Considered

- **Risk β€” change spans more than `area/vscode`.** The dedicated endpoint (Option B) touches the forge concept, a core type, the Tower server, and `TowerClient` β€” not just vscode. This is unavoidable: the extension can't shell to `gh`, so body must come through Tower. Mitigation: all additive (a new endpoint + optional field), `/api/overview` and every existing consumer untouched, web-dashboard payload unchanged.
- **Risk β€” stale data while the panel is open.** The endpoint fetches fresh on open but the host filters a snapshot. Mitigation: re-fetch on `OverviewCache.onDidChange` (throttled, like `view-issue.ts`) and on the explicit Search button, so an open panel tracks backlog changes.
- **Risk β€” first webview in the extension β†’ CSP/theming pitfalls.** Mitigation: strict nonce'd CSP, `localResourceRoots`, CSS variables only; manually verified across dark/light/high-contrast at the `dev-approval` gate (PR diff can't catch theme regressions β€” the core PIR justification).
- **Risk β€” `#918` command-name race.** Mitigated by Decision 3 (`codev.openBacklogSearch`).
- **Alternative rejected β€” Option A (body on the shared `/api/overview`).** Smaller surface but bloats the always-on overview payload for the web dashboard and forces truncation/cache compromises. Rejected in favor of isolating body on a dedicated, on-demand search path.
- **Alternative rejected β€” server-side filtering per keystroke.** A fresh `gh` fetch (~1 s) on every debounced keystroke makes live search unusable. The endpoint supplies the dataset once; the host filters in-memory.
- **Alternative rejected β€” webview-side filtering in JS.** Would need full body shipped into the webview and duplicates/untestable logic. Host-side pure helpers (the issue's own skeleton) keep body off the webview and stay vitest-testable.
- **Alternative rejected β€” per-issue `/api/issue` body fetch for search.** 200 round-trips per query; non-starter.

## Test Plan

**Unit (vitest, `pnpm --filter @cluesmith/codev build` + test):**
- `searchBacklog`: text substring (title-only, body-only, both; case-insensitive); each scope dropdown; scopes AND-composed; empty-query + scopes; empty-query + empty-scopes returns all up to cap; sort asc/desc per column (esp. Age by timestamp).
- `formatAge`: day/week/month/year thresholds; just-created.

**Manual at `dev-approval` (running worktree β€” reviewer exercises these):**
1. Search icon visible in Backlog title bar; click opens a **`Search Backlog`** tab to the side of the active editor.
2. Tab shows the three scope dropdowns, the query row, the sortable results table, and the match-count footer.
3. Typing filters live (~150 ms debounce); Search button also submits. Column-header clicks sort with an arrow indicator on the active column.
4. Empty query + a scope β†’ scoped matches; empty query + no scope β†’ all (≀200, Load-more present if applicable). Switching **Status** Openβ†’Closedβ†’All re-fetches and the result set changes accordingly.
5. Click a result row β†’ the issue opens via `codev.viewBacklogIssue` (same as a sidebar click).
6. Re-invoke the command while open β†’ focuses the existing panel (no duplicate tab).
7. **Theme sweep:** dark, light, high-contrast all render cleanly β€” no hand-coded colors.
8. **No regression:** sidebar tree, mine/all toggle (#809), area grouping (#811) all still behave; Quick Pick (#918) unaffected.

**Cross-platform:** N/A (desktop VSCode extension only).
30 changes: 30 additions & 0 deletions codev/projects/920-vscode-editor-tab-webview-for-/status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
id: '920'
title: vscode-editor-tab-webview-for-
protocol: pir
phase: review
plan_phases: []
current_plan_phase: null
gates:
plan-approval:
status: approved
requested_at: '2026-05-29T23:13:52.523Z'
approved_at: '2026-05-30T00:30:38.273Z'
dev-approval:
status: approved
requested_at: '2026-05-30T00:45:09.185Z'
approved_at: '2026-06-01T12:10:36.344Z'
pr:
status: approved
requested_at: '2026-06-01T12:28:05.634Z'
approved_at: '2026-06-01T12:30:46.727Z'
iteration: 1
build_complete: true
history: []
started_at: '2026-05-29T23:07:49.040Z'
updated_at: '2026-06-01T12:30:46.728Z'
pr_history:
- phase: review
pr_number: 957
branch: builder/pir-920
created_at: '2026-06-01T12:17:39.413Z'
pr_ready_for_human: false
1 change: 1 addition & 0 deletions codev/resources/arch.md
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,7 @@ The VS Code extension (`packages/vscode`) is a thin client over Tower's existing
- **TerminalLocation.Editor**: Terminals open directly in editor area via `ViewColumn.One` (architect) and `ViewColumn.Two` (builders). Uses stable VS Code API, not the undocumented `moveIntoEditor` command.
- **Subpath exports**: `codev-core` uses subpath exports (`./tower-client`, `./escape-buffer`, etc.) to prevent Node builtins from leaking into the dashboard's Vite build.
- **Injectable auth**: `TowerClient` accepts a `getAuthKey` callback. CLI uses `ensureLocalKey()` (creates key if missing). Extension uses `readLocalKey()` + `SecretStorage` (never creates keys).
- **Editor-tab webviews (#920)**: Richer-than-TreeView surfaces use `vscode.window.createWebviewPanel` (editor area), not a sidebar `WebviewView`. Pattern: the panel is a thin view that posts debounced criteria to the extension host; **filtering/sorting runs host-side** in vscode-free pure helpers (`views/backlog-filter.ts`, vitest-tested) so logic stays testable and sensitive data (e.g. issue bodies) never crosses into the webview β€” only display rows do. HTML/CSS/JS live in a sibling `*.template.ts` (no esbuild asset-copy step); theming is **CSS variables only** (`--vscode-*`) so dark/light/high-contrast render natively; CSP is nonce'd. First instance: the "Search Backlog" panel (`webviews/backlog-search-panel.ts`), fed by the dedicated `issue-search` forge concept β†’ `GET /api/issue-search` (kept separate from `issue-list` so `/api/overview` stays body-free).
- **Startup CLI preflight (#791)**: On `activate()` the extension verifies the `codev` CLI is installed and at least its own `package.json` version (`codev --version`, resolved like `resolveAfxPath`, cached per session, 400ms-bounded, fire-and-forget so activation never blocks). Missing β†’ `Get started with Codev` walkthrough; outdated β†’ upgrade notification; either dismissed β†’ CLI-dependent commands no-op with one "run setup" toast. Commands register through two helpers β€” `reg` (unguarded) and `regCli` (guarded) β€” so the registrar name *is* the guard policy (no separate list). Preflight also sets the `codev.cliReady` context key, which drives the walkthrough's Verify-step completion. Lives in `src/preflight/` (`preflight-core.ts` pure + unit-tested, `preflight.ts` vscode glue).

## Repository Dual Nature
Expand Down
2 changes: 2 additions & 0 deletions codev/resources/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Generalizable wisdom extracted from review documents, ordered by impact. Updated

## Architecture

- [From #920] When a feature needs *extra* data or a *different* query from a shared forge concept (e.g. issue `body`, or `--state closed`), add a **dedicated concept** (`issue-search`) rather than bolting opt-in env flags onto the shared one (`issue-list`). Parameterizing the shared concept couples a feature's needs into a primitive other paths depend on, and β€” worse β€” only the GitHub script honored the new flags, so non-GitHub forges would have *silently returned wrong results* (e.g. open issues when "Closed" was requested). A dedicated concept keeps the shared one byte-for-byte unchanged and degrades **honestly**: a forge that hasn't implemented `issue-search` returns null β†’ the UI shows "search unavailable" instead of wrong data. Extends the forge-agnostic-layer lesson [From #909].
- [From #920] Name by layer, not by feature: an HTTP route/concept/lib fn is a *resource* (`/api/issue-search`, `searchIssues`) and should match its sibling resource endpoints (`/api/issue`); only the user-facing UI keeps the *feature* name ("Search Backlog" panel). Mixing the two (a `backlog-search` route feeding an `issue-search` concept) creates a self-inflicted vocabulary seam.
- [From 810] The builder-overview shape is defined twice β€” the `OverviewBuilder` wire type (`packages/types`) and a structurally-identical local `BuilderOverview` interface in `overview.ts`, kept in sync by hand. Adding a field to only the wire type compiles for clients (vscode/dashboard) but breaks the codev build at the server-side `builders.push({...})` sites. Compounding footgun: the codev package has no `check-types` script, so the mismatch is invisible until a full `pnpm build` runs `tsc` over `codev/src` β€” vscode/dashboard type-checks pass meanwhile. When touching the overview projection, build the codev package, not just the client type-check.
- [From 0395] Prompt-based instructions beat programmatic file manipulation for flexible document generation β€” the Builder already has context and can write natural responses, while code would need fragile parsing and placeholder logic
- [From 0395] Keep specs and plans clean as forward-looking documents β€” append review history (consultation feedback, lessons learned) to review files, not to the documents being reviewed
Expand Down
Loading
Loading