Skip to content

feat: merge tui-experience — test suite, screenshots, README focus - #14

Merged
mrn-dk merged 2 commits into
mainfrom
feat/tui-experience
Aug 3, 2026
Merged

mrn-dk merged 2 commits into
mainfrom
feat/tui-experience

Conversation

@mrn-dk

@mrn-dk mrn-dk commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Merges the accumulated work from the feat/tui-experience feature branch into main:

Both PRs were merged into feat/tui-experience and are now rebased onto current main as a clean two-commit stack.

mrn-dk and others added 2 commits August 3, 2026 12:16
* chore: remove add-dark-mode change and stale add-live-kanban tasks.md

* feat(tui): revamp the TUI experience (#11)

* feat(tui): move styling into app.tcss with semantic stage tokens

All four inline CSS strings (app, palette, new-change, confirm) collapse
into a single openspec_ui/app.tcss loaded via CSS_PATH, expressed only in
Textual's theme tokens so the interface follows the active theme instead of
shipping its own palette.

Adds openspec_ui/theme.py as the single source of truth for how a pipeline
stage is drawn (colour token, glyph, CSS class), and the matching
.stage-* rules for column headings, card accents and dashboard badges.

Replaces the hard-coded [green] live indicator with the $text-success
token — under an ANSI theme the literal rendered as truecolor #008000
rather than resolving through the terminal palette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): add status bar and replace title messages with toasts

Transient feedback (cancelled edit, editor fallback, reload complete,
failed reads) now goes to Textual toasts instead of overwriting a screen's
#title and ringing the terminal bell, so messages no longer compete with
the state they were painting over.

State moves to a persistent StatusBar docked above the Footer, carrying the
resolved root and the live/paused indicator. App.live_refresh becomes a
reactive so every mounted bar re-renders on toggle without each screen
pushing the change itself.

Failed reads are now reported twice on purpose: a toast so it is noticed,
and a persistent inline state so it stays readable. Kanban columns gain a
centred placeholder for the empty and error cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* perf(tui): run openspec reads off the UI thread and fix idle polling cost

Dashboard, kanban and detail reads move into thread workers sharing one
exclusive "openspec-read" group, so a slow CLI call no longer freezes input
and a superseded read is cancelled rather than landing over a newer render.
The target widget shows a loading indicator while a read is in flight,
cleared on both success and failure.

Also fixes OpenSpecClient.root(): _root_cache was assigned but never read,
so every live-refresh tick spawned `openspec list --json` — once a second,
forever. The mtime gate existed precisely to avoid that subprocess, and the
openspec-tui spec requires no CLI spawn on an unchanged tick. Idle ticks now
spawn nothing.

Kanban initial focus moves to after the first load lands, so it can land on
the first column that actually has cards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): add spec detail browser

Selecting a spec now opens a screen showing its overview and one
Collapsible per requirement, revealing that requirement's scenarios on
demand — a spec with nine requirements and thirty scenarios is unreadable
flat. Data comes from `openspec show --type spec --json`, so the app stays
on the CLI's contract rather than parsing spec.md itself.

Requirement content is passed to the Collapsible constructor rather than
mounted onto it afterwards: mounting attaches to the Collapsible itself
instead of its Contents, which renders the child unconditionally and leaves
expand/collapse doing nothing.

Scenario counts sit before the requirement text, since the long SHALL
statement is truncated at the pane width and would take a trailing count
with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): replace bespoke palette with Textual's fuzzy command palette

Deletes screens/palette.py (a hand-rolled ListView modal with no matching)
in favour of two Providers registered in App.COMMANDS: the global actions,
and a dynamic one that enumerates changes and specs so either can be opened
by name. Textual's palette brings fuzzy ranking and the system commands —
including the theme switcher, which is how themes change in-session without
the app owning any theme state.

The dynamic provider reads once per palette session in startup() via
asyncio.to_thread rather than per keystroke on the event loop, since the
client shells out to the openspec CLI.

No Quit entry of our own: Textual's system commands already provide one,
and two identically-named commands read as a bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): rebuild the dashboard around an aligned table and stat row

The changes list becomes a DataTable keyed by change name (stage badge,
name, progress, tasks, modified). A ListView of pre-formatted label strings
could not line up columns when names differ in length, which was the main
reason the dashboard read as unstructured. Selection now travels by row key
rather than list index. The dim one-line summary becomes a row of Digits.

Both are behind size guards: the stat row hides under 24 rows, and under 90
columns the table drops Modified and narrows the name column so everything
fits inside 80 cells without scrolling.

Stage tokens move to concrete theme values ($foreground-muted,
$foreground-disabled). Textual's $text-* tokens are `auto` values that
resolve against the background they are drawn on; inside
Content.from_markup — how DataTable cells are coloured — there is no
backdrop to resolve against and `auto` collapses to black, which rendered
the progress-bar track invisible on every dark theme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): give kanban cards structure and stage colour

