command_surface_interaction_spec - mark-ik/graphshell GitHub Wiki
Date: 2026-02-28
Status: Canonical interaction contract
Priority: Pre-renderer/WGPU required
Related:
../2026-02-28_ux_contract_register.md../shell/2026-04-03_shell_command_bar_execution_plan.md../workbench/workbench_frame_tile_interaction_spec.md../canvas/graph_node_edge_interaction_spec.md../subsystem_ux_semantics/2026-03-04_model_boundary_control_matrix.md../aspect_control/2026-02-24_control_ui_ux_plan.md../research/2026-02-24_interaction_and_semantic_design_schemes.md../../design/KEYBINDINGS.md
Adopted standards (see standards report §§3.5, 3.6):
- WCAG 2.2 Level AA — SC 2.1.1 (keyboard, all command surfaces), SC 2.4.3 (focus order/return after dismiss), SC 1.4.3 (disabled-state explanation legibility)
- OpenTelemetry Semantic Conventions — diagnostics for failed dispatch, missing context, blocked execution
-
GraphId= truth boundary. -
GraphViewId= scoped view state. - graph-scoped Navigator hosts = chrome surfaces that may host command entry points while remaining above workbench arrangement semantics.
-
Navigator= graph-backed hierarchical projection over relation families. Legacy alias: "file tree". - workbench = arrangement boundary.
Command dispatch may target these boundaries, but command surfaces do not redefine their ownership.
Normative command contracts use: intent, trigger, preconditions, semantic result, focus result, visual result, degradation result, owner, verification.
- Tile/frame arrangement is not content hierarchy.
- Navigator is not content truth authority.
- Physics presets are not camera modes.
This spec defines the interaction contract for Graphshell's command surfaces.
Command surfaces may be launched from graph-scoped Navigator chrome, workbench-scoped Navigator context, or direct pointer/keyboard invocation, but those launch locations do not become semantic owners of the graph/workbench targets they address.
It explains:
- what command surfaces are for,
- what each command-entry surface means semantically,
- who owns command meaning and availability,
- what state transitions command invocation implies,
- what visual feedback must accompany command use,
- what fallback behavior must happen when command execution is blocked,
- which command surfaces are core, planned, and exploratory.
This spec covers command invocation surfaces, not graph or workbench semantics themselves.
Graphshell has three first-class command-entry families:
-
Keyboard Commands
- direct keybinding path for known actions
-
Command surfaces — two:
- Command Palette (Modal overlay; flat ranked list; fuzzy-search input is the discovery mechanism; Zed/VSCode-shaped)
- Context Menu (right-click on an interactable target; flat list of available actions for that target)
-
Omnibar-Initiated Commands
- command execution from the search/navigation field
Both command surfaces source actions from the same ActionRegistry; both
dispatch via the same ActionId execution path. They differ only in
trigger and rendering.
Retired (2026-04-29 simplification):
- Search Palette Mode + Context Palette Mode distinction — collapsed into the single Command Palette. Search is the palette's input affordance, not a separate mode. Right-click is the Context Menu, not a contextual mode of the palette.
- Two-tier (Tier 1 categories + Tier 2 options) rendering — replaced by flat ranked lists in both surfaces. Categories appear only as inline badges on rows for breadth visibility, never as a separate selector tier. See §3.3.
- Cross-mode equivalence rule (Tier 1 strip = Tier 1 ring) — moot; there is no Tier 1.
-
Radial Palette Mode — deferred indefinitely. Was originally
gamepad-oriented; if gamepad input lands later as part of the
input-subsystem rework, a radial surface can be reintroduced as a
third command-dispatch route with its own design pass. The geometry
research in
radial_menu_geometry_and_overflow_spec.mdis preserved as a future reference. See §5 Planned Extensions.
- Servo's native webview context menu may still exist as an embedder surface, but it is not a Graphshell command authority.
- The earlier retirement of "Context Menu" as a Graphshell concept (in favor of "Context Palette") is itself reversed by the 2026-04-29 simplification: Context Menu is the canonical name again, since it is no longer a "mode" of the palette but a separate surface with its own trigger and rendering.
To avoid ambiguity in implementation and UI copy:
-
Command Palette = the Modal flat-list surface invoked by
Ctrl+P,F2, or the CommandBar trigger button. Fuzzy-search is its discovery mechanism. - Context Menu = the right-click contextual flat-list surface scoped to the right-click target.
- Radial Menu = deferred (see §5).
- "Context Palette" is retired as a name — it is now just the Context Menu.
- "Search Palette Mode" / "Context Palette Mode" / "Radial Palette Mode" are all retired names; do not reintroduce them in new code or docs.
Interaction Menu should not be used as a canonical name; it blurs
distinctions that this revision tightens.
- Graphshell owns command meaning, targeting, availability, and execution.
-
ActionRegistryis the semantic command authority. - UI surfaces may render and collect input, but they must not define separate command behavior.
Command-surface interactions fall into five semantic categories:
-
Discover
- reveal available actions
-
Filter
- narrow visible actions by context or search
-
Target
- resolve which object, pane, or graph context the action applies to
-
Invoke
- execute the selected action
-
Dismiss
- close the surface without mutating app state
The command system must make these user expectations reliable:
- the same action means the same thing on every command surface,
- contextual filtering changes visibility, not semantics,
- disabled actions are visible and explained rather than silently hidden,
- command failure is explicit rather than silent,
- input mode affects presentation, not command authority,
- command applicability is determined from the full resolved selection set, not by inventing a hidden primary target.
Both command surfaces (Command Palette + Context Menu) render a flat ranked list of available actions. There is no Tier 1 / Tier 2 selector tier; categories appear only as inline badges on rows for breadth visibility.
Common contract:
- Action data sourced from
ActionRegistry. - Disabled actions render with explicit reasons (per §4.1); never hidden silently.
- Verb-target wording per §3.4.
- Selection-set availability gating per §4.1.
- Dispatch via single
ActionIdexecution path. - Pinned actions and recency ordering supported (palette only; context menu is small and contextual, no pinning).
Surface-specific differences:
| Command Palette | Context Menu | |
|---|---|---|
| Trigger |
Ctrl+P / F2 / trigger button / programmatic |
Right-click on interactable target |
| Rendering |
Modal overlay with text_input filter + scrollable flat list |
Anchored ContextMenu flat list |
| Scope | global (default) or scoped via context-fallback origin | scoped to right-click target |
| Filter | fuzzy search; empty-query shows pinned + recent + canonical default | no filter; flat available list for the target |
| Mode-switch | "Search commands…" footer entry in Context Menu opens the Palette pre-scoped to the target | none (Palette doesn't switch back) |
| Resize | Modal max-width + max-height; not user-resizable (Stage F polish question) | Not resizable; sized to content |
| Dismiss | Escape, click outside, action selected | Escape, click outside, action selected |
Right-click on any interactable target (graph node, edge, tile, Pane chrome, Frame border, Navigator row, swatch, base layer) opens the Context Menu scoped to that target. Right-click never opens the Command Palette directly; the Context Menu's "Search commands…" fallback is the keyboard-driven escape into the full action set.
Command labels must follow explicit Verb + Target (+ Destination/Scope when needed) grammar.
Canonical wording rules:
- Use explicit target nouns for destructive verbs:
-
Delete Selected Node(s)(graph content mutation) - avoid targetless
Deletefor command labels.
-
- Reserve
Closefor UI presentation containers (tile/frame/window/dialog), not graph content deletion. - For destination-dependent verbs (
Open,Move), include destination semantics:Open Node in SplitOpen via Frame RouteMove Node to Active Pane
- Disabled action explanations must state unmet precondition and satisfy guidance.
What this domain is for: - Keep command meaning unified across all command-entry surfaces.
Core rule: - Every command surface must route through the same Graphshell command authority.
-
ActionRegistry::list_actions_for_context(...)defines what the user can see. -
ActionRegistry::execute(...)defines what the user actually runs. - The resolved command target is the current selection set when one exists.
- The selection set may be mixed (
Node,Tile,Frame,Edge, or other interactable graph/workbench objects). - A command is available only if it validly applies to every object in the resolved selection set.
- When invoked, the command applies to all selected objects.
- Silent fallback to a hidden primary target is forbidden.
Who owns it: - Graphshell command dispatcher and action registry.
- UI surfaces are render and input adapters only.
State transitions: - Command invocation may mutate graph state, workbench state, selection state, or settings state according to the action's semantic definition.
- Opening a command surface does not mutate semantic state by itself.
Visual feedback: - The active command surface must be visibly distinct.
- Target context and disabled-state explanation must be legible.
Fallback / degraded behavior: - If a command cannot execute, Graphshell must provide a blocked-state reason.
- Silent command no-op behavior is forbidden.
What this domain is for: - Provide the canonical searchable list of actions in a single Modal surface.
Core controls: - Ctrl+P (canonical, Zed/VSCode-shaped) opens the palette.
-
F2(alternate) toggles the same palette. - The CommandBar trigger button opens the palette.
- Arrow keys move focus within the result list.
- Enter dispatches the focused action.
- Escape dismisses and must not trigger unrelated global mode toggles on the same press.
Who owns it: - Graphshell command system owns the action set, availability rules, and ranking.
- The palette UI owns rendering, focus movement within the list, and search text capture.
State transitions: - Opening the palette enters a command-browse state with the input focused.
- Empty query shows pinned + recently-used + canonical-default available actions.
- Non-empty query shows fuzzy-match ranked results.
- Choosing an action dispatches the selected
ActionIdagainst the current selection set. - Dismissing the palette returns focus to the prior region.
Visual feedback: - Render a flat ranked list (no category tier).
- Each row shows: action label (verb-target wording), optional secondary text, optional inline category badge, optional right-aligned keybinding.
- Disabled actions remain visible with reduced opacity and explicit disabled-reason text in the footer when focused.
- Empty result set shows an explicit empty state ("No commands match…").
Fallback / degraded behavior: - If no actions are available in the current scope, show an explicit empty state.
- If a Context Menu's "Search commands…" fallback opens the palette, scope is preset to the right-click target.
What this domain is for: - Provide a right-click-anchored flat list of actions available on the right-click target.
Core controls: - Right-click on any interactable target opens the Context Menu scoped to that target.
- Arrow keys move focus.
- Enter dispatches.
- Escape dismisses.
Who owns it: - Graphshell command system owns target-scoped action availability.
- The Context Menu UI owns rendering and dismissal.
State transitions: - Opening the Context Menu enters a command-browse state focused on the menu.
- Choosing an action dispatches the
ActionIdagainst the right-click target's selection set. - "Search commands…" footer entry opens the Command Palette with
PaletteOrigin::ContextFallback, preserving the right-click target as scope. - Dismissing the menu returns focus to the prior region.
Visual feedback: - Render a flat list of actions; no category tier.
- Each row shows: action label (verb-target wording), optional right-aligned keybinding.
- Disabled actions render with reduced opacity and explicit reason on hover/focus.
- Footer separator + "Search commands…" entry as the keyboard escape into the full action set.
Fallback / degraded behavior: - If no actions are available for the right-click target, show only the "Search commands…" footer.
- If a destructive action is selected, route through
ConfirmDialogper the host spec; do not dispatch silently.
What this domain is for: - Provide the fastest path for known actions.
Core controls: - Keybindings invoke semantic app actions, not widget-local shortcuts with divergent meaning.
- Keyboard commands target the active semantic context.
Who owns it: - Graphshell input and command layers own binding-to-action resolution.
- Widgets may capture text input where appropriate, but must not redefine global command semantics.
State transitions: - Valid keybinding resolution dispatches a semantic action.
- Conflicting text-entry contexts may defer a command when the focused surface owns text input.
Visual feedback: - Global command surfaces should expose shortcut hints.
- Command execution should visibly affect the target surface or emit explicit blocked-state feedback.
Fallback / degraded behavior: - If a command is unavailable in the current context, the user must receive an explicit explanation.
- Hidden suppression is forbidden.
What this domain is for: - Provide URL/address entry and Navigator-projected breadcrumb display in a single chrome surface. Per the 2026-04-29 simplification, the omnibar is not a command-entry surface; commands go through the Command Palette (§4.2). Graph search by title/tag is not an omnibar role; it goes through the Node Finder (a separate Modal surface, Ctrl+P canonical). The omnibar's submission semantics are URL/address-shaped only.
Core controls: - Ctrl+L (canonical) focuses the omnibar in Input mode for URL entry.
- Submission (Enter) opens-or-activates a node by canonical address; if the typed text doesn't resolve as an address, omnibar parsing routes the entry to either the Node Finder (for title/tag-shaped queries) or to a default-search behavior per user preference.
- The omnibar exposes URL completions sourced from history-by-URL and bookmark-URLs providers.
Who owns it: - The omnibar UI is Shell-owned; URL completions are sourced from history and bookmark providers under ControlPanel supervision.
- The Navigator owns the breadcrumb projection (read-only at this seam) per SHELL.md §6.
State transitions: - Display mode shows the breadcrumb (current focused-node address path from Navigator).
- Input mode shows a
text_inputwith URL-shaped completion list. - Submission resolves the entered text via address parsing → opens-or-activates the resulting node, or routes the user to the Node Finder if the entry is non-URL-shaped.
- Dismissing input returns the omnibar to Display mode without mutation.
Canonical parity contract (normative): - The omnibar does not invoke commands. Command invocation is the Command Palette's responsibility (§4.2). If an action exists for "open node by URL," that action is dispatched through the Palette; the omnibar does not have a parallel command-row path.
- Address resolution is deterministic: a URL/address in the typed text always wins over a fuzzy-match interpretation. Ambiguous text routes to the Node Finder, not silently retargeted.
- The omnibar does not have its own
ActionIdsemantic surface; it has one submission verb ("open by address").
Visual feedback: - Display mode shows the breadcrumb token chain (Navigator-projected).
- Input mode shows the text-input cursor + completion list.
- URL completions render with source badges (history / bookmark).
- Failed address resolution (404, invalid URL, unreachable) surfaces via toast plus an Activity Log entry.
Focus ownership and identification (normative): - Omnibar text field must expose explicit focus ownership state even when caret rendering is unavailable (focus ring, field highlight, focus badge, or equivalent deterministic indicator).
- Focus must be applied to the omnibar only through explicit user selection actions (pointer selection,
Ctrl+L/platform equivalent, or explicitOmnibarFocuscommand intent). - On app/frame open, command-surface open, and Context Menu summon paths, focus must not default to omnibar.
- Keyboard command handling must remain owned by the currently focused semantic region unless omnibar focus has been explicitly requested.
Fallback / degraded behavior: - Ambiguous queries route to the Node Finder; the user is not silently retargeted.
- If a typed URL does not resolve, the user remains in Input mode with an explicit error state (recoverable, no mutation).
What this domain is for: - Provide a fuzzy graph-node search surface for "open this node by title / tag / address / content" intents, separate from the omnibar (which is URL-shaped) and the Command Palette (which is action-shaped).
Core controls: - Ctrl+P (canonical, Zed/VSCode-shaped) opens a Modal surface with a text input and a fuzzy-ranked list of graph nodes.
- Enter activates the focused node; arrow keys navigate; Escape dismisses.
- Activation routes the selected node to its destination Pane per a user-configurable rule (active Pane / new Pane / replace focused Pane).
Who owns it: - The Node Finder UI is Shell-owned; ranking is owned by graphshell-runtime's graph index.
- The result list is a derivation over current graph truth; no node-finder state aliases graph state.
State transitions: - Opening enters a graph-search-browse state with the input focused.
- Empty query shows recently-active nodes ranked by recency.
- Non-empty query shows fuzzy-match results across (title, tag, address, content snapshot).
- Activation dispatches a
WorkbenchIntent::OpenNode { node_key, destination }. - Dismissal restores focus to the prior region.
Visual feedback: - Each result row shows: node title (or address if no title), node-type badge (Web / File / Tool / Internal), match-source badge (Title / Tag / URL / Content), optional content-match snippet.
- Empty result set shows explicit empty state.
Fallback / degraded behavior: - If the graph index is unavailable, show explicit "Index unavailable" empty state; do not silently fall back to URL-only matching.
- A "Open as URL…" footer entry opens the omnibar with the typed text pre-filled, when the user wants to interpret their query as a URL after all.
What this domain is for: - Keep command surfaces understandable, inspectable, and usable across input modes.
Accessibility: - Every command surface must be dismissible without pointer input.
- Focus return after dismissal must be deterministic.
- Command surfaces must expose actionable labels and disabled-state reasons.
Diagnostics: - Failed command dispatch, missing context, and blocked execution must be observable.
- Surface divergence is a correctness bug and should be diagnosable.
Boundary rule: - Native embedder menus may exist, but they do not define Graphshell command semantics.
- richer command ranking by recency and context relevance,
- action previews before execution for high-impact commands,
- command aliases and user-defined shortcuts,
- per-domain Command settings pages: pinned-action order, command aliases, keybinding customization — exposed via the Keybindings and General settings categories in
aspect_control/settings_and_control_surfaces_spec.md §4.2, -
Radial Menu reintroduction — deferred from canonical surfaces in the 2026-04-29 simplification; if gamepad input lands as part of the input-subsystem rework, a third command-dispatch surface (radial geometry per
radial_menu_geometry_and_overflow_spec.md) can be reintroduced with its own design pass. Reintroduction would source the sameActionRegistryaction set; only the rendering and gesture model differ.
- voice-triggered command invocation,
- macro and multi-step command sequences,
- mod-provided command bundles with scoped enablement,
- AI-assisted command suggestions that still route through
ActionRegistry.
- Command meaning is unified across keyboard, Command Palette, Context Menu, and omnibar surfaces.
- Command Palette and Context Menu are two separate surfaces sourcing actions from the same
ActionRegistry; both render flat ranked lists. - The Command Palette's fuzzy-search input is the discovery mechanism; there is no separate Search Mode.
- Right-click on any interactable target opens the Context Menu scoped to that target; the "Search commands…" footer entry is the keyboard escape into the full action set.
- Disabled actions remain visible and explain why they are unavailable on both surfaces.
- Dismissal and focus return are deterministic on both surfaces.
- Blocked command execution is explicit and diagnosable.
- Destructive actions route through
ConfirmDialogon both surfaces; dispatch never happens silently. - Omnibar command rows execute the same
ActionIdsemantics and target-scope resolution as keyboard / Command Palette / Context Menu surfaces. - Omnibar and search fields do not capture keyboard commands by default; focus is explicit and visibly identifiable.
- Radial Menu, two-tier rendering, and Search/Context palette mode distinction do not appear in canonical surfaces (they are retired per §2.1 / §3.3). Reintroduction requires an explicit design pass per §5.