Skip to content

Workstation self-introspection: structured UI map + action tools for Claude #64

@smorchj

Description

@smorchj

Sibling to #62 and #63. The third leg of the self-hosting story.

#62 gives Claude a headless browser. #63 gives the user a preview pane. This issue gives Claude a structured map of the Workstation UI itself, so when Claude is running inside Klonode and a user asks "open the GraphView and select the auth folder," Claude doesn't have to take a screenshot and guess pixel coordinates — it queries a real component registry and calls a real action tool.

What

A self-introspection layer the Workstation publishes about itself, plus the tools to query and act on it:

1. Component registry

Every interactive Svelte component in packages/ui/src/lib/components/ gets:

  • A stable component ID (e.g. chat-panel, tree-view, graph-view, github-view, layer-inspector, editor-context, editor-toolbar)
  • A role description — one sentence on what the component does
  • A list of child components / slots it contains
  • A list of actions it exposes (e.g. chat-panel.send-message, tree-view.expand-node, graph-view.select-node, editor-context.start-edit)
  • A list of observable state keys (e.g. chat-panel.current-session-id, tree-view.expanded-paths, graph-view.selected-node-id)

The registry lives in code, not docs — generated from a defineComponent({...}) helper that each component calls once at module load. That guarantees the registry stays in sync with the actual UI.

2. State snapshot tool: klonode_workstation_snapshot

Returns a structured JSON snapshot of the current Workstation state:

{
  "active_pane": "graph-view",
  "panes_open": ["tree-view", "graph-view", "chat-panel", "editor-context"],
  "selected_node": { "id": "node_42", "path": "packages/ui/src/lib/auth", "layer": 2 },
  "active_chat_session": { "id": "session_xyz", "agent": "co", "messages": 8 },
  "layout": "split-vertical",
  "viewport": { "width": 1920, "height": 1080 }
}

This is a first-class tool Claude can call from any spawned session. No screenshots, no regex parsing.

3. Action tools: klonode_workstation_action

A single dispatch tool that takes a component ID and an action name:

{ "component": "tree-view", "action": "expand-node", "args": { "path": "packages/ui/src/lib/auth" } }
{ "component": "graph-view", "action": "select-node", "args": { "node_id": "node_42" } }
{ "component": "chat-panel", "action": "send-message", "args": { "session_id": "session_xyz", "text": "summarize this folder" } }
{ "component": "editor-context", "action": "start-edit", "args": {} }

Each registered action validates its args via the registry and routes to the actual Svelte store update or method call.

4. Discovery tool: klonode_workstation_describe

Returns the full registry of components, their actions, and their observable state — so Claude can ask "what can I do in this UI?" without prior knowledge. This is what read_page is for an HTML page, but for Klonode's own component tree.

Why this matters

Implementation sketch

Component definition

// packages/ui/src/lib/components/TreeView/TreeView.svelte
<script lang="ts">
  import { defineComponent } from '$lib/workstation/registry';

  defineComponent({
    id: 'tree-view',
    role: 'Sidebar tree of the project file structure with expand/collapse and selection',
    parent: 'workstation-layout',
    actions: {
      'expand-node': { args: { path: 'string' } },
      'collapse-node': { args: { path: 'string' } },
      'select-node':  { args: { node_id: 'string' } },
      'reveal-path':  { args: { path: 'string' } },
    },
    state: ['expanded-paths', 'selected-node-id'],
  });
</script>

Registry storage

A single Svelte store under packages/ui/src/lib/workstation/registry.ts collects every defineComponent() call at boot. The store is the source of truth that the snapshot, action, and describe tools all query.

Tool wiring

The three tools (snapshot, action, describe) are exposed as a small MCP server bundled with the Workstation backend. When Klonode spawns a Claude session (per #62's plumbing), this MCP gets attached automatically.

Action handlers

Each action is implemented as a function the component registers alongside its defineComponent() declaration:

defineComponentAction('tree-view', 'select-node', ({ node_id }) => {
  selectedNodeStore.set(node_id);
  return { ok: true, selected: node_id };
});

Files

  • New: packages/ui/src/lib/workstation/registry.ts — central registry store + defineComponent / defineComponentAction helpers
  • New: packages/ui/src/lib/workstation/mcp-server.ts — the MCP server that exposes the three tools
  • New: packages/ui/src/routes/api/workstation/snapshot/+server.ts — HTTP endpoint behind the MCP
  • New: packages/ui/src/routes/api/workstation/action/+server.ts — HTTP endpoint behind the MCP
  • Modified: every interactive component under packages/ui/src/lib/components/ to call defineComponent once
  • Modified: chat session spawn path (per Wire Playwright MCP into Klonode-spawned Claude CLI sessions #62) to attach the workstation MCP

Acceptance criteria

  • klonode_workstation_describe returns at least 8 registered components (chat-panel, tree-view, graph-view, github-view, editor-context, layer-inspector, heatmap-overlay, settings)
  • klonode_workstation_snapshot returns the current selected node, active pane, and active chat session id without taking a screenshot
  • klonode_workstation_action with { component: 'tree-view', action: 'select-node', args: { node_id: <real_id> } } actually selects the node in the UI (verified by a follow-up snapshot)
  • Adding a new interactive component requires calling defineComponent — there's a CI check (or at minimum a runtime warning) for components that have UI but no registry entry
  • Documented in CONTEXT.md for packages/ui/src/lib/workstation/

Risks / open questions

  • Action surface size. Don't try to expose every internal store mutation — only user-facing actions that make sense for an external agent to perform. Start with the obvious ones (select node, switch tab, send message, expand/collapse, scroll-to) and grow from real Claude usage logs.
  • State snapshot freshness. The snapshot has to reflect the current Svelte store state when the tool is called, not a stale cache. The MCP server reads stores directly via Svelte's get() so this is fine — but make sure the test suite verifies it.
  • Multi-window / multi-session. If the user has two Workstation windows open, which one does the snapshot describe? Probably scope by the WebSocket session ID the spawned Claude is bound to.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions