Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ on:
branches: [main]

jobs:
test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.8.x"

- name: Set up Python
run: uv python install

# The suite drives the app through Textual's headless test pilot, so it
# needs no terminal. Tests that shell out to the `openspec` CLI skip
# themselves when it is not installed.
- name: Run tests
run: uv run --group dev pytest -q

build-check:
name: Build & import check
runs-on: ubuntu-latest
Expand Down
93 changes: 36 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
<sub>Screenshots show an example project, not this repository.</sub>

### 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<version>` 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

Expand All @@ -133,7 +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.

## License

MIT
Binary file added docs/images/change-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/command-palette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dashboard-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/filtering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/kanban.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/spec-browser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/tasks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions openspec_ui/screens/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion openspec_ui/screens/detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion openspec_ui/screens/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
2 changes: 1 addition & 1 deletion openspec_ui/screens/spec_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ version_scheme = "semver"
tag_format = "v$version"
update_changelog_on_bump = true
changelog_incremental = true

[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
]

[tool.pytest.ini_options]
# Tests drive the Textual app through its async test pilot, so every test
# coroutine is a real async test; auto mode saves marking each one.
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["tests"]
Loading
Loading