Better Editor for Streamd Tag Maintenance #327
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adding new tags in the streamd tag maintenance TUI is annoying.
Plan
Scope: the tag maintenance view (
streamd config tags maintenance -i, tabconfig, R33), its dialogs Add to dimension and description. All changes are in the CLI (crates/streamd-cli/src/maintenance/interactive/and a shared input incommands/tui/); the core only provides what already exists (tag_summaries,mention_at).Decisions (clarified)
.streamd.toml, astag_summarieslists them, with their one line) — not the other unplaced tags.Tabinserts;Enteralways adds the tag.Enterpicks;j/k/qbecome text there,Escgoes back.1. A shared text input:
commands/tui/input.rsTextInput { text: String, cursor: usize }(cursor a byte offset, always on a char boundary).key(&mut self, KeyEvent) -> bool(changed?): printable chars insert at the cursor (not with Ctrl/Alt),Backspace/Delete,←/→,Home/End,Ctrl-←/Ctrl-→by word,Ctrl-U/Ctrl-W(clear to start / delete word back).insert,replace(range, with)(moves the cursor to the end of what was put in).wrapped(width) -> (Vec<String>, (row, col)): the text broken at spaces into rows of at mostwidthcolumns (byunicode-width, ascolumnsoftui::widgets), a word longer than a row broken inside it; plus where the cursor is. Pure, unit-tested (empty, exact fit, long word, wide chars, cursor at the end of a full row → next row).commands/activity/interactive/range.rs, which has its own char-boundary editing andwrapped) use it too; otherwise leave it and note it in AGENTS.md as a follow-up. Not a requirement of this issue.type_intoofmaintenance/interactive/model.rsstays for the Merge filter (or usesTextInputas well — either is fine, but one way of typing per view is preferred).2. Description dialog (model)
Dialog::Description { dimension: usize, input: TextInput, suggestion: usize }.Loaded/Modelgettags: Vec<TagSummary>→ name itmentionable(thetag_summaries(&config)of the view's own configuration, filled interminal.rsnext todimensionsandtargets; re-export through the CLI'sconfig/project.rs/documentas needed).Model::suggestions(&self) -> Vec<usize>: ifmention_at(input.text, input.cursor)(core,config/project/index.rs) gives a mention, the typed part after the@up to the cursor is the prefix; candidates are thementionablewhose name starts with it (ignoring case unless the prefix has an uppercase char), then those that only contain it, without duplicates, without the name exactly as typed if that is the only one; none if there is no mention at the cursor.↑/↓movesuggestionwhile suggestions show (else they do nothing);Tabreplaces the mention range with@<Name>and, if the next char is not a space, adds nothing else (the user types on);Enter→Effect::Addwith the trimmed text as now;Esc→ back to the dimension dialog (keeping its filter); everything else goes toTextInput::key, and resetssuggestionto 0 when the text changed.Tabis a key of R30 (switch tab), but the dialog is modal so it never reaches the front-end — check that in a test.3. Description dialog (view)
min(area.width - 4, 72)(at least 30), and the height its wrapped rows need (+ suggestion rows), capped by the area; if the text is taller than that, scroll so the cursor row is visible.description:label, continuation rows indented to the same column; tags in the text coloured as everywhere (tui::panes::styled/style::tag_partson the ranges ofdescription_mentions). The cursor is a reversed cell at its position (astypeddoes at the end now); additionallyframe.set_cursor_positionis not needed.@Namein its colour plus itswhatin dark gray, cut to the width (styled_fit), the selected one with▶and the bar (choice), scrolled withpanes::scrolled.type what the tag stands for ↑/↓ choose tab complete enter add esc back(the↑/↓ … tabpart only while suggestions show).4. Dimension dialog with search (model + view)
Dialog::Dimension { filter: String, selected: usize };Model::dimension_candidates(filter)likemerge_candidates, matching name and key withsearch::matches(smartcase).Enteron a candidate opens the description (with the position indimensions), on none does nothing,Escreturns to the menu. The description dialog keeps the filter soEscthere returns to the narrowed list.typed("/", filter)row on top as in Merge,no dimension has this in its name or keywhen empty, at most 10 rows with scrolling. Key bar:type to narrow ↑/↓ move enter choose esc back.5. Tests
tui/input.rs: editing at a cursor in the middle, unicode (ä, wide CJK, emoji) boundaries, word moves, wrapping and cursor position.model.rs: dimension filter narrows by name and key, smartcase, Enter/Esc paths;j/qare text there; description cursor editing; suggestions appear at@Ta|, not after a space or in an e-mail address;Tabinserts@Taskin the middle of text;Enterwhile suggestions show still adds the tag;Esckeeps the dimension filter;Tabis not forwarded (modal).view.rs(TestBackend): a long description wraps over several rows inside the popup at 80 and at 40 columns, the cursor cell in the middle of the text, the suggestion list with colours, the filtered dimension list and its empty message.terminal.rs: the loaded view has thementionabletags of the configuration; adding a tag with a description that was completed writesdescription = "… @Task …".6. Documentation
REQUIREMENTS.mdR33, Add to dimension: the typed filter of the dimensions (by name or key, smartcase, ↑/↓/Home/End, at most 10 rows,Escto the menu), the description prompt (wrapping, cursor keys, the completion of configured tags with ↑/↓ andTab,Enteralways adds,Escback to the narrowed dimensions).README.md: the maintenance section's keys.AGENTS.md:commands/tui/input.rsin the list ofcommands/tui/(and in the description ofmaintenance/interactive/model.rs: theDialogs now carry a filter and aTextInput).nix flake check.Implemented in #330: #330
Elapsed time: approximately 17 minutes (2026-10-25, 09:51–10:08 UTC), including tests, signed commits, push and PR creation.
Token usage: unavailable. This runtime exposes no per-task token counter; the usage query returned no token report, so I cannot provide a measured count.
The description now soft-wraps, scrolls to its editable Unicode-safe cursor and completes configured tags/aliases at the cursor with ↑/↓ and Tab. Enter always adds the tag. Dimensions can be narrowed by name or key with smartcase, and Esc from the description preserves the narrowed list.
Findings:
mention_at,description_mentions,tag_summaries) supply the necessary tag information; all implementation changes stay in the CLI, without new dependencies.NO_COLOR; commit hooks passed with it unset and a colour-capable TERM.Validation: all 1,748 Rust tests, strict Clippy, formatting and commit hooks passed. The final
nix flake checkpassed, including 102 Kotlin checks. Tests cover cursor editing, Unicode wrapping, smartcase and suggestion order, modal Tab routing, rendering at 80/40 columns and tiny sizes, scrolling, and writing a completed description to.streamd.toml.Three GPG-signed conventional commits were pushed; README.md, REQUIREMENTS.md R33 and AGENTS.md are updated.