2 Commits

Author SHA1 Message Date
d2f6c5c525 some hygiene 2026-08-09 23:39:35 -04:00
f758b53336 drop tasks dir 2026-08-09 22:29:24 -04:00
11 changed files with 9 additions and 557 deletions

View File

@@ -19,12 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This project bundles third-party components under their own licenses:
- **cava** (karlstav/cava, vendored under `cava/`) — MIT,
Copyright (c) 2015 Karl Stavestrand. See `cava/LICENSE-cava.txt`.
- **Bun runtime** (embedded in the standalone binary) — MIT.
- **OpenTUI** (`@opentui/core`) — MIT.

View File

@@ -26,18 +26,9 @@ external player with full transport control — all from your terminal.
- A terminal with UTF-8 and modern color support (kitty, iTerm2, WezTerm,
tmux, GNOME Terminal, etc.).
- An **audio player** on `PATH`. PodTui auto-detects in priority order:
| Player | Platforms | Seek | Speed | Position tracking |
|----------|----------------|:----:|:-----:|:------------------|
| `mpv` | any | ✔ | ✔ | ✔ (recommended) |
| `ffplay` | any | ✔ | ✘ | ✘ |
| `afplay` | macOS built-in | ✔ | ✔ | ✘ |
| `open`/`xdg-open` | any | ✘ | ✘ | ✘ |
Install `mpv` for the best experience (`brew install mpv`,
`sudo apt install mpv`, `pacman -S mpv`). You can force a specific backend
with `PODTUI_AUDIO_BACKEND=mpv|ffplay|afplay|system|none`.
- **mpv** on `PATH` for audio playback. PodTui drives mpv over JSON IPC, so
seek, speed, and position tracking all work. Without `mpv` on `PATH`,
playback is a silent no-op (the `none` backend).
## Installation
@@ -47,7 +38,7 @@ Linux (arm64/x64). Pick whichever fits your platform.
### 1. Homebrew (macOS)
```sh
brew install mikefreno/tap/podtui # requires mpv: brew install mpv
brew install mikefreno/tap/podtui
```
> The formula installs the standalone binary plus its two native libraries
@@ -98,8 +89,7 @@ symlink, and pulls in `mpv` (the sole audio backend) as a dependency.
>
> Publishing is on hold until [AUR account registrations](https://aur.archlinux.org)
> reopen (suspended while the AUR team works on suspicious-package
> moderation). Once a key can be registered, push `PKGBUILD` + `.SRCINFO`
> with `git push ssh://aur@aur.archlinux.org/podtui-bin` and update this note.
> moderation).
### 4. From source
@@ -177,12 +167,14 @@ default (`$XDG_CONFIG_HOME/podtui` if set).
| File | Purpose |
|------|---------|
| `feeds.json` | Your subscribed feeds (RSS/podcast sources) |
| `sources.json` | Custom feed sources |
| `config.json` | Unified settings (theme, playback speed, download path), feeds, and custom feed sources |
| `downloads.json` | Downloaded episode metadata |
| `keybinds.jsonc` | Keybinding remaps (see above) |
| `themes/` | Optional custom theme files |
Legacy `feeds.json`, `sources.json`, and `app-state.json` are auto-migrated
into `config.json` on first run.
Env overrides: `PODTUI_AUDIO_BACKEND`, `XDG_CONFIG_HOME`. Startup also reads
the same OpenTUI environment variables.

View File

