fix(tui): include plugin-registered commands in slash command autocom…#61966
fix(tui): include plugin-registered commands in slash command autocom…#61966ivanvmoreno wants to merge 1 commit into
Conversation
…plete Plugin commands registered via api.registerCommand() are stored in the pluginCommands map but were never surfaced in the TUI '/' autocomplete dropdown. listChatCommandsForConfig() only returns built-in commands; listPluginCommands() was only called for the /commands text response. Add a listPluginCommands() loop in getSlashCommands() so commands like /remember and /recall from external plugins appear as hints when typing '/'.
Greptile SummaryThis PR fixes a gap in the TUI slash command autocomplete ( The fix adds a loop in
Confidence Score: 4/5Safe to merge; the change is low-risk, isolated, and functionally correct. The implementation correctly mirrors the existing gateway-commands loop pattern, uses the seen set for deduplication, and normalizes names consistently. The only gap is missing test coverage for the new loop, which is low-risk given the simplicity of the logic.
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/tui/commands.ts
Line: 139-146
Comment:
**Missing test for new code path**
`src/tui/commands.test.ts` already tests `getSlashCommands`, but there is no coverage for the new plugin-command loop. Because `pluginCommands` is a global singleton (keyed by `Symbol.for("openclaw.pluginCommandsState")`), it can be populated and torn down in a test without module-level mocking. A minimal addition to the existing `describe("getSlashCommands", ...)` block would cover the happy path, the `seen`-based dedup guard, and name normalization:
```ts
import { registerPluginCommand, clearPluginCommands } from "../plugins/command-registration.js";
it("includes plugin-registered commands and skips duplicates", () => {
registerPluginCommand(
{ name: "myplugin", description: "My plugin cmd", handler: async () => ({ text: "" }) },
{ pluginId: "test-plugin" }
);
try {
const commands = getSlashCommands();
expect(commands.find((c) => c.name === "myplugin")).toMatchObject({
name: "myplugin",
description: "My plugin cmd",
});
// Built-in "help" must not be duplicated
expect(commands.filter((c) => c.name === "help")).toHaveLength(1);
} finally {
clearPluginCommands();
}
});
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(tui): include plugin-registered comm..." | Re-trigger Greptile |
| for (const command of listPluginCommands()) { | ||
| const name = command.name.trim().toLowerCase(); | ||
| if (!name || seen.has(name)) { | ||
| continue; | ||
| } | ||
| seen.add(name); | ||
| commands.push({ name, description: command.description }); | ||
| } |
There was a problem hiding this comment.
Missing test for new code path
src/tui/commands.test.ts already tests getSlashCommands, but there is no coverage for the new plugin-command loop. Because pluginCommands is a global singleton (keyed by Symbol.for("openclaw.pluginCommandsState")), it can be populated and torn down in a test without module-level mocking. A minimal addition to the existing describe("getSlashCommands", ...) block would cover the happy path, the seen-based dedup guard, and name normalization:
import { registerPluginCommand, clearPluginCommands } from "../plugins/command-registration.js";
it("includes plugin-registered commands and skips duplicates", () => {
registerPluginCommand(
{ name: "myplugin", description: "My plugin cmd", handler: async () => ({ text: "" }) },
{ pluginId: "test-plugin" }
);
try {
const commands = getSlashCommands();
expect(commands.find((c) => c.name === "myplugin")).toMatchObject({
name: "myplugin",
description: "My plugin cmd",
});
// Built-in "help" must not be duplicated
expect(commands.filter((c) => c.name === "help")).toHaveLength(1);
} finally {
clearPluginCommands();
}
});Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/commands.ts
Line: 139-146
Comment:
**Missing test for new code path**
`src/tui/commands.test.ts` already tests `getSlashCommands`, but there is no coverage for the new plugin-command loop. Because `pluginCommands` is a global singleton (keyed by `Symbol.for("openclaw.pluginCommandsState")`), it can be populated and torn down in a test without module-level mocking. A minimal addition to the existing `describe("getSlashCommands", ...)` block would cover the happy path, the `seen`-based dedup guard, and name normalization:
```ts
import { registerPluginCommand, clearPluginCommands } from "../plugins/command-registration.js";
it("includes plugin-registered commands and skips duplicates", () => {
registerPluginCommand(
{ name: "myplugin", description: "My plugin cmd", handler: async () => ({ text: "" }) },
{ pluginId: "test-plugin" }
);
try {
const commands = getSlashCommands();
expect(commands.find((c) => c.name === "myplugin")).toMatchObject({
name: "myplugin",
description: "My plugin cmd",
});
// Built-in "help" must not be duplicated
expect(commands.filter((c) => c.name === "help")).toHaveLength(1);
} finally {
clearPluginCommands();
}
});
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
Updates the TUI slash-command autocomplete so it also includes commands registered by plugins via api.registerCommand(), aligning the autocomplete list with what /commands already surfaces.
Changes:
- Import
listPluginCommands()into the TUI command list builder. - Append plugin-registered commands to
getSlashCommands()while de-duping against existing entries.
| for (const command of listPluginCommands()) { | ||
| const name = command.name.trim().toLowerCase(); | ||
| if (!name || seen.has(name)) { | ||
| continue; | ||
| } | ||
| seen.add(name); | ||
| commands.push({ name, description: command.description }); | ||
| } |
There was a problem hiding this comment.
The new plugin-command autocomplete behavior isn’t covered by tests. Since getSlashCommands() now merges results from listPluginCommands(), add a unit test that registers a plugin command (and clears the registry after) and asserts the command appears in the returned list (and is de-duped against existing names).
|
This pull request has been automatically marked as stale due to inactivity. |
|
Thanks for the context here. I did a careful shell check against current Current main already implements the requested TUI autocomplete behavior through the merged Gateway command-inventory path, so this local plugin-registry branch is obsolete and should not be merged. So I’m closing this as already implemented rather than keeping a duplicate issue open. Review detailsBest possible solution: Keep the current-main Gateway Do we have a high-confidence way to reproduce the issue? Yes for the historical bug by source inspection: older TUI autocomplete was built from static/local command lists while plugin commands lived in Gateway/runtime command inventory. Current main no longer has that gap because TUI fetches and merges text-scope Is this the best way to solve the issue? Yes. The current-main solution is the better fix because it reuses the Gateway runtime command inventory instead of adding a second TUI-only lookup against a process-local plugin singleton. Security review: Security review cleared: Cleared: the reviewed PR diff only reads existing plugin command metadata and introduces no dependency, workflow, credential, auth, package, or supply-chain change. What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against f07c87405c30; fix evidence: commit 6fcfeed5dc67, main fix timestamp 2026-05-19T03:55:26Z. |
## Summary - Problem: gateway-connected TUI slash autocomplete used its local/static command list, so plugin commands already exposed by the running Gateway through `commands.list` were invisible in TUI suggestions. - Why it matters: users can verify plugin commands such as `/phone`, `/pair`, or `/dreaming` in the Gateway command surface, but typing the same prefix in `openclaw tui` did not suggest those plugin-owned commands. - What changed: `GatewayChatClient` now exposes `commands.list`; TUI fetches text-scope command entries from the connected Gateway when supported and merges their aliases into slash autocomplete while keeping built-ins first. - What did NOT change (scope boundary): this does not change plugin command registration, command execution/dispatch semantics, Discord native slash command sync, or embedded/local TUI command discovery. ## Change Type (select all) - [x] Bug fix - [ ] Feature - [ ] Refactor required for the fix - [ ] Docs - [ ] Security hardening - [ ] Chore/infra ## Scope (select all touched areas) - [ ] Gateway / orchestration - [ ] Skills / tool execution - [ ] Auth / tokens - [ ] Memory / storage - [ ] Integrations - [ ] API / contracts - [x] UI / DX - [ ] CI/CD / infra ## Linked Issue/PR - Closes # - Related openclaw#78347 - Related openclaw#73984 - Related openclaw#61966 - [x] This PR fixes a bug or regression ## Real behavior proof (required for external PRs) - Behavior or issue addressed: plugin commands present in the running Gateway command registry did not appear in `openclaw tui` slash autocomplete. - Real environment tested: patched OpenClaw Docker image built from this branch (`se7en/openclaw:tui-plugin-autocomplete-test`), OpenClaw `2026.5.17`, loopback Gateway, temporary plugin command setup. - Exact steps or command run after this patch: 1. Confirm a plugin command exists in the running Gateway text command surface. Example from an existing OpenClaw setup: ```bash openclaw gateway call commands.list --params '{"scope":"text","includeArgs":false}' --json \ | jq '.commands[] | select(.name == "phone")' ``` 2. Start the patched TUI against a Gateway with a plugin command loaded. 3. Type the plugin command prefix, for example `/pho` for `/phone` or `/nemo` in the temporary NemoClaw validation setup. 4. Submit the plugin command through TUI. - Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): ```text Existing Gateway plugin command example: { "name": "phone", "textAliases": ["/phone"], "description": "Arm/disarm high-risk phone node commands (camera/screen/writes).", "source": "plugin", "scope": "both", "acceptsArgs": true } Patched Docker/TUI validation command surface contained: { "name": "nemoclaw", "nativeName": "nemoclaw", "textAliases": ["/nemoclaw"], "description": "NemoClaw sandbox management (status, eject).", "source": "plugin", "scope": "both", "acceptsArgs": true } openclaw tui autocomplete after typing /nemo showed: → nemoclaw NemoClaw sandbox management (status, eject). openclaw tui --message '/nemoclaw status' returned: NemoClaw: No operations performed yet. Run nemoclaw onboard to get started. ``` - Observed result after fix: plugin command aliases from the running Gateway appeared in TUI slash autocomplete and the command could be submitted through the TUI/Gateway path. - What was not tested: Discord native slash command registration/sync, `openclaw agent --message` plugin dispatch, and embedded/local TUI dynamic plugin discovery. - Before evidence (optional but encouraged): current installed host OpenClaw `2026.5.12` and sandbox OpenClaw `2026.4.24` TUI bundles used static `getSlashCommands(...)` autocomplete and did not call `commands.list`. ## Root Cause (if applicable) - Root cause: `src/tui/tui.ts` built its autocomplete provider from `getSlashCommands(...)`, which combined built-in/local command metadata but did not consume the running Gateway's plugin command registry. - Missing detection / guardrail: there was no TUI-side regression coverage proving `commands.list` entries are requested and merged into slash autocomplete. - Contributing context (if known): plugin commands can be registered and exposed by the Gateway independently from the static TUI command list, so a connected TUI needs to use the Gateway command surface as the source of truth for dynamic commands. ## Regression Test Plan (if applicable) - Coverage level that should have caught this: - [x] Unit test - [x] Seam / integration test - [ ] End-to-end test - [ ] Existing coverage already sufficient - Target test or file: - `src/tui/commands.test.ts` - `src/tui/gateway-chat.test.ts` - Scenario the test should lock in: dynamic command entries from the Gateway are requested through `commands.list`, merged into TUI slash commands, deduplicated, and do not replace built-in commands on name collisions. - Why this is the smallest reliable guardrail: it tests the TUI command merge behavior and the Gateway client RPC without requiring a real plugin package or a channel integration. - Existing test that already covers this (if any): none before this change. - If no new test is added, why not: N/A; focused tests were added. ## User-visible / Behavior Changes Gateway-connected `openclaw tui` can now suggest plugin-owned slash command aliases that are already exposed by the running Gateway command surface. Built-in/static suggestions remain first and keep priority on collisions. ## Diagram (if applicable) ```text Before: [user types /pho in openclaw tui] -> TUI local/static slash list only -> plugin command from Gateway commands.list is not suggested After: [user types /pho in openclaw tui] -> TUI local/static slash list -> Gateway commands.list text-scope entries -> merged autocomplete suggestions include plugin aliases such as /phone ``` ## Security Impact (required) - New permissions/capabilities? (`Yes/No`): No - Secrets/tokens handling changed? (`Yes/No`): No - New/changed network calls? (`Yes/No`): Yes - Command/tool execution surface changed? (`Yes/No`): No - Data access scope changed? (`Yes/No`): No - If any `Yes`, explain risk + mitigation: the TUI may call the existing authenticated Gateway RPC method `commands.list` over the already-established Gateway connection. This reads command metadata only; it does not execute commands or expose secrets. Failures are caught and ignored so autocomplete falls back to the existing static list. ## Repro + Verification ### Environment - OS: Linux - Runtime/container: Docker image built from patched OpenClaw source (`se7en/openclaw:tui-plugin-autocomplete-test`) - Model/provider: not relevant for autocomplete; sandbox test used `nvidia/nemotron-3-super-120b-a12b` for the NemoClaw environment - Integration/channel (if any): TUI connected to Gateway - Relevant config (redacted): loopback Gateway with temporary local plugin registration; no secrets included in evidence ### Steps 1. Install or load a plugin that registers a slash command through `api.registerCommand(...)`. 2. Confirm the running Gateway exposes it: ```bash openclaw gateway call commands.list --params '{"scope":"text","includeArgs":false}' --json ``` 3. Start `openclaw tui` against that Gateway and type the plugin command prefix. ### Expected - TUI autocomplete includes plugin command aliases from the running Gateway command surface. ### Actual - Before this patch, TUI autocomplete only included the local/static slash command list. - After this patch, plugin command aliases from `commands.list` are merged into autocomplete. ## Evidence Attach at least one: - [x] Failing test/log before + passing after - [x] Trace/log snippets - [ ] Screenshot/recording - [ ] Perf numbers (if relevant) Focused verification run after the final narrowing change: ```bash COREPACK_HOME=/tmp/corepack PNPM_HOME=/tmp/pnpm \ node scripts/run-vitest.mjs run \ src/tui/commands.test.ts \ src/tui/gateway-chat.test.ts \ src/tui/embedded-backend.test.ts # 3 files passed, 43 tests passed COREPACK_HOME=/tmp/corepack PNPM_HOME=/tmp/pnpm \ pnpm exec tsc --noEmit --pretty false --project tsconfig.core.json # passed git diff --check # passed ``` ## Human Verification (required) What you personally verified (not just CI), and how: - Verified scenarios: - Patched Docker image built from the OpenClaw branch. - Gateway `commands.list` returned a plugin command entry for `/nemoclaw` in the temporary plugin validation setup. - Patched `openclaw tui` autocomplete showed `→ nemoclaw NemoClaw sandbox management (status, eject).` - `/nemoclaw status` submitted through the patched TUI/Gateway path returned plugin output. - Focused TUI command merge/client tests passed after narrowing the code path to gateway backends. - Edge cases checked: - Dynamic plugin commands do not override built-in command names. - `commands.list` failures are soft-failed and leave the static autocomplete list intact. - Backends without `listCommands` keep the previous behavior. - What you did **not** verify: - Discord native slash command sync/ack behavior. - `openclaw agent --message` plugin command dispatch. - Embedded/local TUI dynamic plugin discovery. ## Review Conversations - [ ] I replied to or resolved every bot review conversation I addressed in this PR. - [ ] I left unresolved only the conversations that still need reviewer or maintainer judgment. N/A — no PR review conversations exist yet. ## Compatibility / Migration - Backward compatible? (`Yes/No`): Yes - Config/env changes? (`Yes/No`): No - Migration needed? (`Yes/No`): No - If yes, exact upgrade steps: N/A ## Risks and Mitigations - Risk: a Gateway may be older or may reject/fail `commands.list`. - Mitigation: the call is optional and failures are ignored; TUI keeps the existing static slash autocomplete. - Risk: plugin command names could collide with built-in commands. - Mitigation: built-in/static commands are added first and dynamic commands are deduplicated without overriding them.
|
ClawSweeper applied the proposed close for this PR.
|
…plete
Plugin commands registered via api.registerCommand() are stored in the pluginCommands map but were never surfaced in the TUI '/' autocomplete dropdown. listChatCommandsForConfig() only returns built-in commands; listPluginCommands() was only called for the /commands text response.
Add a listPluginCommands() loop in getSlashCommands() so commands like /remember and /recall from external plugins appear as hints when typing '/'.
Summary
Describe the problem and fix in 2–5 bullets:
If this PR fixes a plugin beta-release blocker, title it
fix(<plugin-id>): beta blocker - <summary>and link the matchingBeta blocker: <plugin-name> - <summary>issue labeledbeta-blocker. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write
N/A. If the cause is unclear, writeUnknown.Regression Test Plan (if applicable)
For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write
N/A.User-visible / Behavior Changes
List user-visible changes (including defaults/config).
If none, write
None.Diagram (if applicable)
For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write
N/A.Security Impact (required)
Yes/No)Yes/No)Yes/No)Yes/No)Yes/No)Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
Expected
Actual
Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes/No)Yes/No)Yes/No)Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.