Repository navigation
Slash command creates custom entity nodes inline #971
Description
Overview
The `/` slash command lets users create nodes inline (task, text, header, etc.). User-defined custom schema types (e.g. "Customer", "Invoice") are not yet available in the slash command picker. This issue adds them.
Custom schemas are created via the MCP handler and live in the database as nodes with `node_type = 'schema'` and `isCore = false`. They may optionally define a `title_template` (e.g. `"{first_name} {last_name}"`) that computes the node's display title from its properties — implemented in #824.
What needs to be built
Part 1 — Backend: `title_template` field name validation
File: `packages/core/src/behaviors/mod.rs`
Function: `SchemaNodeBehavior::validate_schema_node()`
If `title_template` is set on a schema, every `{field_name}` token in the template must match a field name defined in `schema.fields`. If any token does not match a defined field, reject the schema with a clear error message like:
`title_template references undefined field 'first_name' — add it to schema.fields`
File: `packages/core/src/mcp/handlers/schema.rs`
Functions: `handle_create_schema()` and `handle_update_schema()`
These functions call `NodeService` to create/update schema nodes. The validation above fires automatically through the existing behavior validation pipeline. Make sure any `ValidationFailed` error is returned to the MCP caller as a proper error response (not swallowed). Check how other validation errors are currently handled in this file and follow the same pattern.
Part 2 — Frontend: Add custom types to the slash command picker
Background — how the slash command works today:
The slash command picker is driven by a plugin registry. The service that assembles the list is:
- `packages/desktop-app/src/lib/services/slash-command-service.ts` — `getCommands()` returns all registered commands from `pluginRegistry.getAllSlashCommands()`
Custom schema types are not registered as plugins, so they don't appear. The simplest approach is to extend `getCommands()` to append custom schema types from the schemas store after the plugin-registered commands.
The list of custom schemas is available from:
- `packages/desktop-app/src/lib/stores/schemas.ts` — `customSchemas` derived store (already loaded, reactive)
Each custom schema needs to become a `SlashCommand` entry (see the `SlashCommand` interface in `slash-command-service.ts`):
- `id`: schema id (e.g. `"customer"`)
- `label`: schema display name (e.g. `"Customer"`)
- `nodeType`: schema id
- `icon`: use the generic circle/node icon (same as `text` node fallback)
The slash command dropdown component is at:
- `packages/desktop-app/src/lib/components/ui/slash-command-dropdown/slash-command-dropdown.svelte`
Part 3 — Frontend: Node creation behavior
When a custom type is selected from the slash command, the node is created as a child of the current node — same as any other slash command today. No special parent handling needed.
Without `title_template`: the new node behaves exactly like a task node. `content` is editable directly inline. The user types the entity name into the block. No special handling needed beyond registering the type.
With `title_template`: the new node is created with `content = "Untitled"`. The inline block must:
- Show the raw template string (e.g. `"{first_name} {last_name}"`) as a read-only placeholder when `title` is `"Untitled"` or empty
- Show the interpolated `title` value (e.g. `"John Doe"`) once properties are filled
- Keep `content` read-only — the user cannot type into this block directly
The `title` field on the node is recomputed automatically by the Rust backend (`compute_title()` in `packages/core/src/services/node_service.rs`) whenever properties are updated. The frontend just needs to display `title ?? content` and react to node updates via the existing `sharedNodeStore`.
How to detect whether a schema has a `title_template`: the `SchemaNode` type (frontend) is at `packages/desktop-app/src/lib/types/schema-node.ts`. Add the `titleTemplate?: string` field if it isn't already there, and read it from the schema when rendering the inline node.
Part 4 — Frontend: "Open" button opens node in other pane
The inline block for custom entity nodes must include the existing "open" button — the same button that appears on task nodes that opens the node in a new tab in the other pane.
Look at `packages/desktop-app/src/lib/design/components/task-node.svelte` for the pattern. This is how the user fills in properties for `title_template` types. There is no auto-open — it's always user-initiated via the open button.
Acceptance Criteria
- `validate_schema_node()` rejects a schema whose `title_template` references a field name not in `schema.fields`, with a clear error message
- MCP `create_schema` and `update_schema` return a proper error response when validation fails (not a silent success)
- Custom schema types (`isCore: false`) appear in the `/` slash command picker
- Selecting a custom type without `title_template` creates a child node with editable `content`, behaves like a task node
- Selecting a custom type with `title_template` creates a child node with `content = "Untitled"`
- Node with `title_template`: shows raw template string as read-only placeholder when title is empty/Untitled
- Node with `title_template`: shows interpolated title reactively once properties are filled via the side pane
- Node with `title_template`: `content` field is not editable inline
- Inline node has an "open" button that opens the node in a new tab in the other pane
- All tests pass (`bun run test:all`)
- Code passes `bun run quality:fix`
Technical Specifications
Key Files
Backend (Rust):
- `packages/core/src/behaviors/mod.rs` — `SchemaNodeBehavior::validate_schema_node()`: add field name cross-validation for `title_template`
- `packages/core/src/mcp/handlers/schema.rs` — `handle_create_schema()` and `handle_update_schema()`: ensure validation errors surface to MCP caller
- `packages/core/src/models/schema_node.rs` — `SchemaNode` struct (has `title_template: Option`)
- `packages/core/src/services/node_service.rs` — `compute_title()`: already handles template interpolation, no changes needed
Frontend (Svelte/TypeScript):
- `packages/desktop-app/src/lib/services/slash-command-service.ts` — extend `getCommands()` to include custom schemas
- `packages/desktop-app/src/lib/stores/schemas.ts` — `customSchemas` derived store (source of custom type list)
- `packages/desktop-app/src/lib/types/schema-node.ts` — add `titleTemplate?: string` if missing
- `packages/desktop-app/src/lib/design/components/task-node.svelte` — reference for "open" button pattern
- `packages/desktop-app/src/lib/design/components/base-node.svelte` — base node rendering
How `title_template` validation should work (Rust)
The existing syntax validation (unclosed braces, empty placeholders) is already in `validate_schema_node()` from #824. Add a second pass after it:
```rust
// After syntax validation, cross-check field names against schema.fields
if let Some(template) = &schema.title_template {
let defined_fields: HashSet<&str> = schema.fields.iter().map(|f| f.name.as_str()).collect();
// extract {field_name} tokens and check each against defined_fields
// return Err if any token is not in defined_fields
}
```
Non-Goals
- Custom icons/emojis per schema type (future — circle indicator is the placeholder for now)
- `@mention` create flow for custom types (separate issue)
- UI for creating/editing schemas (currently MCP-only, separate issue)
Related Issues
- Add title_template support for schema-driven title computation #824 (title_template implementation — prerequisite, already merged)
Activity
malibio commentedon Mar 17, 2026
Frontend baseline: 118 test files, 3722 tests passed
feat: slash command creates custom entity nodes inline (closes #971)
Slash command creates custom entity nodes inline (#972)