Cards become a ChangeCard widget (name, progress bar, task count) whose
pieces carry CSS classes, instead of one Label with colour markup baked into
a string. Each card takes a stage-coloured left border and each column
heading its stage glyph and colour, both from STAGE_STYLE, so a column is
identifiable without reading its label.

Column heading backgrounds stay neutral: tinting them per stage looks right
on truecolor themes but collapses on ANSI ones, where $accent-muted and
$text-accent resolve to the same palette entry and the heading text
disappears entirely.

progress_bar moves to theme.py so the dashboard and kanban share one
implementation. Card names truncate with an ellipsis rather than wrapping,
which was misaligning every card below them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): render artifacts with MarkdownViewer and per-file tabs

Each artifact now gets a MarkdownViewer: independent scrolling,
syntax-highlighted fences and a navigable table of contents. An artifact
group resolving to several files becomes nested tabs labelled by filename,
which removes the plain-text concatenation the old pane documented as a
limitation of composing several Markdown documents in one Static.

The TOC hides below 100 columns and toggles with `t`, so it never eats the
document it indexes on a narrow terminal.

Nested tabs are built in compose(): TabbedContent's positional arguments are
tab titles rather than panes, and add_pane needs children that exist only
after mounting completes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): render a change's tasks as a parsed checklist

The Tasks pane becomes one Collapsible per tasks.md section, titled with its
done/total count, over an overall progress line. Finished sections start
collapsed so the eye lands on the work that is left.

The parser ignores checkboxes inside fenced code blocks (task files often
show example markup) and collects items appearing before any heading, so a
flat list is not dropped. A file with no checkbox items at all falls back to
markdown rather than showing an empty checklist that implies no tasks
exist; a change with no tasks.md gets an empty state.

The pane stays read-only: task edits go through the openspec CLI or $EDITOR,
per the thin-client invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(tui): add name filtering and a key-binding help panel

`/` reveals a filter bar on the dashboard and kanban that narrows the
listing by case-insensitive substring on name; escape clears it. Filtering
works on data already loaded rather than re-querying, so it costs nothing,
keeps stage grouping and progress intact, and is re-applied after each
live-refresh reload instead of fighting it. Nothing is persisted.

`?` opens Textual's help panel with the focused screen's bindings. It is
bound as a printable key, so a focused Input swallows it first — typing "?"
into the filter or the new-change name types a "?" rather than opening help.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(tui): guard the stylesheet in CI, document the new keys, fit 80x24

app.tcss is package data rather than a module, so nothing in CI would have
caught it dropping out of the wheel — the app would install cleanly and then
fail at startup with no CSS. CI now resolves it through importlib.resources
from the installed wheel.

Kanban column headings split into a name and a right-pinned count: a
trailing count was truncated away on narrow columns, and a heading wrapping
to two lines pushed that column's cards out of alignment with its
neighbours. Card progress bars shrink to 10 cells and no longer wrap.

README documents the key table, the new screens, theming, and the Textual
1.0 floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Morten R. N. <morten@rothnielsen.dk>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* bump: version 0.3.0 → 0.4.0

* test: add a headless test suite for the TUI

103 tests driving the real app through Textual's test pilot, so they need no
terminal. Data comes from a fake client so assertions do not depend on
whatever is in this repo's own openspec/ root; the few tests that exercise
the real CLI skip themselves when it is absent.

Covers each screen's rendering and bindings, the thread-worker read path
(off-thread reads, loading flags, superseded reads not landing), the
tasks.md parser, filtering, the command palette, and the help panel.

Two checks encode invariants that were violated during development and are
easy to regress:

- Theme readability. Every screen is audited under light, dark and ANSI
  themes for text whose foreground is unreadable against its background, and
  for truecolor literals reaching the screen while an ANSI theme is active —
  the signature of a hard-coded colour bypassing the theme.
- 80x24. Every screen must render without overflowing the width or losing
  content off the bottom.

Adds a dev dependency group and a CI job to run them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop unused imports

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Morten R. N. <morten@rothnielsen.dk>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Eight screenshots covering the dashboard, kanban, change detail, tasks,
spec browser, command palette and filtering, plus a light-theme dashboard
to show the app follows the terminal's theme.

Also focus the README to just the essentials: openspec-ui, install & run,
what it does, screenshots, contributing. Remove Keys, Theming, Design,
Status, Releases, and License sections.

Two things the screenshots caught that no text-based assertion could:

- The Header icon rendered as a missing-glyph box. Removed; some terminals
  lack that glyph too.
- Spec requirement counts sat ragged directly beneath the aligned changes
  table. Names are now padded so the counts line up.

Co-authored-by: Morten R. N. <morten@rothnielsen.dk>
@mrn-dk
mrn-dk merged commit efe9898 into main Aug 3, 2026
2 checks passed
@mrn-dk
mrn-dk deleted the feat/tui-experience branch August 3, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant