[Feat] Wire Gateway commands.list RPC into slash menu#429
[Feat] Wire Gateway commands.list RPC into slash menu#429
Conversation
Replaces the hardcoded STATIC_SLASH_COMMANDS list with a dynamic catalog hydrated from the upstream 'commands.list' RPC (openclaw/openclaw#62656). - shared: add CommandEntry / CommandArg / CommandsListParams / CommandsListResponse types mirroring the Gateway schema (7 categories, source=native|skill|plugin, scope=text|native|both, structured args) - main: new gateway-client.listCommands + ws:commands-list IPC handler; preload bridge + .d.ts exposure - core: GatewayTransportPort.listCommands; ui-store.commandCatalogByGateway + setter; gateway-dispatcher fetches commands in parallel with models/agents/tools/skills on connect - pwa: mirror the same port on the web adapter - renderer: slash-commands.ts rewritten around CommandEntry -> SlashCommandView; NATIVE_OVERLAY preserves choices for /think /fast /verbose /reasoning /usage /elevated /activation when Gateway omits them; mergeWithOverlay composes gateway data with local defaults; alias-aware prefix filter - UI: SlashCommandMenu + SlashCommandDashboard show source badges for skill/plugin; Dashboard gains search input, empty state, fixed-height scroll (no jitter on result count change); shared CommandSourceBadge component - keyboard: rAF re-focus + window-level keydown fallback in useSlashAutocomplete to survive Radix Dialog FocusScope handoff - i18n: 8 locales updated - 7 category keys + 3 source labels + search placeholder + empty state; key parity preserved Signed-off-by: samzong <samzong.lu@gmail.com> ## Considered and deferred - packages/desktop/src/renderer/lib/slash-commands.ts [BOT-TASTE]: Default category for uncategorized gateway commands resolves to 'status'. Both 'status' and 'tools' are defensible buckets; upstream CommandEntry.category is optional by schema. Revisit after UX feedback. - packages/desktop/src/renderer/lib/slash-commands.ts [BOT-NIT]: SlashCommandView.acceptsArgs is populated but not currently consumed by any renderer. Kept for parity with upstream CommandEntry shape; safe to drop in a later cleanup. - packages/desktop/src/renderer/lib/slash-commands.ts [BOT-NIT]: getCommandsForGateway runs two sequential .map passes. Cold path (per-reconnect); fuse if profiling ever points here. - packages/desktop/src/renderer/lib/slash-commands.ts [BOT-NIT]: filterSlashCommands lower-cases aliases per keystroke. Negligible for <50 commands; pre-lowercase at view-build time if catalog scales. - packages/desktop/src/renderer/lib/slash-commands.ts [BOT-SCOPE]: No unit tests for mergeWithOverlay / commandEntryToView / detectPickerType / getCommandsForGateway. Follow-up PR to cover overlay precedence and empty-catalog fallback. - packages/shared/src/types.ts [BOT-NIT]: CommandEntry / CommandArg / CommandsListResponse lack JSDoc anchoring to the Gateway upstream schema. Additive types are back-compat; link in a later docs pass. - packages/desktop/src/renderer/components/ChatInput/useSlashAutocomplete.ts [BOT-TASTE]: activeGatewayId selector duplicated in SlashCommandDashboard. Pre-existing pattern across 4+ renderer sites; extract useActiveGatewayId() hook in a cleanup PR. - packages/desktop/src/renderer/components/ChatInput/useSlashAutocomplete.ts [BOT-TASTE]: Window-level keydown fallback survives Radix Dialog FocusScope handoff; document.activeElement guard keeps it a no-op on the happy path. Document the rationale inline in a follow-up. - packages/desktop/src/renderer/components/ChatInput/useSlashAutocomplete.ts [BOT-TASTE]: buildArgOptions reads useTaskStore and useUiStore via getState(). Functionally correct; consider lifting to explicit selectors for surface clarity. Signed-off-by: samzong <samzong.lu@gmail.com>
Deploying cpwa with
|
| Latest commit: |
98b0a42
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1d8c1371.cpwa.pages.dev |
| Branch Preview URL: | https://feat-dynamic-slash-commands.cpwa.pages.dev |
|
Hi @samzong, DetailsInstructions for interacting with me using comments are available here. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request transitions the application from a hardcoded slash command list to a dynamic, gateway-driven architecture. By consuming the upstream Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements dynamic slash command discovery by fetching command catalogs from the gateway. It introduces the listCommands RPC across all layers (core, desktop, and PWA), updates the UI store to manage command catalogs per gateway, and refactors the slash command autocomplete and dashboard to support searching, filtering, and displaying command sources. Feedback focuses on resolving a focus guard that potentially breaks keyboard navigation in the argument picker, refactoring the IPC handler to use the gatewayRpc helper for consistency, and refining the command overlay logic to ensure proper metadata fallback when gateway definitions are incomplete.
| if (!argPickerVisible) return; | ||
| const raf = requestAnimationFrame(() => textareaRef.current?.focus()); | ||
| const onKeyDown = (e: globalThis.KeyboardEvent) => { | ||
| if (document.activeElement === textareaRef.current) return; |
There was a problem hiding this comment.
This focus guard prevents the global keydown listener from handling navigation keys (ArrowUp/Down, Enter, etc.) when the textarea is focused. Since the useEffect explicitly focuses the textarea when the arg picker is visible (line 128), this guard effectively disables keyboard navigation for the picker in the most common scenario. If this was intended as a fallback for lost focus, it should still allow handling when focused unless another listener is specifically handling these keys on the textarea to avoid cursor movement while navigating the picker.
| ipcMain.handle('ws:commands-list', async (_event, payload: { gatewayId: string } & CommandsListParams) => { | ||
| const gw = getGatewayClient(payload.gatewayId); | ||
| if (!gw?.isConnected) return { ok: false, error: 'gateway not connected' }; | ||
| const params: Record<string, unknown> = { | ||
| scope: payload.scope ?? 'text', | ||
| includeArgs: payload.includeArgs ?? true, | ||
| }; | ||
| if (payload.agentId) params.agentId = payload.agentId; | ||
| if (payload.provider) params.provider = payload.provider; | ||
| try { | ||
| const result = await gw.listCommands(params); | ||
| return { ok: true, result }; | ||
| } catch (err) { | ||
| return { ok: false, error: err instanceof Error ? err.message : 'unknown error' }; | ||
| } | ||
| }); |
There was a problem hiding this comment.
The ws:commands-list handler can be simplified by using the gatewayRpc helper function defined earlier in the file. This ensures consistent error handling (including errorCode and errorDetails) and reduces boilerplate code for checking the gateway connection.
ipcMain.handle('ws:commands-list', async (_event, payload: { gatewayId: string } & CommandsListParams) =>
gatewayRpc(payload.gatewayId, (gw) =>
gw.listCommands({
scope: payload.scope ?? 'text',
includeArgs: payload.includeArgs ?? true,
agentId: payload.agentId,
provider: payload.provider,
}),
),
);| if (!gwFirst || !ovFirst) return entry; | ||
| const gwHasChoices = (gwFirst.choices?.length ?? 0) > 0; | ||
| const ovHasChoices = (ovFirst.choices?.length ?? 0) > 0; | ||
| if (gwHasChoices || !ovHasChoices) return entry; |
There was a problem hiding this comment.
The current logic in mergeWithOverlay returns early if the gateway entry is missing arguments (!gwFirst). However, the purpose of the overlay is to provide metadata (like choices) that the gateway might omit. If the gateway omits the entire argument definition for a native command, we should fall back to the overlay's argument definition entirely to ensure the UI can still render the appropriate picker.
if (!ovFirst) return entry;
if (!gwFirst) return { ...entry, args: overlay.args };
const gwHasChoices = (gwFirst.choices?.length ?? 0) > 0;
const ovHasChoices = (ovFirst.choices?.length ?? 0) > 0;
if (gwHasChoices || !ovHasChoices) return entry;There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98b0a42789
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| deps.gateway.listAgents(gatewayId), | ||
| deps.gateway.getToolsCatalog(gatewayId), | ||
| deps.gateway.getSkillsStatus(gatewayId), | ||
| deps.gateway.listCommands(gatewayId, { scope: 'text', includeArgs: true }), |
There was a problem hiding this comment.
Fetch command catalogs with task context
commands.list was wired with optional agentId/provider params, but this call always requests a single gateway-wide catalog (scope: 'text') and stores it under commandCatalogByGateway. In environments where command availability is agent/provider-specific, tasks on the same gateway will reuse the wrong command set, so users can see missing commands for the active task or commands that fail at send time. Please fetch with active task context (or key the cache by that context) instead of only by gateway.
Useful? React with 👍 / 👎.
Summary
Replaces the hardcoded
STATIC_SLASH_COMMANDSlist with a dynamic catalog hydrated from the upstreamcommands.listRPC (openclaw/openclaw#62656). The slash menu in ChatInput and the/command Dashboard now reflect the actual native / skill / plugin commands available on the connected Gateway, with a local overlay supplyingchoicesfor native commands (e.g./think,/fast,/verbose) when the Gateway omits them and a full static fallback when the Gateway is unreachable.Type of change
[Feat]new feature[Fix]bug fix[UI]UI or UX change[Docs]documentation-only change[Refactor]internal cleanup[Build]CI, packaging, or tooling change[Chore]maintenanceWhy is this needed?
The old static table drifted out of sync with OpenClaw's native command surface and could never show workspace skills or plugin commands. Upstream PR openclaw/openclaw#62656 added a
commands.listgateway RPC that returns a structured, per-session command list (with source, scope, category, and argument definitions). ClawWork needs to consume this RPC so the slash UI reflects the real set of commands available to the active session.What changed?
CommandEntry/CommandArg/CommandArgChoice/CommandsListParams/CommandsListResponse/CommandSource/CommandScope/CommandCategorytypes mirroring the Gateway schema one-to-one.gateway-client.listCommands()(commands.listsendReq),ws:commands-listIPC handler defaulting toscope: 'text'+includeArgs: true, preload bridge +.d.tsexposure.GatewayTransportPort.listCommands,uiStore.commandCatalogByGateway+setCommandCatalogForGateway,gateway-dispatcher.fetchCatalogspulls commands in parallel with models / agents / tools / skills on connect.slash-commands.tsrewritten aroundCommandEntry → SlashCommandView.NATIVE_OVERLAYpreserveschoicesfor/think /fast /verbose /reasoning /usage /elevated /activationwhen Gateway data omits them (only forsource: 'native'entries).mergeWithOverlaycomposes gateway data with local defaults; alias-aware prefix filter handlestextAliases(e.g./elev→/elevated).SlashCommandMenuandSlashCommandDashboardshow aCommandSourceBadgefor skill/plugin commands; Dashboard gains a search input, empty state, and fixed-height scroll area (no jitter when result count changes).useSlashAutocompleteadds arequestAnimationFramere-focus + window-level keydown fallback so arg-picker navigation survives Radix Dialog FocusScope returning focus to the Dashboard trigger;document.activeElementarbitration prevents double-trigger.Architecture impact
listModels/listAgentschains).shared ← core ← desktop/pwa) preserved.docs/architecture-invariants.md:commands.listflows through the existing WS client).ipcMain.handle()registered once at app startup insideregisterWsHandlers.listCommandsmethod; no rawipcRenderer.listModels/listAgents(existing, reviewed patterns) at every layer.greptileai-style adversarial scan found 0 divergences from the canonical chain.Linked issues
Closes #
Depends on: openclaw/openclaw#62656 (merged; live in OpenClaw 2026.4.15-beta.1+).
Validation
pnpm lintpnpm testpnpm buildpnpm check:ui-contractCommands, screenshots, or notes:
Screenshots or recordings
UI changes: new
CommandSourceBadgevisible for non-native commands in both the popover and Dashboard; Dashboard has a search input with sticky position. All colors routed through existing CSS variables (--accent,--border-subtle,--text-muted, etc.) — no new hex values. Nopnpm check:ui-contractrules bent; one switch frommax-h-[60vh](arbitrary-value escape hatch) toh-96(token-compliant) landed during this PR to appease the layout-escape rule.Release note
NONE.Checklist
git commit -s)[Feat]Considered and deferred