11import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js" ;
2- import type {
3- ChatCommandDefinition ,
4- CommandArgChoice ,
5- } from "../../../../src/auto-reply/commands-registry.types.js" ;
2+ import type { CommandEntry , CommandsListResult } from "../../../../src/gateway/protocol/index.js" ;
3+ import type { GatewayBrowserClient } from "../gateway.ts" ;
64import type { IconName } from "../icons.ts" ;
75import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts" ;
86
@@ -24,6 +22,21 @@ export type SlashCommandDef = {
2422 shortcut ?: string ;
2523} ;
2624
25+ type LocalArgChoice = string | { value : string ; label : string } ;
26+
27+ type CommandLike = {
28+ key : string ;
29+ name : string ;
30+ aliases ?: string [ ] ;
31+ description : string ;
32+ args ?: Array < {
33+ name : string ;
34+ required ?: boolean ;
35+ choices ?: LocalArgChoice [ ] ;
36+ } > ;
37+ category ?: string ;
38+ } ;
39+
2740const COMMAND_ICON_OVERRIDES : Partial < Record < string , IconName > > = {
2841 help : "book" ,
2942 status : "barChart" ,
@@ -130,26 +143,22 @@ const COMMAND_ARGS_OVERRIDES: Partial<Record<string, string>> = {
130143 steer : "[id] <message>" ,
131144} ;
132145
133- function normalizeUiKey ( command : ChatCommandDefinition ) : string {
146+ function normalizeUiKey ( command : CommandLike ) : string {
134147 return command . key . replace ( / [: .- ] / g, "_" ) ;
135148}
136149
137- function getSlashAliases ( command : ChatCommandDefinition ) : string [ ] {
138- return command . textAliases
150+ function getSlashAliases ( command : CommandLike ) : string [ ] {
151+ return ( command . aliases ?? [ ] )
139152 . map ( ( alias ) => alias . trim ( ) )
140- . filter ( ( alias ) => alias . startsWith ( "/" ) )
141- . map ( ( alias ) => alias . slice ( 1 ) ) ;
153+ . filter ( Boolean )
154+ . map ( ( alias ) => ( alias . startsWith ( "/" ) ? alias . slice ( 1 ) : alias ) ) ;
142155}
143156
144- function getPrimarySlashName ( command : ChatCommandDefinition ) : string | null {
145- const aliases = getSlashAliases ( command ) ;
146- if ( aliases . length === 0 ) {
147- return null ;
148- }
149- return aliases [ 0 ] ?? null ;
157+ function getPrimarySlashName ( command : CommandLike ) : string | null {
158+ return command . name . trim ( ) || null ;
150159}
151160
152- function formatArgs ( command : ChatCommandDefinition ) : string | undefined {
161+ function formatArgs ( command : CommandLike ) : string | undefined {
153162 if ( ! command . args ?. length ) {
154163 return undefined ;
155164 }
@@ -161,28 +170,41 @@ function formatArgs(command: ChatCommandDefinition): string | undefined {
161170 . join ( " " ) ;
162171}
163172
164- function choiceToValue ( choice : CommandArgChoice ) : string {
173+ function choiceToValue ( choice : LocalArgChoice ) : string {
165174 return typeof choice === "string" ? choice : choice . value ;
166175}
167176
168- function getArgOptions ( command : ChatCommandDefinition ) : string [ ] | undefined {
177+ function getArgOptions ( command : CommandLike ) : string [ ] | undefined {
169178 const firstArg = command . args ?. [ 0 ] ;
170- if ( ! firstArg || typeof firstArg . choices === "function" ) {
179+ if ( ! firstArg ) {
171180 return undefined ;
172181 }
173182 const options = firstArg . choices ?. map ( choiceToValue ) . filter ( Boolean ) ;
174183 return options ?. length ? options : undefined ;
175184}
176185
177- function mapCategory ( command : ChatCommandDefinition ) : SlashCommandCategory {
178- return CATEGORY_OVERRIDES [ normalizeUiKey ( command ) ] ?? "tools" ;
186+ function mapCategory ( command : CommandLike ) : SlashCommandCategory {
187+ const override = CATEGORY_OVERRIDES [ normalizeUiKey ( command ) ] ;
188+ if ( override ) {
189+ return override ;
190+ }
191+ switch ( command . category ) {
192+ case "session" :
193+ return "session" ;
194+ case "options" :
195+ return "model" ;
196+ case "management" :
197+ return "tools" ;
198+ default :
199+ return "tools" ;
200+ }
179201}
180202
181- function mapIcon ( command : ChatCommandDefinition ) : IconName | undefined {
203+ function mapIcon ( command : CommandLike ) : IconName | undefined {
182204 return COMMAND_ICON_OVERRIDES [ normalizeUiKey ( command ) ] ?? "terminal" ;
183205}
184206
185- function toSlashCommand ( command : ChatCommandDefinition ) : SlashCommandDef | null {
207+ function toSlashCommand ( command : CommandLike ) : SlashCommandDef | null {
186208 const name = getPrimarySlashName ( command ) ;
187209 if ( ! name ) {
188210 return null ;
@@ -200,12 +222,100 @@ function toSlashCommand(command: ChatCommandDefinition): SlashCommandDef | null
200222 } ;
201223}
202224
203- export const SLASH_COMMANDS : SlashCommandDef [ ] = [
204- ...buildBuiltinChatCommands ( )
225+ function normalizeCommandEntry ( entry : CommandEntry ) : CommandLike {
226+ const aliases = Array . isArray ( entry . textAliases )
227+ ? entry . textAliases . filter ( ( alias ) => typeof alias === "string" )
228+ : [ ] ;
229+ const primaryAlias = aliases . find ( ( alias ) => alias . startsWith ( "/" ) ) ;
230+ const primaryName = primaryAlias ? primaryAlias . slice ( 1 ) : entry . name ;
231+ return {
232+ key : primaryName ,
233+ name : primaryName ,
234+ aliases,
235+ description : entry . description ,
236+ args : entry . args ?. map ( ( arg ) => ( {
237+ name : arg . name ,
238+ required : arg . required ,
239+ choices : arg . dynamic ? undefined : arg . choices ,
240+ } ) ) ,
241+ category : entry . category ,
242+ } ;
243+ }
244+
245+ function replaceSlashCommands ( next : SlashCommandDef [ ] ) {
246+ SLASH_COMMANDS . splice ( 0 , SLASH_COMMANDS . length , ...next ) ;
247+ }
248+
249+ function buildSlashCommandsFromEntries ( entries : CommandEntry [ ] ) : SlashCommandDef [ ] {
250+ const mapped = entries
251+ . map ( normalizeCommandEntry )
205252 . map ( toSlashCommand )
206- . filter ( ( command ) : command is SlashCommandDef => command !== null ) ,
207- ...UI_ONLY_COMMANDS ,
208- ] ;
253+ . filter ( ( command ) : command is SlashCommandDef => command !== null ) ;
254+ const deduped = new Map < string , SlashCommandDef > ( ) ;
255+ for ( const command of [ ...mapped , ...UI_ONLY_COMMANDS ] ) {
256+ const key = normalizeLowercaseStringOrEmpty ( command . name ) ;
257+ if ( ! key || deduped . has ( key ) ) {
258+ continue ;
259+ }
260+ deduped . set ( key , command ) ;
261+ }
262+ return Array . from ( deduped . values ( ) ) ;
263+ }
264+
265+ function buildFallbackSlashCommands ( ) : SlashCommandDef [ ] {
266+ const builtins = buildBuiltinChatCommands ( )
267+ . map ( ( command ) => ( {
268+ key : command . key ,
269+ name : command . textAliases [ 0 ] ?. replace ( / ^ \/ / , "" ) ?? command . key ,
270+ aliases : command . textAliases ,
271+ description : command . description ,
272+ args : command . args ?. map ( ( arg ) => ( {
273+ name : arg . name ,
274+ required : arg . required ,
275+ choices : Array . isArray ( arg . choices ) ? arg . choices : undefined ,
276+ } ) ) ,
277+ category : command . category ,
278+ } ) )
279+ . map ( toSlashCommand )
280+ . filter ( ( command ) : command is SlashCommandDef => command !== null ) ;
281+ return buildSlashCommandsFromEntries ( [ ] ) . concat (
282+ builtins . filter (
283+ ( command ) =>
284+ ! UI_ONLY_COMMANDS . some (
285+ ( uiCommand ) =>
286+ normalizeLowercaseStringOrEmpty ( uiCommand . name ) ===
287+ normalizeLowercaseStringOrEmpty ( command . name ) ,
288+ ) ,
289+ ) ,
290+ ) ;
291+ }
292+
293+ export const SLASH_COMMANDS : SlashCommandDef [ ] = buildFallbackSlashCommands ( ) ;
294+
295+ export async function refreshSlashCommands ( params : {
296+ client : GatewayBrowserClient | null ;
297+ agentId ?: string | null ;
298+ } ) : Promise < void > {
299+ const agentId = params . agentId ?. trim ( ) ;
300+ if ( ! params . client || ! agentId ) {
301+ replaceSlashCommands ( buildFallbackSlashCommands ( ) ) ;
302+ return ;
303+ }
304+ try {
305+ const result = await params . client . request < CommandsListResult > ( "commands.list" , {
306+ agentId,
307+ includeArgs : true ,
308+ scope : "text" ,
309+ } ) ;
310+ replaceSlashCommands ( buildSlashCommandsFromEntries ( result ?. commands ?? [ ] ) ) ;
311+ } catch {
312+ replaceSlashCommands ( buildFallbackSlashCommands ( ) ) ;
313+ }
314+ }
315+
316+ export function resetSlashCommandsForTest ( ) : void {
317+ replaceSlashCommands ( buildFallbackSlashCommands ( ) ) ;
318+ }
209319
210320const CATEGORY_ORDER : SlashCommandCategory [ ] = [ "session" , "model" , "tools" , "agents" ] ;
211321
0 commit comments