diff --git a/README.md b/README.md index ea59078..465b1da 100644 --- a/README.md +++ b/README.md @@ -48,75 +48,58 @@ newer (installed automatically). - **Live refresh** — open views re-read from disk as things change on disk; press `p` to pause. -### Keys +Press `?` at any time for the key bindings available on the current screen. -Press `?` at any time for the bindings available on the current screen. +## Screenshots -| Key | Does | -| --- | --- | -| `?` | Show key bindings for the current screen | -| `Ctrl+P` | Command palette (actions, jump to change/spec, theme) | -| `/` | Filter the current listing by name (`escape` clears) | -| `enter` | Open the selected change or spec | -| `k` / `d` | Kanban / dashboard | -| `n` | New change | -| `e` | Edit the current artifact in `$EDITOR` | -| `r` | Refresh now | -| `p` | Pause or resume live refresh | -| `t` | Toggle the table of contents (change detail) | -| `q` | Quit | +Screenshots show an example project, not this repository. -### Theming +**Dashboard** — changes in an aligned table with stage, progress and task +counts, over headline counts per stage. -`openspec-ui` ships no palette of its own — all colour comes from the active -Textual theme, so it follows your terminal. Switch themes in-session from the -command palette (`Ctrl+P` → *Theme*). Nothing is written to disk: the app -persists no state, theme included. +![The dashboard: changes in an aligned table with stage, progress and task +counts, over headline counts per stage](docs/images/dashboard.png) -## Design +**Kanban** — `k`. Every column heading and card border carries its stage's +colour, so the shape of the pipeline reads before any of the text does. -`openspec-ui` is a **thin client**: it owns no state. Every read comes from -`openspec ... --json` or direct file reads under `openspec/`; every write is -delegated to the `openspec` CLI or to `$EDITOR`. This keeps the view layer -replaceable (a future SvelteKit web UI could reuse the same data contract). +![Kanban: four stage columns with colour-coded headings and cards carrying a +stage-coloured left border](docs/images/kanban.png) -## Status +**Change detail** — `enter` on a change. Artifacts render as scrollable +markdown with a table of contents; code blocks and ASCII diagrams keep their +monospace alignment. -Alpha. Built with [Textual](https://textual.textualize.io/). Supports OpenSpec -CLI `1.7.x` (`@fission-ai/openspec`). +![A change's design document with a navigable table of contents on the left and +an ASCII diagram preserved in the body](docs/images/change-detail.png) -## Releases +**Tasks** — the `tasks.md` checklist, one section per heading with its +done/total count. Finished sections start collapsed so the eye lands on what is +left. Read-only: edits go through `$EDITOR`. -Releases are **fully automated**. Maintainers never edit the version field by -hand. +![The tasks pane: sections with completion counts, completed sections collapsed +and remaining work expanded](docs/images/tasks.png) -1. Conventional commits land on `main` (via PRs). -2. On each push to `main`, CI runs [`commitizen`](https://commitizen-tools.github.io/commitizen/) - (`cz bump --yes --changelog`), which determines the next semver from the - commit log, updates `version` in `pyproject.toml`, updates `CHANGELOG.md`, - and creates/pushes a `v` tag. -3. The tag push triggers a second CI job that builds the wheel+sdist and - publishes them to [PyPI](https://pypi.org/project/openspec-ui/) using - **trusted publishing (OIDC)** — no stored API tokens. -4. `feat:`/`fix:`/breaking commits release; pure `chore:`/`docs:` commits do - not (commitizen determines there is no increment). +**Spec browser** — `enter` on a spec. Requirements expand to reveal their +scenarios, which is the only way a spec with thirty of them stays readable. -### Trusted publisher setup (one-time, manual) +![A spec's requirements listed with scenario counts, one expanded to show its +WHEN/THEN scenarios](docs/images/spec-browser.png) -Before the publish job works, register a trusted publisher on -[pypi.org](https://pypi.org) once: +**Command palette** — `Ctrl+P`. Fuzzy-matched actions, plus every change and +spec by name. -- **PyPI project:** `openspec-ui` (create it if it does not yet exist). -- **GitHub repo:** `mrn-dk/openspec-ui`. -- **Workflow filename:** `.github/workflows/release.yml`. -- **Environment name:** `pypi` (the publish job uses `environment: pypi`). +![The command palette filtered to billing-related commands](docs/images/command-palette.png) -This is a web step on pypi.org that cannot be automated from within the repo. -Until it is done, the publish job will fail with an OIDC error; the build -artifacts still exist in the workflow run and can be uploaded manually. +**Filter** — `/`. Narrows the changes and specs by name; `escape` clears it. -The first release also needs a seeded `v0.1.0` tag (commitizen bumps -*relative* to an existing tag); after that, `cz bump` always has a reference. +![The dashboard with a filter narrowing the list to changes whose names contain +"add"](docs/images/filtering.png) + +**Light theme** — all colour comes from the active Textual theme, so it follows +your terminal. + +![The same dashboard under a light theme](docs/images/dashboard-light.png) ## Contributing @@ -133,21 +116,3 @@ Git history uses **Conventional Commits** and **conventional branch names**: For changes managed through OpenSpec, see `openspec/config.yaml` for the project context and per-operation guidance that the openspec skills follow. - -## Development - -```bash -uv sync --group dev -uv run pytest -``` - -The suite drives the real app through [Textual's headless test -pilot](https://textual.textualize.io/guide/testing/), so it needs no terminal. -It covers each screen's rendering and bindings, the thread-worker read path, -theme readability under light, dark and ANSI themes, and that every screen -still fits an 80x24 terminal. Tests that shell out to the `openspec` CLI skip -themselves when it is not on PATH. - -## License - -MIT diff --git a/docs/images/change-detail.png b/docs/images/change-detail.png new file mode 100644 index 0000000..f73b1df Binary files /dev/null and b/docs/images/change-detail.png differ diff --git a/docs/images/command-palette.png b/docs/images/command-palette.png new file mode 100644 index 0000000..6bc78f1 Binary files /dev/null and b/docs/images/command-palette.png differ diff --git a/docs/images/dashboard-light.png b/docs/images/dashboard-light.png new file mode 100644 index 0000000..703a311 Binary files /dev/null and b/docs/images/dashboard-light.png differ diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png new file mode 100644 index 0000000..7ae8a1b Binary files /dev/null and b/docs/images/dashboard.png differ diff --git a/docs/images/filtering.png b/docs/images/filtering.png new file mode 100644 index 0000000..167205d Binary files /dev/null and b/docs/images/filtering.png differ diff --git a/docs/images/kanban.png b/docs/images/kanban.png new file mode 100644 index 0000000..d7ab2a2 Binary files /dev/null and b/docs/images/kanban.png differ diff --git a/docs/images/spec-browser.png b/docs/images/spec-browser.png new file mode 100644 index 0000000..fc0dbed Binary files /dev/null and b/docs/images/spec-browser.png differ diff --git a/docs/images/tasks.png b/docs/images/tasks.png new file mode 100644 index 0000000..29bcf83 Binary files /dev/null and b/docs/images/tasks.png differ diff --git a/openspec_ui/screens/dashboard.py b/openspec_ui/screens/dashboard.py index 707d98b..41b9add 100644 --- a/openspec_ui/screens/dashboard.py +++ b/openspec_ui/screens/dashboard.py @@ -88,7 +88,7 @@ def __init__(self) -> None: # -- compose -------------------------------------------------------- def compose(self) -> None: - yield Header(show_clock=False) + yield Header(show_clock=False, icon="") yield Static("", id="title") yield FilterBar(id="filter") yield Horizontal( @@ -329,12 +329,16 @@ def _render_specs(self, specs) -> None: self._show_specs_empty("No specs yet.") return self._show_specs_empty(None) + # Pad names to the longest so the counts line up in a column, matching + # the changes table above rather than sitting ragged beneath it. + name_width = max(len(spec.name) for spec in specs) for spec in specs: noun = "requirement" if spec.requirement_count == 1 else "requirements" specs_list.append( ListItem( Label( - f"{spec.name} [$text-muted]{spec.requirement_count} {noun}[/]" + f"{spec.name:<{name_width}} " + f"[$text-muted]{spec.requirement_count:>3} {noun}[/]" ), name=spec.name, ) diff --git a/openspec_ui/screens/detail.py b/openspec_ui/screens/detail.py index 0b1792d..ec28396 100644 --- a/openspec_ui/screens/detail.py +++ b/openspec_ui/screens/detail.py @@ -195,7 +195,7 @@ def action_toggle_toc(self) -> None: self._apply_toc() def compose(self) -> None: - yield Header(show_clock=False) + yield Header(show_clock=False, icon="") yield Static("", id="title") yield TabbedContent(id="tabs") yield StatusBar() diff --git a/openspec_ui/screens/kanban.py b/openspec_ui/screens/kanban.py index 8c489e0..0af7006 100644 --- a/openspec_ui/screens/kanban.py +++ b/openspec_ui/screens/kanban.py @@ -118,7 +118,7 @@ def __init__(self) -> None: self._filter = "" def compose(self) -> None: - yield Header(show_clock=False) + yield Header(show_clock=False, icon="") yield Static("Kanban — change pipeline", id="title") yield FilterBar(id="filter") with Horizontal(id="board"): diff --git a/openspec_ui/screens/spec_detail.py b/openspec_ui/screens/spec_detail.py index c2a73aa..695e4db 100644 --- a/openspec_ui/screens/spec_detail.py +++ b/openspec_ui/screens/spec_detail.py @@ -29,7 +29,7 @@ def __init__(self, spec_name: str) -> None: self.spec_name = spec_name def compose(self): - yield Header(show_clock=False) + yield Header(show_clock=False, icon="") yield Static("", id="title") yield VerticalScroll(id="spec-body") yield StatusBar()