Composer completion — one config, one popup, one dispatch
What the person sees
In every composer — Blazor, the React portal, the React Native app (iOS, Android, Expo web, the Mac
shell) — typing / at the start of the draft opens the commands and skills available here:
/harness, /agent, /model, /effort, every skill the viewer can read for this context, and under a
CLI harness that harness's own commands (/login, /logout, …) plus /harness. Typing @ anywhere
opens nodes and agents. Each row shows its label, its kind and its description. On the web and on a Mac
↑/↓ move the highlight, Enter or Tab pick, Escape closes; on a phone a row is tapped.
Picking a command does what the command does, exactly as submitting it would:
| Command | What happens |
|---|---|
/harness, /agent, /model |
The picker opens in the composer; the pick rides the next send. |
/effort |
The value picker opens over the choices the /effort skill node declares; the pick is patched onto the open thread's composer (composer.effort) or seeded on the thread the next send starts. |
/login, /logout (CLI harness) |
The harness's sign-in page opens (HarnessLogin, or the device mesh's Claude Code page); the composer says so, or says the harness has no such page here. Never forwarded to the headless CLI. |
| an instruction skill | Picked bare, the draft keeps /name and shows the skill's description; sent with a task, the task is submitted with the load_skill directive (SkillInfo.ToSubmissionText). |
| an OpenContent / Navigate skill | The declared node (or the typed target) opens. |
a NewThread skill (/clear) |
The composer starts a fresh conversation. |
| any other command under a CLI harness | Forwarded to the CLI as the message. |
| a word no skill answers | Sent as a plain message, as before. |
One declaration
src/MeshWeaver.AI/Completion/ComposerCompletion.cs declares the configuration:
- the trigger characters (
@,/), the debounce (250 ms) and the popup's row limit (8); - the slash query grammar —
/word, or/word harness:<id>— because anAutocompleteRequestcarries only a query and a context, and the harness decides the list; - the
ComposerCommandActionvocabulary every composer dispatches on, withActionOf(SkillAction)andActionOf(HarnessCommandKind)deriving it from the declaration that owns a command; BuiltInActions, the commands every composer knows by name.ComposerCompletionTestholds it to the built-in skill nodes (Data/Skill/{harness,agent,model,effort}.md) and the harnesses' auth commands;HarnessCommandItems, the CLI harness list.
Blazor reads the class directly: ThreadChatView.GetCompletionConfig takes its trigger characters
and BuildHarnessCommandCompletions its harness list from it.
The clients are served the list, never a copy of it. ComposerCommandAutocompleteProvider is
registered on every node hub, so the AutocompleteRequest the clients already send to the viewer's
own hub for @ answers a /word query too, from the same SkillAutocompleteProvider catalogue Blazor
lists (each row keeping its skill node path) or, under a CLI harness, from HarnessCommandItems. For
any other query it answers nothing, so it adds nothing to an @ answer. A client keeps only the rows in
the Commands category.
The client mirror is drift-checked. clients/react/src/controls/composerCompletion.ts mirrors the
scalar config (so the client can debounce and parse without a round trip), the action vocabulary, the
built-in map and the instruction-skill submission text. composerCompletion.test.ts reads the C# source
and fails when the two disagree. A server whose AI module predates the provider answers a / query with
no command row at all; the client then offers only the built-in commands, never a second catalogue.
One popup, one model, one dispatch
| Piece | Where | What it owns |
|---|---|---|
useComposerCompletion |
clients/react/src/controls/composerModel.ts |
token tracking (/word at the start, @token anywhere), the debounced, generation-guarded fetch, the highlight, the keys, splice-on-pick |
ComposerPopup |
clients/react/src/controls/composerPopup.tsx |
rows (label · kind · description), highlight, selected row, loading / failed / empty states, accessibility roles. It imports nothing platform-specific: the web leaf passes DOM primitives (composerPopupDom.tsx), the native leaf View/Pressable/Text (app/react-native/src/rnComposerPopup.tsx) |
resolveComposerCommand / runComposerCommand |
clients/react/src/controls/composerCompletion.ts |
what a command does — resolved like Blazor's dispatch (CLI harness first, then the skill node read from the picked row's path or found through the server's list) and run through the leaf's handlers |
| the harness registry | clients/react/src/controls/composerHarness.ts |
the /harness picker's options, now shared by the web and native composers |
The pickers a command opens are the same ComposerPopup over the option catalogue. The RN parity suite
(app/react-native/src/parity.test.ts) fails if either leaf stops using the shared popup, model or
dispatch.
What is not covered here
- The CLI's runtime command list (what a live Claude Code session reports in its init message) exists only inside a Blazor session; the clients list the harness's declared commands.
- Signing a CLI harness out uses the same HarnessLogin page that signs it in (its Sign out).
- Blazor's in-composer credential paste (
/login <key>) stays Blazor's; the clients open the sign-in page.
Tests
src/MeshWeaver.AI.Test/ComposerCompletionTest.cs— the query grammar, the harness list, the word filter, the action map, the built-in map against the skill nodes and the harness commands, and the provider's gating.clients/react/src/controls/composerCompletion.test.ts— the drift guard against the C# source, the resolution and every dispatch path.clients/react/src/controls/composerModel.test.tsx— the model (both triggers, the fallback, the generation guard, the keys) and the popup.clients/react/src/controls/threadChat.test.tsxandapp/react-native/src/rnComposer.commands.test.tsx— each leaf end to end: the list, a picked/model,/effortpatched or seeded, an instruction skill,/loginunder Claude Code.