@@ -1,54 +0,0 @@
# 01. Rearchitect nav model — remove the sidebar pane
meta:
id: yazi-remake-01
feature: yazi-remake
priority: P1
depends_on: []
tags: [implementation, nav-model, tests-required]
objective:
- Remove the always-on `SIDEBAR_PANE` concept from the navigation context so `activeTab` is plain tab state (not a pane), establishing clean parent|current|preview semantics for the yazi remake.
deliverables:
- `src/context/NavigationContext.tsx` — delete `SIDEBAR_PANE` constant and all references; `activeTab` is no longer a pane
- `src/utils/navigation.ts` — update `TabPaneCount` semantics; depth-tabs = 1 focusable pane (current), the 3 visible columns are a render concern not 3 panes
- Updated header/comment block describing the parent|current|preview model
- `swipe()` / `popDepth()` reworked: depth-tabs `l`=drill (`open`), `h`=pop (noop at depth 0); fixed-pane tabs `h/l` move between parent/current/preview
- Tab-enter resets focus to `DEPTH_CENTER_PANE` (current pane), not a sidebar
steps:
- Audit every reference to `SIDEBAR_PANE` across the codebase (grep)
- In `NavigationContext.tsx`: delete the `SIDEBAR_PANE = -1` export and the `focusedIndex`/`setFocusedIndex` SIDEBAR_PANE branch added previously
- Set the initial `activePane` signal and the tab-switch createEffect to reset to `DEPTH_CENTER_PANE` (the current pane), not `SIDEBAR_PANE`
- Rework `swipe()` to clamp to `[0, paneCount-1]` for fixed-pane tabs (the sidebar is no longer in the chain); depth-tabs don't use `swipe` for drill/pop (that lives in Shell dispatch)
- In `utils/navigation.ts`: confirm `TabPaneCount` reflects focusable content panes only (depth-tabs = 1, Search = 3, Player = 1); update `PANE_RATIO` leave-behind note (ratio change happens in task 02)
- Update the file header comment block to describe parent|current|preview
- Run `lens_diagnostics` on the two files
tests:
- Unit: `focusedIndex(DEPTH_CENTER_PANE)` on a depth-tab returns the top frame's focus; `setFocusedIndex` writes to the top frame (Arrange a tab with a 2-frame stack, Act by calling setFocusedIndex, Assert topFrame.focus updated)
- Integration: tab-switch effect sets `activePane` to `DEPTH_CENTER_PANE` (not -1); `swipe(-1, 3)` on a fixed tab clamps to 0 not -1
- e2e (harness): app boots with `nav.state.pane === 0` (current), not -1
acceptance_criteria:
- No symbol `SIDEBAR_PANE` exists anywhere in `src/`
- Initial `activePane` === `DEPTH_CENTER_PANE` (0)
- Tab-enter sets `activePane` to `DEPTH_CENTER_PANE`
- `swipe()` lower bound is 0 (no `-1`)
validation:
- `grep -rn "SIDEBAR_PANE" src/` returns nothing
- `bun run build` passes
- `lens_diagnostics` paths=[`src/context/NavigationContext.tsx`,`src/utils/navigation.ts`] severity=error → 0 findings
notes:
- This task unblocks 03/04/05/06. It must not delete `DEPTH_CENTER_PANE` — that constant is generalised to "the current pane" and retained
- `SIDEBAR_ACTIONS` (added in Shell in a prior turn) is removed in task 06 (the keybind rewrite), not here — but Shell will temporarily fail to compile after this task until 05/06 land; that's expected and the build command ignores type errors, so gate success on grep + targeted diagnostics, not the full build

View File

