Typed slash-command registry + ClientCommand routing (fixes get_transcript leak)#330
Merged
Conversation
…mand
Replaces the three hand-synchronized, string-keyed slash-command lists
(the zarvis adapter's SLASH_COMMANDS popup array, its after-submit
string-prefix ladder, and the TUI's run_slash_command table) with one
typed source of truth, and fixes the agentd_get_transcript leak by making
model-visibility a property of each command rather than an ad-hoc
"string-sniff the `tui` tool" check.
protocol (new `slash` module):
- CommandId (dispatch key) + dimension enums: Routing, ModelVisibility,
TranscriptPolicy, Render, Args.
- A SlashCommand descriptor table (COMMANDS) — one row per command — plus
resolve()/by_id()/popup_names() helpers and an is_model_hidden() predicate.
- New SessionEvent::ClientCommand { id, args } carrying the typed CommandId,
replacing the `tui` ToolUse convention for client-routed commands.
zarvis adapter:
- The after-submit dispatch resolves the verb once, then branches on
cmd.routing / cmd.id (no more strip_prefix/== string matching). Adapter
commands (model/reset/compact) mutate state here; /loop synthesizes the
tool call; client commands emit ClientCommand. Unknown verbs fall back to
the legacy `tui` tool.
- The `/` popup is now driven by slash::popup_names() (SLASH_COMMANDS deleted).
client (TUI):
- Dispatches ClientCommand by reconstructing the canonical verb from the
registry and reusing run_slash_command.
mcp:
- agentd_get_transcript filters slash::is_model_hidden events, so control
commands (/zoom, and the legacy `tui` fallback) stay in the durable
transcript for forensics but never reach a reading model.
Verified end-to-end against a live daemon + agentd-mcp: a /zoom ClientCommand
stays on disk but is stripped from the model-facing transcript tool.
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.
Why
Slash commands were described in three hand-synchronized, string-keyed places:
the zarvis adapter's
SLASH_COMMANDSpopup array, its after-submitstrip_prefix/==ladder, and the TUI'srun_slash_commandtable. Eachconsumer re-derived a command's behavior from its name string — fragile ("keep
in lockstep" comments), and the reason UI-control commands leaked into the
model-facing transcript: a
/zoomwas encoded as a faketuiToolUse thatagentd_get_transcriptreturned verbatim to any reading model.This replaces the strings with typed dimensions. A command is parsed into a
CommandIdonce, at the input edge; every decision after that — routing,persistence, model-visibility, rendering, the popup list — is a match on the id
or a read of a descriptor field.
What
protocol— newslashmodule (single source of truth):CommandId(dispatch key) + dimension enums:Routing,ModelVisibility,TranscriptPolicy,Render,Args.SlashCommanddescriptor table (COMMANDS, one row per command) withresolve()/by_id()/popup_names()and anis_model_hidden()predicate.SessionEvent::ClientCommand { id, args }carrying the typedCommandId,replacing the
tuiToolUse convention for client-routed commands.zarvis adapter:
cmd.routing/cmd.id— no more string matching.model/reset/compactmutate state here;
/loopsynthesizes its tool call; client commands emitClientCommand; unknown verbs fall back to the legacytuitool./popup is driven byslash::popup_names()(SLASH_COMMANDSdeleted).client (TUI): dispatches
ClientCommandby reconstructing the canonical verbfrom the registry and reusing
run_slash_command.mcp:
agentd_get_transcriptfiltersslash::is_model_hiddenevents, socontrol commands stay in the durable transcript (forensics) but never reach a
reading model.
Verified
Unit tests (5 in
slash): table invariants, alias/case-insensitive resolve,every client command is model-invisible, and the
is_model_hiddenfilter.agentd-adapter-zarvis: all 33interactivetests pass (incl. the updatedpopup test).
End-to-end against a live daemon + real
agentd-mcp: injected a messageand a
/zoomClientCommand, then read the session two ways —Notes for review
/popup now shows canonical names only —/exitis an alias of/quit(still resolves when typed) so it's no longer a separate ghostentry; popup order is now sorted for determinism.
/foostill uses the legacytuifallback (kept so stray slashesdon't get sent to the model); both the
tuifallback andClientCommandare now covered by the transcript filter.
representable (e.g. a
Clientcommand markedEffect); guarded with a testrather than encoded in the type — reasonable for ~15 commands.
the popup note above.