Skip to content

fix(chat): drop backend commands that collide with a desktop alias - #818

Merged
fathah merged 2 commits into
fathah:mainfrom
AmirF194:fix/slash-catalog-desktop-alias-collision
Jul 4, 2026
Merged

fathah merged 2 commits into
fathah:mainfrom
AmirF194:fix/slash-catalog-desktop-alias-collision

Conversation

@AmirF194

@AmirF194 AmirF194 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #817

Problem

The slash-catalog reconciliation filtered backend agent commands against desktop command names but not desktop aliases — an asymmetry with its two sibling filters (desktop-target aliases and the alias map both already exclude desktop aliases).

A backend command whose name equals a desktop alias therefore survived into the merged catalog and was registered as an agent command; when the desktop command later registered that same name as its alias, createSlashCatalog threw Duplicate slash command alias and the app crashed on startup.

Today the only trigger is a backend /commands command, since desktop help aliases commands — but it's the same startup-crash class as the /compact collisions in #802 / #804 / #813.

Fix

  • Extract the reconciliation out of Chat.tsx's useMemo into a pure, exported reconcileSlashCatalog so the merge is unit-testable (mirrors the tested-helper approach from fix(chat): drop canon alias that duplicates a pairs command #808).
  • In that helper, exclude any backend command whose name matches a desktop name OR alias.

Desktop authoring still wins deterministically (/commands resolves to the desktop help command), and createSlashCatalog's throw-on-collision guard for genuine desktop-vs-desktop authoring conflicts is left intact.

Tests

Added reconcileSlashCatalog specs in commandCatalog.test.ts:

  • backend /commands colliding with help's commands alias no longer throws, and resolves to the desktop command
  • desktop wins over a same-named backend command
  • non-colliding backend commands are still exposed

vitest run src/renderer/src/screens/Chat/slash/ → 23 passed. tsc --noEmit clean. lat check passes.

The slash catalog reconciliation filtered backend agent commands against
desktop command *names* but not desktop *aliases*, an asymmetry with the
two sibling filters (desktop-target aliases and the alias map both already
exclude desktop aliases). A backend command whose name equals a desktop
alias therefore survived into the merged catalog and was registered as an
agent command; when the desktop command later registered that same name as
its alias, createSlashCatalog threw `Duplicate slash command alias` and the
app crashed on startup.

The only current trigger is a backend `/commands` command, since desktop
`help` aliases `commands` — but it is the same startup-crash class as the
`/compact` collisions in fathah#802 / fathah#804 / fathah#813.

Extract the reconciliation from Chat.tsx's useMemo into a pure, exported
`reconcileSlashCatalog` so the merge is unit-testable, and exclude any
backend command whose name matches a desktop name OR alias. Desktop
authoring still wins deterministically, and createSlashCatalog's
throw-on-collision guard for genuine desktop-vs-desktop conflicts is intact.
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a startup crash where a backend /commands command collided with the help desktop command's commands alias, by extracting the slash catalog reconciliation into a pure reconcileSlashCatalog helper and extending the backend-command filter to cover desktop aliases in addition to desktop names.

  • commandCatalog.ts: Adds reconcileSlashCatalog, a self-contained, exported function that performs the full backend-vs-desktop merge; the key fix is !desktopAliases.has(command.name) alongside the existing !desktopNames.has(command.name) guard.
  • Chat.tsx: Slims the slashCatalog useMemo from ~40 lines to a single reconcileSlashCatalog(...) call; logic is identical but now lives in a testable unit.
  • commandCatalog.test.ts: Adds three reconcileSlashCatalog specs — alias-collision regression, desktop-wins-over-same-named-backend, and non-colliding backend command passthrough — all matching the fix.

Confidence Score: 5/5

Safe to merge — the change is a targeted extraction and single-predicate fix with a direct regression test that would have caught the original crash.

The refactoring is mechanical — the reconciliation logic from Chat.tsx is moved verbatim into reconcileSlashCatalog, with exactly one predicate added (!desktopAliases.has(command.name)) to close the reported gap. Three new unit tests cover the regression, the desktop-wins case, and the non-colliding passthrough. No new control paths were introduced; createSlashCatalog's throw-on-collision guard for genuine authoring conflicts remains intact.

No files require special attention.

Important Files Changed

Filename Overview
src/renderer/src/screens/Chat/slash/commandCatalog.ts Adds reconcileSlashCatalog with the corrected alias-collision guard; logic is a clean extraction of the existing Chat.tsx useMemo with the single-line alias check added.
src/renderer/src/screens/Chat/Chat.tsx useMemo reduced to a thin call-site; dependency array unchanged; behavior is preserved exactly.
src/renderer/src/screens/Chat/slash/commandCatalog.test.ts Three new specs for reconcileSlashCatalog cover the regression, desktop-wins, and passthrough cases; all build on existing test infrastructure.
lat.md/chat-commands.md Documentation updated to describe reconcileSlashCatalog and clarify the two-layer reconciliation; accurate and consistent with the implementation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[agentCommandCatalog] --> B[reconcileSlashCatalog]
    C[desktopCommands] --> B
    D[fallbackAgentCommands] --> B

    B --> E[agentCommandsFromCatalog]
    E --> F{discovered?}

    F -- yes --> G[discovered.commands]
    F -- no --> H[fallbackAgentCommands]

    G --> I[Filter: not in desktopNames AND not in desktopAliases NEW]
    H --> I

    E --> J[discovered.aliases]
    J --> K[desktopTargetAliases backend alias to desktop target]
    J --> L[pass-through aliases non-desktop source and target]

    K --> M[agentCommands array]
    I --> M

    M --> N[createSlashCatalog]
    C --> N
    L --> N

    N --> O[SlashCommandCatalog]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[agentCommandCatalog] --> B[reconcileSlashCatalog]
    C[desktopCommands] --> B
    D[fallbackAgentCommands] --> B

    B --> E[agentCommandsFromCatalog]
    E --> F{discovered?}

    F -- yes --> G[discovered.commands]
    F -- no --> H[fallbackAgentCommands]

    G --> I[Filter: not in desktopNames AND not in desktopAliases NEW]
    H --> I

    E --> J[discovered.aliases]
    J --> K[desktopTargetAliases backend alias to desktop target]
    J --> L[pass-through aliases non-desktop source and target]

    K --> M[agentCommands array]
    I --> M

    M --> N[createSlashCatalog]
    C --> N
    L --> N

    N --> O[SlashCommandCatalog]
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into pr/818" | Re-trigger Greptile

Comment on lines +153 to +156
const desktopNames = new Set(desktopCommands.map((command) => command.name));
const desktopAliases = new Set(
desktopCommands.flatMap((command) => command.aliases ?? []),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The desktopAliases Set is populated from raw alias strings (e.g. "commands"), while backend command names arriving from agentCommandsFromCatalog are already normalized by normalizeName (lowercased, leading slashes stripped). If a desktop command ever defines an alias with a leading slash (e.g. "/commands") or mixed casing, desktopAliases.has(command.name) will return false for the normalized backend name and the collision guard will silently miss it — leaving createSlashCatalog to throw at runtime. The same inconsistency affects desktopNames, which is also un-normalized. Normalizing both sets at construction time closes the gap without changing any behavior for the current, well-formed inputs.

Suggested change
const desktopNames = new Set(desktopCommands.map((command) => command.name));
const desktopAliases = new Set(
desktopCommands.flatMap((command) => command.aliases ?? []),
);
const desktopNames = new Set(desktopCommands.map((command) => normalizeName(command.name)));
const desktopAliases = new Set(
desktopCommands.flatMap((command) => (command.aliases ?? []).map(normalizeName)),
);

@fathah fathah left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed and verified locally — approving.

Confirmed the bug is live on main: replicated the current Chat.tsx merge logic with a backend catalog exposing /commands against the desktop help command (which aliases commands) — it throws Duplicate slash command alias: /commands exactly as described in #813. So this is not covered by #808, which fixed the backend-internal pairs-vs-canon collision; this PR closes the remaining backend-command-vs-desktop-alias axis.

Verification performed:

  • The reconcileSlashCatalog extraction is a faithful move of the Chat.tsx useMemo logic — the desktopTargetAliases and alias-map filters are byte-identical; the only behavioral change is the added !desktopAliases.has(command.name) guard, which is the fix.
  • Mutation-tested the regression test: removing just the alias check makes it fail with the exact production error; with the fix, all 23 slash tests pass.
  • Mixed-case squatting is covered — normalizeName lowercases backend names before the filter.
  • The catalog: null fallback path now also gets alias protection — a strict improvement over main.
  • createSlashCatalog's throw-on-collision stays intact for genuine desktop-vs-desktop authoring conflicts, consistent with the layering established in #808.
  • tsc clean (the one i18n/index.ts error is pre-existing on main), eslint clean on touched files (the 3 exhaustive-deps warnings in Chat.tsx are pre-existing, outside this diff), lat check passes, and the lat.md update accurately documents the new reconciliation layer.

Nice work keeping the fix at the reconciliation boundary and making the merge unit-testable.

@fathah
fathah merged commit f528c35 into fathah:main Jul 4, 2026
2 checks passed
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.

Backend command whose name equals a desktop alias crashes startup (Duplicate slash command alias)

2 participants