@@ -1,56 +0,0 @@
# 02. Build the reusable 3-pane layout primitive (1:3:3 ratio, stable parent slot)
meta:
id: yazi-remake-02
feature: yazi-remake
priority: P1
depends_on: []
tags: [implementation, layout, tests-required]
objective:
- Create one reusable `<YaziPaneRow>` primitive that renders three bordered columns (parent | current | preview) at a 1:3:3 grow ratio with a stable 1/7 parent slot even when blank, so every list tab shares an identical, layout-stable shell.
deliverables:
- `src/components/YaziPaneRow.tsx` — new component: props `parent`, `current`, `preview` (Solid JSX/accessors), `parentLabel`, `currentLabel`, `previewLabel`, `focused` (boolean, defaults to current)
- `src/utils/navigation.ts``PANE_RATIO` updated to `{ parent: 1, current: 3, preview: 3 }` (was `{ parent: 1, current: 4, preview: 3 }`)
- Each pane: bordered `scrollbox` + slim header label row (height=1)
- Parent pane keeps its 1/7 `flexGrow` slot even when empty (renders a muted placeholder, never `width:0`)
- Focus ring (border color = accent on current; muted `border` on parent & preview)
steps:
- Set `PANE_RATIO = { parent: 1, current: 3, preview: 3 }` in `utils/navigation.ts`
- Create `YaziPaneRow.tsx` exporting a component that lays out three `<box flexGrow={PANE_RATIO.x}>` columns in a row
- Each column: a height-1 header `<box>` with the label text, then a `<scrollbox height="100%" border borderColor=…>` rendering the passed children
- Thread a `theme` via `useTheme()` inside the primitive (don't require callers to pass colors)
- `focused` prop controls which column gets the accent border — default current; parent & preview always muted
- Ensure the parent column renders a muted placeholder box (e.g. a single `<text fg={muted}>—</text>` or empty) when its children are null, but critically keeps `flexGrow={PANE_RATIO.parent}` so width never collapses
- Add a JSDoc header describing the yazi 1:3:3 contract
- Run diagnostics on the new file
tests:
- Unit: the primitive renders three boxes with flexGrow 1/3/3 regardless of null children (Arrange null parent, render, Assert three columns present with correct flexGrow)
- Integration: toggling `focused` swaps the accent border onto the requested column
- e2e (harness): a page using the primitive shows three equal-ratio columns with the parent column visibly non-zero width even when blank
acceptance_criteria:
- `PANE_RATIO` is `{ parent: 1, current: 3, preview: 3 }`
- `YaziPaneRow` accepts parent/current/preview children + labels + focused
- Parent column width never collapses to 0 (stable 1/7 slot)
- Only the focused column shows the accent border
validation:
- `grep -n "PANE_RATIO" src/utils/navigation.ts` shows the new 1:3:3 values
- `lens_diagnostics` paths=[`src/components/YaziPaneRow.tsx`,`src/utils/navigation.ts`] severity=error → 0 findings
- Harness: render a throwaway page using `<YaziPaneRow>`; confirm 3 columns at 1:3:3 via the frame
notes:
- Independent of task 01 (no nav-state dependency) — can be built in parallel
- Callers (tasks 03/04) pass their own parent/current/preview JSX; the primitive is purely structural
- opentui scrollbox: use `focused` only on the current pane so scroll focus follows the cursor

View File

@@ -1,58 +0,0 @@
# 03. Convert Feed/MyShows/Discover/Settings to the shared parent|current|preview primitive
meta:
id: yazi-remake-03
feature: yazi-remake
priority: P2
depends_on: [yazi-remake-01, yazi-remake-02]
tags: [implementation, pages, tests-required]
objective:
- Rewrite the four depth-stack list tabs to render through `<YaziPaneRow>`, with the previous-depth list now visible in the parent pane (blank at depth 0), the current-depth list in current, and the hovered item in preview — eliminating per-page bespoke 3-column JSX.
deliverables:
- `src/pages/Feed/FeedPage.tsx` — rewritten to use `<YaziPaneRow>`; parent = previous-depth list, current = current-depth list, preview = hovered item detail
- `src/pages/MyShows/MyShowsPage.tsx` — same conversion
- `src/pages/Discover/DiscoverPage.tsx` — same conversion
- `src/pages/Settings/SettingsPage.tsx` — same conversion (sections → items → editor)
- Each page's `nav.action` handler retained but only acts on the current pane
- All per-page bespoke row/flexbox 3-column JSX removed
steps:
- For each of the four pages, read the current implementation to extract the parent/current/preview content builders
- Wrap the page body in `<YaziPaneRow parent={…} current={…} preview={…} focused={isActive} />`
- Parent pane: render the previous-depth frame's list (depth-1). At depth 0 the parent receives null/placeholder (the primitive keeps the slot)
- Current pane: the current-depth list, focusable, with `onMouseDown` row handlers calling `nav.setActivePane(DEPTH_CENTER_PANE)` + `nav.setDepthFocus(i, depth)`
- Preview pane: hovered-item detail derived from `focusedIndex(DEPTH_CENTER_PANE)` (unchanged logic, just relocated into the preview slot)
- Keep `pushDepth`/`popDepth` calls in the `open` action (drill) — behaviour unchanged, only layout changes
- Remove the old inline `<box flexGrow={PANE_RATIO.parent/current/preview}>` columns in favour of the primitive
- Verify each page's `nav.action` handler guards on `data.pane === DEPTH_CENTER_PANE && nav.activePane() === DEPTH_CENTER_PANE`
tests:
- Unit: each page's `open` action pushes a frame and the parent pane switches from blank to the previous list (Arrange depth 0, Act open, Assert stack length 2 and parent renders the old list)
- Integration: `h` (pop) returns parent to blank at depth 0; `l` (drill) populates parent with the previous list
- e2e (harness): Feed depth 0→1→2 shows parent blank → previous feeds list → previous episodes list; Settings sections→items→editor shows the chain in the parent pane
acceptance_criteria:
- All four pages render via `<YaziPaneRow>` (no bespoke 3-column JSX remains)
- Parent pane is blank at depth 0, populated at depth ≥ 1
- Drilling (l/Enter) populates the parent with the previous-depth list
- Popping (h) empties the parent back to blank at depth 0
- j/k move focus only within the current pane
validation:
- `grep -rn "YaziPaneRow" src/pages/` returns 4 files
- `lens_diagnostics` paths over the four page files severity=error → 0 findings
- Harness walk: `init` → navigate Feed → `l` (drill) → `l` (drill) → `h` (pop) → `h` (pop); confirm parent slot transitions blank→list→list→blank
notes:
- Depends on 01 (pane model) and 02 (the primitive) being merged
- The already-working `<Show when={item}>{(item) => (… item() …)}</Show>` accessor pattern for opentui `<Show>` callbacks must be preserved in preview panes
- Keep `LoadingIndicator` usages where they exist

View File

@@ -1,52 +0,0 @@
# 04. Fit Search and Player into the 3-pane (1:3:3) model
meta:
id: yazi-remake-04
feature: yazi-remake
priority: P2
depends_on: [yazi-remake-01, yazi-remake-02]
tags: [implementation, pages, tests-required]
objective:
- Bring the two fixed-layout tabs (Search, Player) into the same 1:3:3 parent|current|preview shell, deciding per-page whether to adopt the depth-stack or stay fixed-3-pane, while applying the new ratios throughout.
deliverables:
- `src/pages/Search/SearchPage.tsx` — rendered through `<YaziPaneRow>`; parent = query input + recent-search history, current = results list, preview = focused-result detail
- `src/pages/Player/PlayerPage.tsx` — rendered through `<YaziPaneRow>`; current = now-playing transport, preview = episode description/notes, parent = blank placeholder (or compact episode list if available)
- Decision recorded in each file's header comment: depth-stack vs fixed-3-pane
steps:
- Read both pages to understand their current pane semantics
- Search: map INPUT→parent, RESULTS→current, DETAIL→preview inside `<YaziPaneRow>`. If the 1/7 parent slot is too narrow for the input box, widen parent for Search only by passing an override ratio OR move the query into current and results into parent — pick the option that keeps the input usable and document it
- Search: keep the `inputFocused` effect (Shell yields keys to `<input>` when current-pane focus is on the query) — adapt to whichever pane the input lives in
- Player: single content pane; parent = blank/placeholder (1/7), current = transport + progress + controls (3/7), preview = episode art/description/notes (3/7). If no preview data, render a muted placeholder but keep the slot
- Confirm fixed-pane tab swipe (h/l between parent/current/preview) still routes correctly for Search
- Run diagnostics
tests:
- Unit: Search's `handleSubmit` swipes to the results pane and sets focus index 0 (Arrange empty results, Act submit, Assert activePane === results pane & focusedIndex 0)
- Integration: Player renders with parent blank and the transport in current
- e2e (harness): Search shows query | results | detail at 1:3:3; Player shows blank | transport | notes at 1:3:3
acceptance_criteria:
- Both pages render via `<YaziPaneRow>` at 1:3:3
- Search input remains typeable (Shell yields keys when the query pane is focused)
- Player's transport is in the current pane with focus
- No layout collapse: parent & preview keep their slots even if blank
validation:
- `grep -rn "YaziPaneRow" src/pages/Search src/pages/Player` returns 2 files
- `lens_diagnostics` paths over both files severity=error → 0 findings
- Harness: navigate to Search, type a query, press Enter, see results in current + detail in preview; navigate to Player, see transport + notes
notes:
- Depends on 01 (pane model — though Search is fixed-pane, the model cleanup affects `swipe` bounds) and 02 (the primitive)
- If Search input at 1/7 is genuinely too tight (~14 cols at 100w), prefer moving the query into the current pane for Search only and the results into parent — but confirm width with the harness before committing
- Player is single-content; the 1:3:3 with blanks is mostly cosmetic but keeps the layout globally consistent

View File

@@ -1,56 +0,0 @@
# 05. Rebuild Shell chrome — drop sidebar, add yazi bottom status/tab bar
meta:
id: yazi-remake-05
feature: yazi-remake
priority: P1
depends_on: [yazi-remake-01]
tags: [implementation, shell-chrome, tests-required]
objective:
- Remove the always-on left tab sidebar entirely and replace it with a full-width page area above a slim yazi-style bottom bar that surfaces the active tab, depth/counts, selection, now-playing, and a discoverable tab strip.
deliverables:
- `src/components/Shell.tsx` — sidebar JSX deleted; render `LayerGraph[tab]()` full-width + a rebuilt bottom status/command bar
- Bottom bar (normal mode): mode label, `TAB_LABEL[tab] · depth N · i/len` (or `pane i/n` for fixed tabs), selection count `●N`, now-playing `♪ title`, pending-keybind hint, and a compact tab strip `[1]Feed [2]MyShows …` with the active tab marked
- Bottom bar (command mode): `:` prompt + buffer + error (unchanged, just relocated if needed)
- Help overlay kept; now-playing relocated from the old sidebar footer into the status bar
steps:
- Read `Shell.tsx` and delete the entire left tab sidebar `<box flexDirection="column" width={14}>…` block
- Replace the middle row with a single full-width `<box flexGrow={1}>{LayerGraph[nav.activeTab()]()}</box>`
- Rebuild the bottom bar: a height-1 `<box flexDirection="row">` with the fragments described above
- Tab strip: render `Object.values(TABS)` filtered to numbers; for each tab show `[N] Label` with the active tab inverted/highlighted (accent bg or `≡` marker)
- Status fragment: `nav.activePane() === DEPTH_CENTER_PANE ? (isDepthTab ? \`depth ${currentDepth()}\` : \`pane ${activePane()+1}/${count}\`) : 'tabs'` — but since the sidebar is gone, default to the depth/pane string (focus starts on current)
- Relocate `nowPlaying()` text from the sidebar footer into the bottom bar
- Keep `runCommand`, `handleCommandKey`, the help overlay, and `playEpisodeAndSwitch` untouched
- Run diagnostics
tests:
- Unit: `nowPlaying()` formats `♪ <truncated title>` (Arrange a current episode, Assert the string)
- Integration: switching tabs updates the tab strip's active marker and the status tab label
- e2e (harness): `init` shows no left sidebar, a full-width page, and a bottom bar containing the tab strip + `Feed · depth 0`; cycling tabs moves the strip's active marker
acceptance_criteria:
- No `width={14}` sidebar `<box>` remains in `Shell.tsx`
- The active page fills the full content width
- The bottom bar shows the active tab, depth, counts, selection, now-playing, and the tab strip
- The active tab is visually marked in the strip
validation:
- `grep -n "width={14}" src/components/Shell.tsx` returns nothing
- `grep -n "LayerGraph" src/components/Shell.tsx` shows the full-width render
- `lens_diagnostics` paths=[`src/components/Shell.tsx`] severity=error → 0 findings
- Harness: `init` frame has no sidebar column and shows the tab strip in the last row
notes:
- Depends on 01 (the pane model: focus starts on current, so the status fragment no longer needs the `SIDEBAR_PANE` branch)
- Task 06 rewrites the dispatch keybinds in this same file; do the chrome here and leave the dispatch `SIDEBAR_ACTIONS` branch for 06 to remove (or remove it here if 01 already deleted the constant — coordinate with 01)
- `playEpisodeAndSwitch` and the command bar must keep working

View File

@@ -1,56 +0,0 @@
# 06. Rewire keybinds — h/l drill+pop, digits switch tabs, focus starts on current
meta:
id: yazi-remake-06
feature: yazi-remake
priority: P1
depends_on: [yazi-remake-01, yazi-remake-05]
tags: [implementation, keybinds, tests-required]
objective:
- Rewire the Shell dispatch so the sidebar's special-cased j/k branch is gone, h/l drill/pop on depth-tabs and swipe on fixed tabs, digit keys + `[ ]` are the sole tab switcher, and app focus starts on the current pane.
deliverables:
- `src/components/Shell.tsx` (dispatch) — `SIDEBAR_ACTIONS` set + the `if (nav.activePane() === SIDEBAR_PANE)` branch deleted
- `h`/`l` unified: depth-tabs `l`=current-drills (`open` emit), `h`=current-pops (noop at depth 0); fixed-pane tabs `h/l`=`swipe(∓1, count)`
- `1`-`6` / `tab-goto-*`, `tab-next`/`tab-prev` (`[`/`]`) — the only tab switchers
- Initial focus + tab-enter land on `DEPTH_CENTER_PANE`
- `keybinds.jsonc` reviewed (update labels/help only if needed)
steps:
- Read the current `dispatch()` (post task 01 it references a deleted `SIDEBAR_PANE` — fix the compile here)
- Remove the `SIDEBAR_ACTIONS` constant and its branch
- In the `default` case, implement: digit/tab-goto → `setActiveTab`; `swipe-prev` → (depth-tab & current & depth>0) `popDepth` else (depth-tab & current & depth==0) noop else `swipe(-1, count)`; `swipe-next` → (depth-tab & current) emit `open` else `swipe(1, count)`
- Move/list actions (`move-down/up`, `jump-*`, `page-*`, `goto-top/bottom`) flow to `PAGE_ACTIONS``emit("nav.action")` for the current pane only
- Confirm `escape`/`command`/`visual-mode`/`toggle-select`/audio/global branches unchanged
- Verify the app boot path sets focus to current (task 01 set the signal; confirm dispatch doesn't override)
- Run diagnostics + harness key sequence
tests:
- Unit: `dispatch("move-down")` on a depth-tab current pane emits `nav.action {action:"move-down"}` (Arrange current pane, Act, Assert emit)
- Integration: `dispatch("swipe-next")` on a depth-tab at depth 0 emits `open` (drill); `dispatch("swipe-prev")` at depth 1 pops to depth 0; at depth 0 `swipe-prev` is a noop
- e2e (harness): `l` drills (depth 0→1, parent populates), `h` pops (1→0, parent blanks), `1`/`2`/`3` switch tabs, `j`/`k` move the current list cursor without changing depth
acceptance_criteria:
- No `SIDEBAR_PANE` or `SIDEBAR_ACTIONS` references in `Shell.tsx`
- `h` at depth 0 is a noop (does not error, does not change pane)
- `l` at current on a depth-tab drills (depth+1)
- Digit keys switch tabs; focus lands on current pane
- `j`/`k` move within current only
validation:
- `grep -n "SIDEBAR" src/components/Shell.tsx` returns nothing
- `lens_diagnostics` paths=[`src/components/Shell.tsx`] severity=error → 0 findings
- Harness: `init` (focus on current) → `l` (depth 1, parent filled) → `l` (depth 2) → `h` (depth 1) → `h` (depth 0, parent blank) → `3` (Discover tab, focus on current) → `j`/`k` move
notes:
- Depends on 01 (pane model: `swipe` bounds, no SIDEBAR) and 05 (dispatch lives in the rebuilt Shell)
- If `keybinds.jsonc` has a `tab-next`/`tab-prev` mapping conflict, resolve here
- The noop `h` at depth 0 should feel inert (yazi: at root, `h` does nothing)

View File

@@ -1,98 +0,0 @@
# 07-blocker — Task 04 (Search + Player) never converted to YaziPaneRow
meta:
id: yazi-remake-07-blocker
feature: yazi-remake
priority: P0
blocks: [yazi-remake-07]
blocked_by: [yazi-remake-04]
tags: [blocker, verification, tasks-required]
## Problem
Task 04 (`04-fit-search-and-player-panes.md`) is marked complete, but its
core deliverable was never implemented:
> - `src/pages/Player/PlayerPage.tsx` — rendered through `<YaziPaneRow>`
> - `src/pages/Search/SearchPage.tsx` — rendered through `<YaziPaneRow>`
`grep -rn "YaziPaneRow" src/pages/Search src/pages/Player`**0 files**.
## Evidence (from task 07 harness walk-through, 100×30)
### Player ❌
`src/pages/Player/PlayerPage.tsx` renders a single full-width
`<scrollbox>` (now-playing transport + controls). No parent pane, no preview
pane. Frame `.harness/player-current.txt`:
```
│ Now Playing 0:00 / 0:00 (0%) │
...
│ │ │[Prev]│ │[Play]│ │[Next]│ Vol 70% Speed 1x ... │ │
```
Expected (task 04 + feature exit criteria): `blank | transport | notes` at
1/7 : 3/7 : 3/7.
### Search ⚠️ ratio wrong
`src/pages/Search/SearchPage.tsx` renders three custom `<box flexGrow={
PANE_RATIO.* }>` columns but **omits `flexBasis={0}`**, so Yoga distributes
space by natural content width (the input box is `width={28}`). Measured
column widths at 100 cols: **31 / 32 / 31** (equal thirds), NOT the target
**~14 / 43 / 43** (1:3:3).
`<YaziPaneRow>` exists precisely to set `flexBasis={0}` per column and force
the exact 1:3:3 ratio regardless of content (see its header comment). Routing
Search through it fixes the ratio for free.
## Why not patched in task 07
Task 07 is explicitly the verification gate. Its notes:
> - This is the gate for the whole feature — do not mark done if any criterion
> fails; open a blocker task instead
> - If the harness reveals a visual regression (e.g. parent collapses, ratios
> off), file it against the responsible task (02 or 03) rather than
> patching here
This is a missing implementation in task 04, not a regression in 02/03, so the
responsible task is 04. Patches belong there.
## Failing exit criteria
- "All tabs render three stable columns at 1/7 : 3/7 : 3/7" — Player fails
(not 3 columns); Search fails (wrong ratio).
- "`5` → Player: blank|transport|notes at 1:3:3" — fails.
- "`4` → Search: query|results|detail at 1:3:3" — 3 columns yes, ratio wrong.
## Fix plan (task 04 do-over)
1. `src/pages/Player/PlayerPage.tsx` — wrap the existing transport JSX in a
`<YaziPaneRow current={transport} parent={undefined} preview={notes} />`.
Parent should fall through to the primitive's muted `—` placeholder (it
already keeps its 1/7 slot when blank). Preview = episode description /
waveform (currently inline under "Now Playing"). Keep `PlayerPaneCount=1`
(the visible columns are a render concern; only current=0 is focusable).
2. `src/pages/Search/SearchPage.tsx` — replace the three custom `<box
flexGrow={PANE_RATIO.*}>` columns with a single `<YaziPaneRow
parent={queryInput+recent} current={resultsList} preview={detail}
focused={!inputFocused() ? /* results */ : false} />`. Keep the
`inputFocused` effect so the Shell yields keys to the native `<input>`
when the query pane is focused — note `inputFocused` is a Search-owned
signal; YaziPaneRow's `focused` prop only drives the accent ring + scroll
focus, which for Search can stay on the current (results) column.
3. Remove the now-dead custom ratio code from both files after the swap.
4. Re-run: `grep -rn YaziPaneRow src/pages/Search src/pages/Player` → 2 files;
`bun run build`; `bun test`; harness walk-through: Player shows 3 cols,
Search cols measure ~14/43/43.
## Verification gates (re-run task 07 after fix)
- `bun run build` → "Build complete"
- `bun test` → 0 fail
- harness:
- Player frame has 3 bordered columns (parent `—`, current transport,
preview notes/placeholder) at 1:3:3
- Search frame columns measure ~14/43/43

View File

@@ -1,61 +0,0 @@
# 07. Verify the remake — build + diagnostics + harness walk-through
meta:
id: yazi-remake-07
feature: yazi-remake
priority: P1
depends_on: [yazi-remake-03, yazi-remake-04, yazi-remake-05, yazi-remake-06]
tags: [verification, tests-required]
status: BLOCKED # see .harness/verification-07.md + tasks/yazi-remake/07-blocker-task-04-player-search.md
objective:
- Confirm the yazi remake meets every exit criterion via a clean build, zero diagnostics, and a full harness walk-through of every tab and depth.
deliverables:
- A passing `bun run build`
- `lens_diagnostics mode=all` with zero errors across edited files
- Harness frames + state proving the parent|current|preview 1:3:3 layout, drill/pop behaviour, tab switching, and status bar across all six tabs
steps:
- Run `bun run build` — expect "Build complete"
- Run `lens_diagnostics mode=all severity=error` — expect 0 findings across all session-edited files
- Run the drive harness (`scripts/tui-harness.tsx`) walk-through:
- `init` → confirm no sidebar, 3 columns at 1:3:3, focus on current, bottom tab strip visible
- Feed: `l` (depth 0→1, parent fills) → `l` (1→2) → `h` (2→1) → `h` (1→0, parent blanks) ; `j`/`k` move current
- `2` → MyShows: drill show→episodes, parent reflects
- `3` → Discover: category→results, parent shows categories
- `6` → Settings: sections→items→editor, parent shows the previous list at each depth
- `4` → Search: query|results|detail at 1:3:3; type + Enter works
- `5` → Player: blank|transport|notes at 1:3:3
- Capture the status bar content (active tab + depth + counts + now-playing + tab strip) from a representative frame
tests:
- Build: `bun run build` exits 0 with "Build complete"
- Diagnostics: `lens_diagnostics` mode=all → 0 errors
- Harness (integration/e2e): the walk-through above produces the expected frames & state (parent blank at depth 0, populates on drill, blanks on pop; digits switch tabs; h noop at depth 0)
acceptance_criteria:
- `bun run build` passes
- `lens_diagnostics` mode=all reports zero errors
- All six tabs render 3 stable columns at 1:3:3
- Parent pane is blank at depth 0; drill fills it with the previous-depth list; pop empties it
- `h` is a noop at depth 0; `l` drills; `1-6`/`[`/`]` switch tabs; `j/k` move current only
- No sidebar; focus starts on current; bottom bar shows active tab + depth + counts + tab strip
validation:
- `bun run build 2>&1 | tail -3` → "Build complete"
- `lens_diagnostics` mode=all severity=error → "No error issues…"
- Harness `state nav` after `init` shows `pane === 0` (current), not -1
- Harness frames for Feed depth 0/1/2 show the parent slot transition blank→list→list
notes:
- This is the gate for the whole feature — do not mark done if any criterion fails; open a blocker task instead
- If the harness reveals a visual regression (e.g. parent collapses, ratios off), file it against the responsible task (02 or 03) rather than patching here
- Save a representative `.harness/last-frame.txt` snapshot if a visual reference is useful for future sessions

View File

@@ -1,40 +0,0 @@
# Yazi UI Remake
Objective: Remake the PodTUI shell into a yazi-pure parent|current|preview 3-pane layout (1:3:3 ratio) with a bottom tab strip and no always-on sidebar.
Status legend: [ ] todo, [~] in-progress, [x] done
## Tasks
- [x] 01 — rearchitect-nav-model → `01-rearchitect-nav-model.md`
- [x] 02 — build-three-pane-layout-primitive → `02-build-three-pane-layout-primitive.md`
- [x] 03 — convert-list-tabs-to-primitive → `03-convert-list-tabs-to-primitive.md`
- [x] 04 — fit-search-and-player-panes → `04-fit-search-and-player-panes.md`
- [x] 05 — rebuild-shell-chrome → `05-rebuild-shell-chrome.md`
- [x] 06 — rewire-keybinds → `06-rewire-keybinds.md`
- [x] 07 — verify-remake → `07-verify-remake.md`
## Dependencies
- 03 depends on 01
- 03 depends on 02
- 04 depends on 01
- 04 depends on 02
- 05 depends on 01
- 06 depends on 01
- 06 depends on 05
- 07 depends on 03
- 07 depends on 04
- 07 depends on 05
- 07 depends on 06
## Exit criteria
- The feature is complete when the left tab sidebar is gone; tabs switch only via digit keys `1-6` / `[ ]` and a bottom tab strip
- All tabs render three stable columns at 1/7 : 3/7 : 3/7 (parent | current | preview)
- The parent pane renders the previous-depth list and is blank (but keeps its 1/7 slot) at depth 0
- `h`/`l` drill (push) and pop depths on list tabs; `h` is a noop at depth 0
- `j`/`k` move within the current pane only; focus starts on the current pane
- Feed depth 0→1→2, MyShows, Discover, Settings (sections→items→editor), Search, and Player all render correctly via the drive harness
- `bun run build` passes and `lens_diagnostics` (mode=all) reports zero errors
- The bottom status bar shows active tab + depth + counts, selection count, now-playing, and the tab strip