Skip to content

feat(cli): migrate tui to schema-driven adapter API (#269)#300

Merged
tikazyq merged 4 commits into
mainfrom
claude/implement-issue-269-fdzmc
May 19, 2026
Merged

feat(cli): migrate tui to schema-driven adapter API (#269)#300
tikazyq merged 4 commits into
mainfrom
claude/implement-issue-269-fdzmc

Conversation

@tikazyq

@tikazyq tikazyq commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #269.

Rewrites the terminal UI to talk through the active Adapter and render every status / priority / filter using the resolved SpecSchema. Replaces hard-coded SpecStatus and SpecPriority references across all 8 TUI files with dynamic, schema-driven equivalents in a single atomic migration, matching the design in the spec.

Key changes:

  • tui/mod.rs — re-wires the previously stubbed module; run() resolves an adapter via --specs-dir / --project / AdapterRegistry::from_project() and drives the event loop. No more "TUI is not yet migrated" error; no markdown-only guard.
  • tui/app.rsApp now holds Box<dyn Adapter>, SpecSchema, status_key, priority_key, and Vec<SpecDoc> instead of SpecLoader + Vec<SpecInfo>. FilterState.fields: HashMap<String, Vec<String>> replaces typed statuses / priorities. SortOption::FieldDesc(String) replaces PriorityDesc. BoardGroup.value: String replaces typed status. New DepsIndex built from doc.links (link_type = depends_on) replaces the file-path-keyed DependencyGraph.
  • tui/theme.rsfield_style(value, key, schema) and field_symbol(value, key, schema) replace status_style / status_symbol / priority_symbol. New parse_hex_color handles EnumOption::color strings with a downgrade for non-truecolor terminals.
  • tui/board.rs, list.rs, detail.rs, filter.rs, search.rs, deps.rs, keybindings.rs — all rendering and dispatch now goes through the schema. Detail metadata block iterates schema.fields and renders each inline field generically. Filter popup is grouped by field key (status, priority, …) rather than hardcoded sections.
  • No SpecStatus / SpecPriority / SpecInfo / SpecFilterOptions imports remain in TUI code.

Board column order now follows schema declaration order (draft → planned → in-progress → complete → archived) instead of the legacy hardcoded order. The spec explicitly notes this and instructs to update snapshots in the same PR.

Test plan

  • cargo test -p leanspec-cli --bin leanspec — 195 passed; 1 unrelated pre-existing flake (test_regression_special_characters_in_name, fails on main too).
  • cargo test -p leanspec-cli --test tui_e2e — all 4 e2e headless tests pass.
  • cargo clippy -p leanspec-cli --tests -- -D warnings — clean.
  • cargo build --workspace — clean.
  • Smoke test: leanspec --specs-dir tests/fixtures/tui-sample tui --headless "ss" returns expected schema-ordered board groups and the priority sort label.
  • Smoke test: filter popup toggles by (key, value) pair and filtered counts update correctly when ESC closes the overlay.

https://claude.ai/code/session_01EUhnsCqrYncNjpzQnNCB46


Generated by Claude Code

Rewrites the terminal UI to talk through the active adapter and render
every status / priority / filter using the resolved `SpecSchema`. Replaces
hard-coded `SpecStatus` and `SpecPriority` references across all 8 TUI
files with dynamic, schema-driven equivalents: `FilterState.fields`
keyed by string, `SortOption::FieldDesc(key)`, `BoardGroup.value`, and
`theme::field_style` / `field_symbol`.

The TUI is no longer gated behind a markdown-only guard and runs
uniformly against GitHub / ADO / Jira projects via
`AdapterRegistry::from_project()`.

https://claude.ai/code/session_01EUhnsCqrYncNjpzQnNCB46
Copilot AI review requested due to automatic review settings May 19, 2026 05:52
CI's `cargo fmt --check` flagged a few struct/enum/closure layouts that
exceeded rustfmt's preferred line lengths after the schema-driven
rewrite. No behavioural change.

https://claude.ai/code/session_01EUhnsCqrYncNjpzQnNCB46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the leanspec tui implementation from hard-coded SpecStatus / SpecPriority types to a schema-driven, adapter-based model, so the TUI can render fields (status/priority/filter/symbols/colors) from the active adapter’s resolved SpecSchema.

Changes:

  • Rewires the TUI entry point to resolve an adapter and run the interactive event loop (plus headless scripting for e2e tests).
  • Refactors core TUI state to store Box<dyn Adapter>, SpecSchema, and Vec<SpecDoc>; updates sorting/filtering/board grouping to use schema field keys and enum option order.
  • Updates all TUI views (board/list/detail/filter/search/deps) and theming to render symbols/styles based on schema enum options (with fallbacks).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
rust/leanspec-cli/src/commands/tui/mod.rs New adapter-resolving entry point, terminal setup/restore, main event loop, headless runner
rust/leanspec-cli/src/commands/tui/app.rs Central refactor: adapter/schema/doc state, schema-driven filter/sort/grouping, dependency index, body extraction
rust/leanspec-cli/src/commands/tui/theme.rs Schema-driven field styling/symbols; hex color parsing with non-truecolor downgrade
rust/leanspec-cli/src/commands/tui/board.rs Board grouping/rendering now uses schema-driven status/priority fields
rust/leanspec-cli/src/commands/tui/list.rs List rendering now uses SpecDoc ids and schema-based status/priority symbols
rust/leanspec-cli/src/commands/tui/detail.rs Detail header becomes schema-driven inline metadata + adapter-loaded body rendering
rust/leanspec-cli/src/commands/tui/filter.rs Filter popup rewritten to enumerate schema enum values and toggle (key, value) pairs
rust/leanspec-cli/src/commands/tui/search.rs Search overlay rendering updated to display schema-based status symbol and SpecDoc id
rust/leanspec-cli/src/commands/tui/deps.rs Dependency view switched to doc.links-derived index and schema-based status styling
rust/leanspec-cli/src/commands/tui/keybindings.rs Mouse/key handling updated for schema-driven filter entries and sort cycling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +112
// 2. --project resolves through the registry to a project + markdown adapter.
if let Some(name) = project_name {
if let Ok(registry) = leanspec_core::storage::ProjectRegistry::new() {
if let Some(project) = registry
.all()
.iter()
.find(|p| p.name == name || p.id == name)
.cloned()
.cloned()
{
let specs_dir = project.specs_dir.to_string_lossy().into_owned();
let adapter: Box<dyn Adapter> = Box::new(MarkdownAdapter::new(&specs_dir));
return Ok((adapter, Some(project)));
}
Comment on lines +102 to 112
impl FilterState {
/// Build a filter from the schema. By default no filter is active
/// (all values pass) and archived items are hidden.
pub fn from_schema(_schema: &SpecSchema) -> Self {
Self {
statuses: Vec::new(),
priorities: Vec::new(),
fields: HashMap::new(),
assignees: Vec::new(),
tags: Vec::new(),
hide_archived: true,
}
}
Comment on lines 88 to 99
/// Active filter state for the spec list.
#[derive(Debug, Clone)]
///
/// `fields` maps field key → list of selected values. Empty list (or absent
/// key) means "no filter applied for this field — show everything." The
/// `assignees` and `tags` lists are kept separately for semantic convenience;
/// they apply on top of `fields`.
#[derive(Debug, Clone, Default)]
pub struct FilterState {
pub statuses: Vec<SpecStatus>,
pub priorities: Vec<SpecPriority>,
pub fields: HashMap<String, Vec<String>>,
pub assignees: Vec<String>,
pub tags: Vec<String>,
pub hide_archived: bool,
Comment on lines +39 to 50
///
/// `FieldDesc(key)` / `FieldAsc(key)` sort by an enum field's declared
/// option order in the schema (e.g. priority). Documents whose value is
/// missing or unknown sort last regardless of direction.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum SortOption {
#[default]
IdDesc,
IdAsc,
PriorityDesc,
FieldDesc(String),
TitleAsc,
UpdatedDesc,
Comment on lines +523 to +541
digits.parse::<u32>().unwrap_or(0)
}

impl App {
/// Construct the app against a resolved adapter. Loads all specs once and
/// derives the initial filter, sort, and board view.
pub fn new(
specs_dir: &str,
adapter: Box<dyn Adapter>,
initial_view: PrimaryView,
initial_project: Option<leanspec_core::storage::Project>,
) -> Result<Self, Box<dyn Error>> {
let loader = SpecLoader::new(specs_dir);
let specs = loader.load_all_metadata()?;
let dep_graph = DependencyGraph::new(&specs);
let stats = SpecStats::compute(&specs);
let mut schema = adapter.schema().clone();
// Best effort — adapters with static schemas (markdown) no-op here.
let _ = adapter.resolve_schema(&mut schema);
let status_key = schema
.key_for_semantic(semantic::STATUS)
.map(|k| k.to_string());
let priority_key = schema
.key_for_semantic(semantic::PRIORITY)
Comment on lines 705 to 732
if let Some(ha) = prefs.hide_archived {
self.filter.hide_archived = ha;
}
}

/// Close any overlay and return to Normal mode.
pub fn close_overlay(&mut self) {
self.mode = AppMode::Normal;
self.project_switcher = None;
// Keep project_mgmt around so state survives a re-open? No — clear it.
self.project_mgmt = None;
}

/// Switch to a project: reload specs from its specs_dir, update last_accessed.
/// Switch to a project: instantiate its adapter via the registry and
/// reload state. Errors here are surfaced as `message` on the project
/// management view so the user can recover without crashing the TUI.
pub fn switch_project(&mut self, project: leanspec_core::storage::Project) {
// Save prefs for the current project before switching
self.save_prefs();

// Update last_accessed in registry.
if let Ok(mut registry) = leanspec_core::storage::ProjectRegistry::new() {
let _ = registry.touch_last_accessed(&project.id);
}

let specs_dir = project.specs_dir.to_string_lossy().into_owned();
self.current_project = Some(project);

// Load prefs for the new project
if let Some(ref p) = self.current_project.clone() {
let prefs = Self::load_prefs(&p.id);
self.apply_prefs(&prefs);
}

self.reload_specs(&specs_dir);
self.reload_with_markdown_dir(&specs_dir);
self.close_overlay();
}
Comment on lines +506 to +513
.field(field_key)
.and_then(|f| match &f.kind {
FieldKind::Enum { options, .. } => options
.iter()
.position(|o| o.value == value)
.map(|i| i as u32),
_ => None,
})
claude added 2 commits May 19, 2026 06:06
- Resolve project adapter from `leanspec.adapter.yaml` in the project
  root rather than forcing markdown, so `--project foo` and the
  in-TUI project switcher honour GitHub/ADO/Jira configurations
  (new `resolve_adapter_for_project` helper).
- Pre-populate `FilterState::from_schema` with every enum value, as
  spec #269 prescribes — popup defaults to all `[x]` and unchecking
  narrows the visible set. `is_empty(&schema)` now reports the
  schema-aware default-state semantic.
- Drop unused `FilterState::assignees` / `tags`; the docstring no
  longer advertises behaviour that wasn't implemented.
- Propagate adapter errors from `App::new` / `reload_with_adapter`
  instead of falling through to an empty spec list, so backend
  auth/network/schema failures surface at startup.
- `id_order` falls back to case-insensitive string compare when the
  doc id has no numeric prefix, so `IdAsc/IdDesc` order correctly
  for adapters with keys like `ABC-123`.
- Fix stale `FieldAsc` doc on `SortOption`.

https://claude.ai/code/session_01EUhnsCqrYncNjpzQnNCB46
Clippy in rust 1.95.0 added `clippy::collapsible-match` to the default
warning set, which deny-with-`-D warnings` turns into a CI failure on
the two `if`-inside-`match` arms in `FilterState::matches`. Rewrite
both as match guards — behaviour is unchanged.

https://claude.ai/code/session_01EUhnsCqrYncNjpzQnNCB46
@tikazyq tikazyq merged commit 657cda7 into main May 19, 2026
2 of 3 checks passed
@tikazyq tikazyq deleted the claude/implement-issue-269-fdzmc branch May 19, 2026 06:27
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.

spec: TUI Generalization

3 participants