fix(chat): drop backend commands that collide with a desktop alias - #818
Conversation
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 SummaryThis PR fixes a startup crash where a backend
Confidence Score: 5/5Safe 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 No files require special attention. Important Files Changed
|
| const desktopNames = new Set(desktopCommands.map((command) => command.name)); | ||
| const desktopAliases = new Set( | ||
| desktopCommands.flatMap((command) => command.aliases ?? []), | ||
| ); |
There was a problem hiding this comment.
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.
| 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
left a comment
There was a problem hiding this comment.
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
reconcileSlashCatalogextraction is a faithful move of theChat.tsxuseMemologic — thedesktopTargetAliasesand 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 —
normalizeNamelowercases backend names before the filter. - The
catalog: nullfallback 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.tscclean (the onei18n/index.tserror is pre-existing on main), eslint clean on touched files (the 3exhaustive-depswarnings inChat.tsxare pre-existing, outside this diff),lat checkpasses, and thelat.mdupdate accurately documents the new reconciliation layer.
Nice work keeping the fix at the reconciliation boundary and making the merge unit-testable.
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,
createSlashCatalogthrewDuplicate slash command aliasand the app crashed on startup.Today the only trigger is a backend
/commandscommand, since desktophelpaliasescommands— but it's the same startup-crash class as the/compactcollisions in #802 / #804 / #813.Fix
Chat.tsx'suseMemointo a pure, exportedreconcileSlashCatalogso the merge is unit-testable (mirrors the tested-helper approach from fix(chat): drop canon alias that duplicates a pairs command #808).Desktop authoring still wins deterministically (
/commandsresolves to the desktophelpcommand), andcreateSlashCatalog's throw-on-collision guard for genuine desktop-vs-desktop authoring conflicts is left intact.Tests
Added
reconcileSlashCatalogspecs incommandCatalog.test.ts:/commandscolliding withhelp'scommandsalias no longer throws, and resolves to the desktop commandvitest run src/renderer/src/screens/Chat/slash/→ 23 passed.tsc --noEmitclean.lat checkpasses.