Skip to content

Tag Display Prioritization #226

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions