Skip to content

[Feat] Wire Gateway commands.list RPC into slash menu#429

Open
samzong wants to merge 1 commit intomainfrom
feat/dynamic-slash-commands
Open

[Feat] Wire Gateway commands.list RPC into slash menu#429
samzong wants to merge 1 commit intomainfrom
feat/dynamic-slash-commands

Conversation

@samzong
Copy link
Copy Markdown
Collaborator

@samzong samzong commented Apr 16, 2026

Summary

Replaces the hardcoded STATIC_SLASH_COMMANDS list with a dynamic catalog hydrated from the upstream commands.list RPC (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 supplying choices for 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] maintenance

Why 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.list gateway 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?

  • shared: new CommandEntry / CommandArg / CommandArgChoice / CommandsListParams / CommandsListResponse / CommandSource / CommandScope / CommandCategory types mirroring the Gateway schema one-to-one.
  • main: gateway-client.listCommands() (commands.list sendReq), ws:commands-list IPC handler defaulting to scope: 'text' + includeArgs: true, preload bridge + .d.ts exposure.
  • core: GatewayTransportPort.listCommands, uiStore.commandCatalogByGateway + setCommandCatalogForGateway, gateway-dispatcher.fetchCatalogs pulls commands in parallel with models / agents / tools / skills on connect.
  • pwa: mirrors the same port on the web adapter (gateway client + gateway adapter + store bridge) so the PWA path stays consistent.
  • renderer: slash-commands.ts rewritten around CommandEntry → SlashCommandView. NATIVE_OVERLAY preserves choices for /think /fast /verbose /reasoning /usage /elevated /activation when Gateway data omits them (only for source: 'native' entries). mergeWithOverlay composes gateway data with local defaults; alias-aware prefix filter handles textAliases (e.g. /elev/elevated).
  • UI: SlashCommandMenu and SlashCommandDashboard show a CommandSourceBadge for skill/plugin commands; Dashboard gains a search input, empty state, and fixed-height scroll area (no jitter when result count changes).
  • keyboard: useSlashAutocomplete adds a requestAnimationFrame re-focus + window-level keydown fallback so arg-picker navigation survives Radix Dialog FocusScope returning focus to the Dashboard trigger; document.activeElement arbitration prevents double-trigger.
  • i18n: 8 locales (en / zh / zh-TW / ja / ko / es / pt / de) updated — 7 category keys + 3 source labels + search placeholder + empty-state string; deterministic key sorting preserved; parity check passes.

Architecture impact

  • Owning layer: shared + core + main + preload + renderer + pwa (cross-layer RPC wiring, same shape as the existing listModels / listAgents chains).
  • Cross-layer impact: yes — additive only. New port method, new store slot, new shared types. No existing behavior path modified; layering direction (shared ← core ← desktop/pwa) preserved.
  • Invariants touched from docs/architecture-invariants.md:
    • Single Gateway WS connection (commands.list flows through the existing WS client).
    • ipcMain.handle() registered once at app startup inside registerWsHandlers.
    • No Node builtins / electron imports leaked into renderer.
    • Preload bridge exposes only the narrow listCommands method; no raw ipcRenderer.
    • Task / session isolation untouched — catalog is gateway-scoped, not session-scoped.
  • Why those invariants remain protected: the new code slots into the exact same template as 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 lint
  • pnpm test
  • pnpm build
  • pnpm check:ui-contract
  • Manual smoke test
  • Not run

Commands, screenshots, or notes:

pnpm check       # EXIT=0 — lint + architecture + ui-contract +
                 # renderer-copy + i18n (8 locales parity OK) +
                 # knip (dead-code) + format + typecheck +
                 # 237/237 desktop tests + 73/73 core + 61/61 pwa
pre-ship         # 7-lens adversarial scan: 0 MUST-FIX, 9 deferred
                 # (see "Considered and deferred" at commit body tail)
manual           # `/` menu shows gateway commands including skill/plugin
                 # source badges; `/think` picker shows choices; model
                 # picker for `/model` still resolves via modelCatalog;
                 # offline fallback surfaces 19 native commands;
                 # dashboard search input stays fixed as results change.

Screenshots or recordings

UI changes: new CommandSourceBadge visible 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. No pnpm check:ui-contract rules bent; one switch from max-h-[60vh] (arbitrary-value escape hatch) to h-96 (token-compliant) landed during this PR to appease the layout-escape rule.

Release note

  • No user-facing change. Release note is NONE.
  • User-facing change. Release note is included below.
Slash commands (`/`) are now sourced live from the connected gateway. Skills
and plugin commands appear alongside native commands, with source badges in
the menu and dashboard. `/think`, `/fast`, and similar options retain their
quick-select arg pickers.

Checklist

  • All commits are signed off (git commit -s)
  • The PR title uses at least one approved prefix: [Feat]
  • The summary explains both what changed and why
  • Validation reflects the commands actually run for this PR
  • Architecture impact is described and references any touched invariants
  • Cross-layer changes are explicitly justified
  • The release note block is accurate

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.

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>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying cpwa with  Cloudflare Pages  Cloudflare Pages

Latest commit: 98b0a42
Status: ✅  Deploy successful!
Preview URL: https://1d8c1371.cpwa.pages.dev
Branch Preview URL: https://feat-dynamic-slash-commands.cpwa.pages.dev

View logs

@github-actions
Copy link
Copy Markdown
Contributor

Hi @samzong,
Thanks for your pull request!
If the PR is ready, use the /auto-cc command to assign Reviewer to Review.
We will review it shortly.

Details

Instructions for interacting with me using comments are available here.
If you have questions or suggestions related to my behavior, please file an issue against the gh-ci-bot repository.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 commands.list RPC, the slash menu and dashboard now accurately reflect the commands available for the active session, including those provided by skills and plugins. The changes span the entire stack—from shared type definitions and core transport ports to renderer-side UI components—ensuring consistent behavior across desktop and PWA environments while maintaining backward compatibility through a native command overlay.

Highlights

  • Dynamic Command Catalog: Replaced the static slash command list with a dynamic catalog fetched from the Gateway via the new commands.list RPC, enabling support for skill and plugin commands.
  • UI Enhancements: Updated the slash command menu and dashboard to display command source badges, added a search input to the dashboard, and improved layout stability.
  • Keyboard Navigation: Enhanced useSlashAutocomplete to ensure argument picker navigation remains functional when focus shifts between the dashboard and the editor.
  • Localization: Updated 8 locales with new category keys, source labels, and UI strings to support the expanded command catalog.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +395 to +410
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' };
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
      }),
    ),
  );

Comment on lines +271 to +274
if (!gwFirst || !ovFirst) return entry;
const gwHasChoices = (gwFirst.choices?.length ?? 0) > 0;
const ovHasChoices = (ovFirst.choices?.length ?? 0) > 0;
if (gwHasChoices || !ovHasChoices) return entry;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

1 participant