Skip to content

fix(chat): badge a sent slash word only when it is a real command - #902

Merged
xintaofei merged 11 commits into
spacering-net:mainfrom
Adam-Dalloul:fix/slash-popup-real-commands
Oct 9, 2026
Merged

xintaofei merged 11 commits into
spacering-net:mainfrom
Adam-Dalloul:fix/slash-popup-real-commands

Conversation

@Adam-Dalloul

@Adam-Dalloul Adam-Dalloul commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

A sent message showed any /word or $word as a command badge based on its shape alone. Paths like /tmp and /etc, or a word like /later in 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. MessageListView provides that list to the bubbles, and a bare token becomes a badge only when it matches an entry exactly. /rev is not /review, and /Review is 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: and codeg: 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.

Adam-Dalloul and others added 11 commits October 8, 2026 11:48
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.
@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 299 is done — #902 (17 files, +1875/-129).

@xintaofei
xintaofei merged commit 9a09b07 into spacering-net:main Oct 9, 2026
7 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.

2 participants