Tag Display Prioritization
Reorder and bold-highlight matched tags in the truncated Tags column of toneforge list recipes when --search or --tags filters are active, so developers can immediately see why each recipe matched.
Problem Statement
The toneforge list recipes Tags column truncates to 14 characters with a positional ellipsis. When a filter is active, the tags that matched the query may be truncated away, giving the user no visible indication of why a recipe appeared in the results.
Users
Game developers using ToneForge to generate procedural sound effects.
- As a game developer, I want the tags that matched my
--tags filter to appear first in the Tags column so I can confirm at a glance that the right recipes were returned.
- As a game developer, I want the tags that contain my
--search keyword to appear first in the Tags column so I can see which tags are relevant to my search.
- As a game developer, I want matched tags to be visually distinct (bold) so I can quickly scan a filtered list and identify matching attributes.
Success Criteria
- When a
--tags filter is active and the Tags column is truncated, tags that exactly match (case-insensitive) the filter values appear before non-matched tags.
- When a
--search filter is active and the Tags column is truncated, tags that contain the search keyword as a substring (case-insensitive) appear before non-matched tags.
- When both
--tags and --search are active, tags matching either filter are prioritized (matched tags first, then non-matched).
- When no filter is active, or only a
--category filter is active, tag order is unchanged (registration order preserved). The --category filter does not affect tag prioritization since it matches against category, not tags.
- In TTY mode, matched tags are rendered in ANSI bold; in non-TTY mode and when
NO_COLOR is set, no ANSI codes are emitted.
- The
--json output preserves the original tag order (no reordering); tag prioritization is a display-only concern for the table.
- Table column alignment is not broken by ANSI escape codes in tag cells (ANSI-aware width measurement is implemented).
Constraints
- Tag column width: Fixed at 14 characters. ANSI bold escape codes are zero-width and must not count toward the column width budget.
- Backward compatibility: Unfiltered output must preserve the current tag order (registration order). No visible change when no filter is active.
- TTY gating: Bold styling must respect the existing
isStdoutTty() / NO_COLOR gating in src/output.ts.
- JSON stability: JSON output schema and tag order must not change.
- No new dependencies: Implement ANSI-aware width measurement without adding external packages (a simple regex strip of
\x1b\[[0-9;]*m sequences is sufficient).
- Consistency: Tag match logic for prioritization should mirror the existing filter logic --
--tags uses exact case-insensitive match, --search uses substring across tag values (consistent with listDetailed() in src/core/recipe.ts).
Existing State
truncateTags() in src/cli.ts:1016-1033 joins tags in array order and truncates at 14 chars with ellipsis. No reordering, no styling.
listDetailed() in src/core/recipe.ts:362-425 returns RecipeDetailedSummary ({ name, description, category, tags }) with no match metadata. Filtering is pass/fail with no indication of which tags contributed.
RecipeFilterQuery (src/core/recipe.ts:160-181) carries search, category, and tags fields.
- Table rendering (
pad(), wordWrap() in src/output.ts:202-261) uses String.length, which does not account for ANSI escape codes.
- ANSI infrastructure (
COLORS, style(), isStdoutTty()) exists in src/output.ts but is not used for table cell content.
- Existing tests cover tag truncation (
src/cli.test.ts:537-545) and filter behavior (src/cli.test.ts:290-409, src/core/recipe.test.ts:133-620+).
Desired Change
Registry API (src/core/recipe.ts)
- Extend
RecipeDetailedSummary (or create a new return type) to include a matchedTags: string[] field indicating which tags contributed to the filter match.
- Modify
listDetailed() to populate matchedTags when a --tags or --search filter is active:
- For
--tags: tags that exactly match (case-insensitive) any filter value.
- For
--search: tags that contain the search keyword as a substring (case-insensitive).
- When no filter is active,
matchedTags is an empty array.
Table rendering (src/output.ts)
- Add an ANSI-stripping utility function (e.g.,
stripAnsi(s: string): string) using a regex like /\x1b\[[0-9;]*m/g.
- Add an ANSI-aware string width function (e.g.,
ansiWidth(s: string): number) that strips ANSI codes before measuring length.
- Update
pad() and wordWrap() to use ansiWidth() instead of String.length for width calculations.
Tag rendering (src/cli.ts)
- Modify
truncateTags() (or create a new function) to accept a matchedTags parameter and reorder matched tags to the front before truncation.
- When in TTY mode, wrap matched tags in ANSI bold (
\x1b[1m...\x1b[0m) before joining.
- Apply truncation after reordering and styling (using ANSI-aware width for the truncation point).
- The CLI
list recipes handler passes matchedTags from the RecipeDetailedSummary to the tag rendering function.
Tests
- Unit tests for matched-tag prioritization with truncation (both
--tags and --search triggers).
- Unit test confirming no reordering when no filter is active.
- Unit test for ANSI bold applied to matched tags in TTY mode.
- Unit test for no ANSI codes in non-TTY /
NO_COLOR mode.
- Unit tests for
stripAnsi() and ansiWidth() utilities.
- Unit test confirming
pad() and wordWrap() handle ANSI-styled strings correctly.
- Unit test confirming JSON output preserves original tag order.
Risks & Assumptions
Assumptions
- All recipes have
tags populated as string[]. If a recipe has no tags, matchedTags will be empty and no reordering occurs -- this is a no-op, not an error.
- The
COLORS.bold ANSI escape sequence (\x1b[1m) and reset (\x1b[0m) are the only codes needed; no 256-color or truecolor sequences are required.
- The
pad() and wordWrap() functions in src/output.ts are only used for table rendering. Updating them to use ANSI-aware width has no unintended side effects elsewhere.
Risks
- ANSI-aware width regression: Changing
pad() and wordWrap() to use ANSI-aware measurement could introduce subtle layout issues if other code paths pass ANSI-styled strings that were previously measured by String.length. Mitigation: Review all call sites of pad() and wordWrap() during implementation; add regression tests for existing table output.
- Truncation mid-escape-code: If truncation slices through an ANSI escape sequence, the terminal may display corrupted output. Mitigation: Truncation logic must operate on visible character positions (after stripping ANSI), then re-apply styling to the truncated result.
- Scope creep: This feature could expand to include color-coded tags by match type, clickable tag links, or tag grouping/sorting beyond match priority. Mitigation: Record enhancement ideas as separate work items linked to TF-0MM7K306X1SLYFOL rather than expanding scope.
Related Work
- Add filtering to list recipes command (TF-0MM7ENNL90AHWC0N, in-progress) -- parent item; this is the only remaining open child.
- CLI Filter Flags & Four-Column Table (TF-0MM7K1YHL12T4EJH, completed) -- implemented current tag truncation and four-column table.
- Registry API: listDetailed with Filter (TF-0MM7K1K371LIKKZZ, completed) -- implemented
listDetailed() and RecipeFilterQuery.
- JSON Output & Help Text Updates (TF-0MM7K2AW61OAO8VV, completed) -- JSON schema for list recipes.
- TUI Wizard: Interactive Sound Palette Builder (TF-0MM7HULM506CGSOP, open) -- may benefit from match metadata in the registry API.
src/library/search.ts -- existing AND-logic filter pattern for reference.
Related work (automated report)
Generated by find_related skill on 2026-03-01.
Work items
- Add filtering to list recipes command (TF-0MM7ENNL90AHWC0N, in-progress) -- Direct parent. This feature is the last remaining open child; it deferred matched-tag prioritization to this work item. The filter-flag parsing,
listDetailed() wiring, and four-column table introduced by its children are the foundation this task extends.
- CLI Filter Flags & Four-Column Table (TF-0MM7K1YHL12T4EJH, completed) -- Implemented
truncateTags() in src/cli.ts:1021-1033 and the four-column table layout. This work item modifies truncateTags() to accept matched-tag metadata, reorder tags, and apply ANSI bold styling.
- Registry API: listDetailed with Filter (TF-0MM7K1K371LIKKZZ, completed) -- Introduced
RecipeDetailedSummary and RecipeFilterQuery in src/core/recipe.ts. This work item extends RecipeDetailedSummary with a matchedTags field and modifies listDetailed() to populate it.
- JSON Output & Help Text Updates (TF-0MM7K2AW61OAO8VV, completed) -- Defined the JSON output schema (
{ command, resource, recipes, total, filters? }). Tag prioritization must not alter JSON output, so the schema stability established here is a constraint.
- Integration Tests & Validation (TF-0MM7K2OG71UMBL1P, completed) -- Added 28 CLI integration tests covering filter behavior, tag truncation with ellipsis, and JSON output structure. New tests for matched-tag prioritization should follow the patterns established here. Has a
depends-on dependency relationship with this item.
- TUI Wizard: Interactive Sound Palette Builder (TF-0MM7HULM506CGSOP, open) -- The wizard's Stage 1 (recipe browsing by category) would benefit from the
matchedTags metadata added to RecipeDetailedSummary, enabling richer display of filter-relevant tags in the interactive recipe picker.
- list recipes should provide a summary of the sound (TF-0MLYXJASS12OSBJK, completed) -- Introduced
listSummaries(), the two-column table format, wordWrap(), and pad() utilities in src/output.ts. The pad() and wordWrap() functions must be updated to use ANSI-aware width measurement as part of this work item.
Repository files
src/cli.ts:1016-1033 (truncateTags()) -- The primary function to modify. Currently joins tags in array order and truncates at a fixed width with ellipsis. Must be extended to accept matchedTags, reorder matched tags first, apply ANSI bold in TTY mode, and use ANSI-aware width for truncation.
src/cli.ts:1247-1320 (list recipes handler) -- Parses --search, --category, --tags flags and calls listDetailed(). The handler must pass matchedTags from the result to the updated truncateTags() function.
src/core/recipe.ts:160-191 (RecipeFilterQuery and RecipeDetailedSummary interfaces) -- RecipeDetailedSummary must be extended with matchedTags: string[]. RecipeFilterQuery defines the filter shape that determines which tags match.
src/core/recipe.ts:362-425 (listDetailed() method) -- Must be modified to compute and populate matchedTags based on the active filter, using exact match for --tags and substring match for --search.
src/output.ts:46-54 (isStdoutTty(), NO_COLOR check) -- TTY/NO_COLOR gating logic that must be respected when applying ANSI bold to matched tags.
src/output.ts:202-261 (pad() and wordWrap()) -- Currently use String.length for width measurement. Must be updated to use a new ansiWidth() function that strips ANSI escape codes before measuring, to prevent ANSI bold codes from breaking column alignment.
src/output.ts:19-27 (COLORS object) -- Provides COLORS.bold (\x1b[1m) and COLORS.reset (\x1b[0m) sequences to be used for matched-tag styling.
src/cli.test.ts:537-545 (tag truncation tests) -- Existing test for tag truncation with ellipsis. New tests for matched-tag prioritization, ANSI bold in TTY mode, and no-ANSI in non-TTY mode should be added alongside these.
src/core/recipe.test.ts:133-620 (listDetailed test suite) -- Comprehensive tests for filter behavior. Tests for matchedTags population should be added here.
src/library/search.ts -- AND-logic filter pattern with SearchQuery interface; serves as a reference implementation for the tag-matching logic to be added to listDetailed().
Tag Display Prioritization
Reorder and bold-highlight matched tags in the truncated Tags column of
toneforge list recipeswhen--searchor--tagsfilters are active, so developers can immediately see why each recipe matched.Problem Statement
The
toneforge list recipesTags column truncates to 14 characters with a positional ellipsis. When a filter is active, the tags that matched the query may be truncated away, giving the user no visible indication of why a recipe appeared in the results.Users
Game developers using ToneForge to generate procedural sound effects.
--tagsfilter to appear first in the Tags column so I can confirm at a glance that the right recipes were returned.--searchkeyword to appear first in the Tags column so I can see which tags are relevant to my search.Success Criteria
--tagsfilter is active and the Tags column is truncated, tags that exactly match (case-insensitive) the filter values appear before non-matched tags.--searchfilter is active and the Tags column is truncated, tags that contain the search keyword as a substring (case-insensitive) appear before non-matched tags.--tagsand--searchare active, tags matching either filter are prioritized (matched tags first, then non-matched).--categoryfilter is active, tag order is unchanged (registration order preserved). The--categoryfilter does not affect tag prioritization since it matches against category, not tags.NO_COLORis set, no ANSI codes are emitted.--jsonoutput preserves the original tag order (no reordering); tag prioritization is a display-only concern for the table.Constraints
isStdoutTty()/NO_COLORgating insrc/output.ts.\x1b\[[0-9;]*msequences is sufficient).--tagsuses exact case-insensitive match,--searchuses substring across tag values (consistent withlistDetailed()insrc/core/recipe.ts).Existing State
truncateTags()insrc/cli.ts:1016-1033joins tags in array order and truncates at 14 chars with ellipsis. No reordering, no styling.listDetailed()insrc/core/recipe.ts:362-425returnsRecipeDetailedSummary({ name, description, category, tags }) with no match metadata. Filtering is pass/fail with no indication of which tags contributed.RecipeFilterQuery(src/core/recipe.ts:160-181) carriessearch,category, andtagsfields.pad(),wordWrap()insrc/output.ts:202-261) usesString.length, which does not account for ANSI escape codes.COLORS,style(),isStdoutTty()) exists insrc/output.tsbut is not used for table cell content.src/cli.test.ts:537-545) and filter behavior (src/cli.test.ts:290-409,src/core/recipe.test.ts:133-620+).Desired Change
Registry API (
src/core/recipe.ts)RecipeDetailedSummary(or create a new return type) to include amatchedTags: string[]field indicating which tags contributed to the filter match.listDetailed()to populatematchedTagswhen a--tagsor--searchfilter is active:--tags: tags that exactly match (case-insensitive) any filter value.--search: tags that contain the search keyword as a substring (case-insensitive).matchedTagsis an empty array.Table rendering (
src/output.ts)stripAnsi(s: string): string) using a regex like/\x1b\[[0-9;]*m/g.ansiWidth(s: string): number) that strips ANSI codes before measuring length.pad()andwordWrap()to useansiWidth()instead ofString.lengthfor width calculations.Tag rendering (
src/cli.ts)truncateTags()(or create a new function) to accept amatchedTagsparameter and reorder matched tags to the front before truncation.\x1b[1m...\x1b[0m) before joining.list recipeshandler passesmatchedTagsfrom theRecipeDetailedSummaryto the tag rendering function.Tests
--tagsand--searchtriggers).NO_COLORmode.stripAnsi()andansiWidth()utilities.pad()andwordWrap()handle ANSI-styled strings correctly.Risks & Assumptions
Assumptions
tagspopulated asstring[]. If a recipe has no tags,matchedTagswill be empty and no reordering occurs -- this is a no-op, not an error.COLORS.boldANSI escape sequence (\x1b[1m) and reset (\x1b[0m) are the only codes needed; no 256-color or truecolor sequences are required.pad()andwordWrap()functions insrc/output.tsare only used for table rendering. Updating them to use ANSI-aware width has no unintended side effects elsewhere.Risks
pad()andwordWrap()to use ANSI-aware measurement could introduce subtle layout issues if other code paths pass ANSI-styled strings that were previously measured byString.length. Mitigation: Review all call sites ofpad()andwordWrap()during implementation; add regression tests for existing table output.Related Work
listDetailed()andRecipeFilterQuery.src/library/search.ts-- existing AND-logic filter pattern for reference.Related work (automated report)
Generated by find_related skill on 2026-03-01.
Work items
listDetailed()wiring, and four-column table introduced by its children are the foundation this task extends.truncateTags()insrc/cli.ts:1021-1033and the four-column table layout. This work item modifiestruncateTags()to accept matched-tag metadata, reorder tags, and apply ANSI bold styling.RecipeDetailedSummaryandRecipeFilterQueryinsrc/core/recipe.ts. This work item extendsRecipeDetailedSummarywith amatchedTagsfield and modifieslistDetailed()to populate it.{ command, resource, recipes, total, filters? }). Tag prioritization must not alter JSON output, so the schema stability established here is a constraint.depends-ondependency relationship with this item.matchedTagsmetadata added toRecipeDetailedSummary, enabling richer display of filter-relevant tags in the interactive recipe picker.listSummaries(), the two-column table format,wordWrap(), andpad()utilities insrc/output.ts. Thepad()andwordWrap()functions must be updated to use ANSI-aware width measurement as part of this work item.Repository files
src/cli.ts:1016-1033(truncateTags()) -- The primary function to modify. Currently joins tags in array order and truncates at a fixed width with ellipsis. Must be extended to acceptmatchedTags, reorder matched tags first, apply ANSI bold in TTY mode, and use ANSI-aware width for truncation.src/cli.ts:1247-1320(list recipes handler) -- Parses--search,--category,--tagsflags and callslistDetailed(). The handler must passmatchedTagsfrom the result to the updatedtruncateTags()function.src/core/recipe.ts:160-191(RecipeFilterQueryandRecipeDetailedSummaryinterfaces) --RecipeDetailedSummarymust be extended withmatchedTags: string[].RecipeFilterQuerydefines the filter shape that determines which tags match.src/core/recipe.ts:362-425(listDetailed()method) -- Must be modified to compute and populatematchedTagsbased on the active filter, using exact match for--tagsand substring match for--search.src/output.ts:46-54(isStdoutTty(),NO_COLORcheck) -- TTY/NO_COLOR gating logic that must be respected when applying ANSI bold to matched tags.src/output.ts:202-261(pad()andwordWrap()) -- Currently useString.lengthfor width measurement. Must be updated to use a newansiWidth()function that strips ANSI escape codes before measuring, to prevent ANSI bold codes from breaking column alignment.src/output.ts:19-27(COLORSobject) -- ProvidesCOLORS.bold(\x1b[1m) andCOLORS.reset(\x1b[0m) sequences to be used for matched-tag styling.src/cli.test.ts:537-545(tag truncation tests) -- Existing test for tag truncation with ellipsis. New tests for matched-tag prioritization, ANSI bold in TTY mode, and no-ANSI in non-TTY mode should be added alongside these.src/core/recipe.test.ts:133-620(listDetailedtest suite) -- Comprehensive tests for filter behavior. Tests formatchedTagspopulation should be added here.src/library/search.ts-- AND-logic filter pattern withSearchQueryinterface; serves as a reference implementation for the tag-matching logic to be added tolistDetailed().