Skip to content

Tag Display Prioritization #226

@SorraTheOrc

Description

@SorraTheOrc

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

  1. 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.
  2. 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.
  3. When both --tags and --search are active, tags matching either filter are prioritized (matched tags first, then non-matched).
  4. 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.
  5. 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.
  6. The --json output preserves the original tag order (no reordering); tag prioritization is a display-only concern for the table.
  7. 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().

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions