Repository navigation
fix(chat): badge a sent slash word only when it is a real command - #902
Merged
xintaofei merged 11 commits intoOct 9, 2026
Merged
Conversation
A sent user message turned any `/word` or `$word` into a command badge on shape alone, so `/tmp`, `/etc` or a word like `/later` in prose rendered as a command the agent never offered. spacering-net#727 gated the composer on the agent's advertised list but left the transcript on the shape test. The bubble now reads the same list the composer's `/` menu offers (the agent's available commands, plus the on-disk `$` skills for Codex) through a context provided by MessageListView, and badges a bare token only when it is on that list, matched exactly. With no list yet, nothing is badged; once the agent advertises its commands, the matching tokens badge in place. `file:` and `codeg:` reference links are unchanged.
MessageListView's availableCommands prop is now required, though it may still be null. A surface that mounts a transcript without its agent's command list now fails type-check instead of silently never badging a real command in its user messages.
The token regex is a shape test only. Both the composer and the sent message bubble require a token to be a known invocation: a command the agent advertises, or one of Codex's on-disk skills. Drop the reference to a transcript rehype plugin that no longer exists.
Every mounted useAgentSkills instance refetches its folder's skills when the window regains focus, and each one used to drop the shared in-flight request first, so instances on the same key each started their own scan. A Codex conversation tab mounts two on one key, its composer and its transcript, so every focus scanned the disk twice per open tab. The first listener to see a focus event now invalidates the key, and the others join the refetch it started. Each instance still receives the refreshed list, and every new focus still refreshes.
Each useAgentSkills caller kept its own copy of a folder's skills. A reader that mounted after the list had loaded pinned that copy and never saw a later refresh, so a skill added while the window was in the background stayed missing from that composer's `$` menu and that transcript's badges until the component remounted. The list now lives in one store per window that every reader subscribes to, and a single focus listener rescans each folder something reads, once. A failed refresh keeps the last list instead of wiping it, a slower earlier scan can no longer overwrite a newer answer, a scan that finds nothing new keeps the array reference, and invalidation rescans the folders on screen while forgetting the rest.
…hook Move the list a transcript's user bubbles may badge out of MessageListView into useTranscriptKnownInvocations and test it: advertised commands for every agent, Codex's disk skills under `$`, a reference that holds still until one of its lists changes, and the badges it produces in a sent message. A source guard pins that MessageListView provides it, for its own agent and folder, around the whole thread.
Codex's on-disk skills are known invocations without being advertised, so the transcript context, the bubble renderer and the segment parser now describe what the agent offers.
A sent `/word` badges only when the agent advertises that command, and the live list arrives with each connection's handshake. Opening a conversation, a restart, and every reconnect showed the transcript first, so its command badges started out as text, or dropped to text and came back. Remember the names each agent last advertised in each folder (`advertised-commands-store`, persisted, size-capped, shared across windows) and let the transcript badge from that record while the connection's list is still unknown. The connection's own list, even an empty one, always wins. The record is written from the connection store's dispatch, off the reducer's result, so the live `available_commands` event and a snapshot carrying the list are one path, filed under the connection's own agent and cwd. The detail panel now passes `null` rather than `[]` while the list is unknown, which the composers read the same way and the transcript needs to tell "not yet" from "offers nothing".
One shared record that every window read, changed and wrote back could lose an entry: two windows recording different folders at once each wrote their own copy over the other's. A write that failed to persist was also dropped from memory by the next write's re-read, and an oversized entry left in storage could keep the record over its cap. Keep one localStorage key per agent and folder, stamped with when it was last advertised. A window writes only its own entry, keeps in memory what it could not store, and follows other windows entry by entry through the `storage` event, waking readers only when names change. The cap is measured on storage itself and drops an entry too large to ever fit before the least recently advertised ones.
A transcript viewer that did not know its agent defaulted to Claude, so before its connection advertised it would badge whatever Claude last advertised in that folder. Name the viewed connection's own agent instead, and with no agent known at all, badge no command. Also pin the stale-snapshot path that fills in a missing list, prove a newer record does not re-render a transcript that has its connection's list, and correct comments that said an empty list badges nothing (Codex's disk skills still do) or that the transcript knows what its composer does.
A `storage` event carries the value as of its own write, and events queue: one sent for another window's older write could land after this window wrote a newer list, put the older one back in this window's copy, and stay there, since a window hears no event for its own writes. Read what is stored when the event arrives instead, and never let an older stamp replace a newer list this window could not store. An entry storage cannot read back is no longer taken for one that is gone. Also correct the reason MessageListView's command list is required: left out, a transcript would badge from whatever the agent last advertised in the folder on any connection, not from its own connection's list.
Collaborator
|
codeg work task |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A sent message showed any
/wordor$wordas a command badge based on its shape alone. Paths like/tmpand/etc, or a word like/laterin prose, appeared in the transcript as commands the agent never offered. #727 fixed this for the composer by checking the agent's advertised commands, but the transcript still used the shape test.The message bubble now checks the agent's advertised commands, plus the on-disk
$skills for Codex.MessageListViewprovides that list to the bubbles, and a bare token becomes a badge only when it matches an entry exactly./revis not/review, and/Reviewis not/review.Until the agent advertises its commands (before its connection is up, or between a disconnect and a reconnect), the bubble checks the list that agent last advertised in the same folder, which is remembered across restarts, plus Codex's on-disk
$skills, which need no connection. In a folder the agent has never advertised in, only Codex's skills can badge, and a view with no agent behind it badges nothing. Once the agent advertises, its list replaces the remembered one, and matching tokens gain or lose their badge in place.Unchanged:
file:andcodeg:reference links. The composer still uses the live command list for its/menu and paste/seed hydration.Tests: updated the plain-text bubble tests and added cases for an unadvertised token, an empty or missing list, and the list arriving after first render.
Review follow-ups: every transcript surface must pass its command list. The remembered command names are kept in local storage per agent and folder, shared across windows, and capped in size. Codex's
$menu and the transcript now share one on-disk skills list per agent and folder within a window, refreshed once per window focus rather than once per mounted reader, and a failed refresh keeps the last good list. These follow-ups have tests.