ADR-013: LLM Canvas — Artifact Projection & Bidirectional Workspace
Status
Proposed (Revised — Implementation Plan Updated)
Note on ADR-017 overlap: ADR-017 (Unified Mapping Experience) Phase 3 delivers the minimum ADR-013 infrastructure: ArtifactDescriptor, ARTIFACT_REGISTRY, artifactStack store, renderArtifact()/dismissArtifact(), and ArtifactHost. It also registers
mapping-editorandconfig-diffas artifact types. This ADR's revised plan starts AFTER ADR-017 Phase 3, building on that foundation rather than duplicating it.
Context
Problem Statement
Conductor's GUI v2 has a three-zone layout: Chat Panel (left), Workspace (center), and Event Stream (right). The chat interface (ADR-007) enables natural language configuration with a Plan/Apply workflow — the LLM proposes changes, a diff view renders in the workspace, and the user approves or rejects. MIDI Learn follows a similar pattern: the LLM triggers learn mode, a refinement card appears in the workspace, and the user's edits feed back into the next LLM turn.
These are both ad-hoc canvas patterns — the LLM projects structured, interactive content into the workspace, and user interactions on that content feed back into the conversation. But each pattern is bespoke: showConfigDiff() handles config diffs, showMidiLearnRefinement() handles learn events, and the workspace view enum (WORKSPACE_VIEWS) hard-codes every possible content type. Adding a new kind of LLM-driven workspace content requires:
- A new workspace view constant
- A new Svelte component
- A new store property for pending state
- Custom wiring in the chat store's agentic loop
- A new
show*()function in the workspace store
This doesn't scale. As the LLM gains capabilities — generating velocity curves, visualizing mapping topologies, rendering device templates, previewing action sequences — each would need its own plumbing. The workspace should be a general-purpose projection surface for LLM-generated artifacts, not a fixed set of hard-coded views.
Existing Canvas-Adjacent Patterns
Three patterns in the current codebase already behave like canvas projections:
| Pattern | Trigger | Workspace View | User Interaction | Feedback Loop |
|---|---|---|---|---|
| Config Diff (Plan/Apply) | LLM calls propose_config_change tool | CONFIG_DIFF → ConfigDiffView.svelte | Apply / Reject / Edit | Decision injected as TOOL_RESULT, agentic loop resumes |
| MIDI Learn Refinement | LLM calls start_midi_learn → user plays controller | MIDI_LEARN → MidiLearnDialog.svelte | Select interpretation, adjust params | Refinement result sent as tool result, LLM creates mapping plan |
| Raw Config Editor | User navigates to view | RAW_CONFIG → RawConfigView.svelte | Direct TOML editing | Saved via Tauri command, no LLM loop |
The first two follow the same meta-pattern: LLM emits structured content → workspace renders it interactively → user actions feed back as tool results → agentic loop continues. The third is a non-LLM view that demonstrates the workspace can host rich editors.
Industry Patterns Reviewed
| Application | Canvas Approach | Key Insight |
|---|---|---|
| Claude Artifacts | render_artifact tool emits code/markdown/SVG; displayed in side panel; user can fork/edit | Tool-driven projection with explicit artifact types — no generic block system |
| ChatGPT Canvas | Full document editor replaces chat; GPT can read edits back; supports code + prose | Bidirectional editing where user changes become LLM context |
| Cursor Composer | LLM proposes file changes in diff view; user accepts hunks selectively | Diff-as-artifact pattern — very close to Conductor's existing Plan/Apply |
| Reaper/ReaLearn | No canvas — all config through traditional UI forms | Validates that domain-specific views beat generic editors for MIDI mapping |
| VS Code Copilot Chat | Inline diff suggestions in editor; /edit command for targeted changes | Contextual actions attached to existing content, not a separate canvas |
Design Principles
-
Generalize, don't genericize. The goal is a unified projection mechanism, not a generic block editor. Each artifact type has its own purpose-built Svelte component — the framework provides routing, lifecycle, and feedback plumbing.
-
LLM-driven, user-refined. The LLM decides what to project and when. The user interacts with the projection and their actions become context for the next LLM turn. Neither side has sole control.
-
Tool result is the feedback channel. User interactions on artifacts are encoded as tool results and injected into the conversation, exactly as Plan/Apply already works. No new feedback mechanism needed.
-
Existing views are first-class artifacts. ConfigDiff and MidiLearnRefinement become artifact types registered in the component registry. Existing code is preserved, not rewritten.
-
Progressive disclosure. Start with structured data artifacts (tables, curves, topology maps). Add editable artifacts later. Contextual actions last. Each phase delivers standalone value.
Decision
D1: Artifact Descriptor Format
The LLM projects content into the workspace by returning an artifact descriptor from a tool call. The descriptor is a typed JSON object:
interface ArtifactDescriptor {
/** Unique artifact ID (for updates and references) */
id: string;
/** Artifact type — determines which component renders it */
type: ArtifactType;
/** Human-readable title shown in workspace header */
title: string;
/** Type-specific payload — schema varies by artifact type */
data: Record<string, unknown>;
/** Optional: actions the user can take on this artifact */
actions?: ArtifactAction[];
/** Optional: whether user edits should be tracked and fed back */
editable?: boolean;
}
type ArtifactType =
| 'config-diff' // Existing: TOML diff with apply/reject
| 'midi-learn' // Existing: Learn refinement card
| 'mapping-table' // New: Tabular mapping overview with sorting/filtering
| 'velocity-curve' // New: Interactive curve editor (linear/log/exp/LUT)
| 'device-topology' // New: Visual device → mode → mapping graph
| 'event-inspector' // New: Detailed event analysis with history
| 'action-sequence' // New: Step-by-step action preview with timing
| 'raw-config' // Existing: TOML editor (non-LLM artifact, user-initiated)
| 'markdown' // New: Rich text display for explanations/guides
| 'custom'; // Future: Plugin-defined artifact types
interface ArtifactAction {
/** Action identifier */
id: string;
/** Display label */
label: string;
/** Visual style */
variant: 'primary' | 'secondary' | 'danger';
/** Tooltip description */
description?: string;
}
When the LLM's tool call returns an artifact descriptor, the agentic loop in chat.js routes it to the workspace instead of displaying it inline in chat. The workspace store receives the descriptor, looks up the component in the registry, and renders it.
D2: Component Registry
A registry maps artifact types to Svelte components. This is the extension point — adding a new artifact type means adding one component and one registry entry:
// lib/artifacts/registry.js
import ConfigDiffView from '$lib/components/ConfigDiffView.svelte';
import MidiLearnDialog from '$lib/components/MidiLearnDialog.svelte';
import MappingTableArtifact from '$lib/artifacts/MappingTableArtifact.svelte';
import VelocityCurveArtifact from '$lib/artifacts/VelocityCurveArtifact.svelte';
import DeviceTopologyArtifact from '$lib/artifacts/DeviceTopologyArtifact.svelte';
import MarkdownArtifact from '$lib/artifacts/MarkdownArtifact.svelte';
export const ARTIFACT_REGISTRY = {
'config-diff': ConfigDiffView,
'midi-learn': MidiLearnDialog,
'mapping-table': MappingTableArtifact,
'velocity-curve': VelocityCurveArtifact,
'device-topology': DeviceTopologyArtifact,
'markdown': MarkdownArtifact,
// Future types registered here
};
The workspace host component uses Svelte's <svelte:component this={...}> for dynamic rendering:
<script>
import { ARTIFACT_REGISTRY } from '$lib/artifacts/registry.js';
import { activeArtifact } from '$lib/stores/workspace.js';
$: component = $activeArtifact
? ARTIFACT_REGISTRY[$activeArtifact.type]
: null;
</script>
{#if component}
<svelte:component
this={component}
artifact={$activeArtifact}
on:action={handleArtifactAction}
on:edit={handleArtifactEdit}
/>
{:else}
<!-- Default workspace view (mappings table) -->
<MappingsView />
{/if}
D3: ArtifactRender Tool Risk Tier
A new tool risk tier is added between ReadOnly and ConfigChange:
pub enum ToolRiskTier {
ReadOnly,
/// Display-only artifacts in the workspace — no state mutation
ArtifactRender,
Stateful,
ConfigChange,
HardwareIO,
Privileged,
}
Rationale: Rendering an artifact in the workspace is more impactful than a read (it changes what the user sees and interrupts their current view) but less risky than a state change (no data is modified). The LLM can project artifacts without user confirmation, but:
- A subtle workspace transition animation signals that the LLM has pushed content
- The user can dismiss any artifact to return to their previous view
- Artifact render events are logged in the audit trail (ADR-007 Phase 4)
- Rate limiting prevents the LLM from rapidly cycling workspace content
The render_artifact tool definition:
impl AgentTool {
// Existing tools...
RenderArtifact {
artifact_type: String,
title: String,
data: serde_json::Value,
actions: Option<Vec<ArtifactAction>>,
editable: Option<bool>,
},
}
impl AgentTool {
pub fn risk_tier(&self) -> ToolRiskTier {
match self {
// ... existing tiers ...
Self::RenderArtifact { .. } => ToolRiskTier::ArtifactRender,
}
}
}
D4: Artifact Lifecycle & Store
The workspace store gains artifact-aware state management:
// Extended workspace store
import { writable, derived } from 'svelte/store';
/** Stack of rendered artifacts (most recent on top) */
export const artifactStack = writable([]);
/** Currently visible artifact (top of stack, or null) */
export const activeArtifact = derived(
artifactStack,
($stack) => $stack.length > 0 ? $stack[$stack.length - 1] : null
);
/** User edits tracked as diffs for feedback to LLM */
export const artifactEdits = writable({});
/**
* Push a new artifact onto the workspace.
* If an artifact with the same ID exists, update it in place.
*/
export function renderArtifact(descriptor) {
artifactStack.update(stack => {
const existing = stack.findIndex(a => a.id === descriptor.id);
if (existing >= 0) {
// Update in place — preserves stack position
const updated = [...stack];
updated[existing] = { ...descriptor, updatedAt: Date.now() };
return updated;
}
return [...stack, { ...descriptor, createdAt: Date.now() }];
});
}
/**
* Dismiss an artifact — remove from stack, return to previous view.
*/
export function dismissArtifact(id) {
artifactStack.update(stack => stack.filter(a => a.id !== id));
artifactEdits.update(edits => {
const { [id]: _, ...rest } = edits;
return rest;
});
}
/**
* Record a user edit on an artifact for LLM feedback.
*/
export function trackArtifactEdit(artifactId, edit) {
artifactEdits.update(edits => ({
...edits,
[artifactId]: [...(edits[artifactId] || []), { ...edit, timestamp: Date.now() }],
}));
}
Artifact stack rather than single artifact: the LLM may project a velocity curve while a config diff is pending. The stack preserves both; dismissing the curve reveals the diff underneath. This mirrors how the existing pendingConfigChange and learnRefinement can technically coexist.
D5: Bidirectional Editing & Feedback
When an artifact is marked editable: true, user modifications are tracked and injected into the next LLM turn as structured context. The feedback path:
User edits artifact → trackArtifactEdit() stores diff →
Next LLM turn includes artifact edits in system context →
LLM sees what user changed → responds accordingly
The edit payload varies by artifact type:
| Artifact Type | Edit Shape | Example |
|---|---|---|
velocity-curve | { points: [{x, y}...], curveType: string } | User drags control point on curve |
mapping-table | { rowId: string, field: string, oldValue, newValue } | User edits a cell in mapping table |
config-diff | { action: 'apply' | 'reject' | 'edit', editedToml?: string } | Existing Plan/Apply (unchanged) |
midi-learn | { selectedInterpretation: string, params: object } | Existing refinement (unchanged) |
The chat store's _buildSystemContext() method appends artifact edits to the system message when edits exist:
function _buildArtifactContext() {
const edits = get(artifactEdits);
if (Object.keys(edits).length === 0) return '';
const active = get(activeArtifact);
let ctx = '\n\n## Active Workspace Artifacts\n';
for (const [id, editList] of Object.entries(edits)) {
const artifact = get(artifactStack).find(a => a.id === id);
if (!artifact) continue;
ctx += `\n### ${artifact.title} (${artifact.type})\n`;
ctx += `User edits: ${JSON.stringify(editList)}\n`;
}
return ctx;
}
D6: Contextual Actions
Artifacts can declare actions — buttons rendered in the artifact's header or inline. When the user clicks an action, it's dispatched as a tool result that resumes the agentic loop:
function handleArtifactAction(artifactId, actionId) {
const artifact = get(artifactStack).find(a => a.id === artifactId);
// Encode as tool result for the LLM
const toolResult = {
type: 'artifact_action',
artifact_id: artifactId,
artifact_type: artifact.type,
action_id: actionId,
// Include current artifact state (user may have edited it)
current_data: artifact.data,
edits: get(artifactEdits)[artifactId] || [],
};
// Resume agentic loop with this context
chatStore.resumeWithToolResult(toolResult);
}
This is the same mechanism as Plan/Apply's "Apply" button — the difference is that actions are declared by the artifact descriptor rather than hard-coded in the component. An LLM rendering a velocity curve might declare actions like:
{
"actions": [
{ "id": "apply-curve", "label": "Apply to Mapping", "variant": "primary" },
{ "id": "randomize", "label": "Randomize", "variant": "secondary" },
{ "id": "reset", "label": "Reset to Linear", "variant": "secondary" }
]
}
D7: Migration Path for Existing Views
Existing workspace views are migrated incrementally to the artifact system:
| Current | Migration | Status |
|---|---|---|
CONFIG_DIFF + pendingConfigChange | Becomes config-diff artifact type. showConfigDiff() calls renderArtifact() internally. | Delivered by ADR-017 Phase 3B |
MAPPING_EDITOR | Becomes mapping-editor artifact type. LLM can project pre-filled editor. | Delivered by ADR-017 Phase 3B |
MIDI_LEARN + learnRefinement | Removed by ADR-017 Phase 1D. MidiLearnDialog replaced by Events panel Learn mode + MappingEditor. Not an artifact — absorbed into the Events bridge. | No longer applicable |
MAPPINGS | Remains as the default view when no artifact is active. Not an artifact — it's the workspace's resting state. | No migration needed |
RAW_CONFIG | Optionally becomes an artifact. Lower priority — it's user-initiated, not LLM-projected. | ADR-013 Phase 3 |
CONFIG_HISTORY, DEVICE_SETTINGS, PROFILE_SETTINGS, APP_SETTINGS, PLUGINS | Remain as direct workspace views. These are navigation destinations, not LLM-projected content. | No migration needed |
The WORKSPACE_VIEWS enum continues to exist for non-artifact views. The workspace host component (with ArtifactHost added by ADR-017 Phase 3A) checks for an active artifact first; if none, it falls back to the current workspaceView value.
D8: Current State — What ADR-017 Phase 3 Delivers
ADR-017 Phase 3 implements the minimum ADR-013 infrastructure. After ADR-017 is complete, the following will exist:
| Component | Delivered By | Status |
|---|---|---|
ArtifactDescriptor interface (id, type, title, data, actions, editable) | ADR-017 3A | Foundation |
ARTIFACT_REGISTRY map (type string → Svelte component) | ADR-017 3A | 2 entries: mapping-editor, config-diff |
artifactStack store with renderArtifact() / dismissArtifact() | ADR-017 3A | Working |
ArtifactHost.svelte with <svelte:component> rendering | ADR-017 3A | Working |
ArtifactRender tool risk tier | ADR-017 3B | In Rust backend |
mapping-editor artifact type | ADR-017 3B | Registered, LLM can project |
config-diff artifact type | ADR-017 3B | Migrated from showConfigDiff() wrapper |
| Workspace host integration (artifact-first, view fallback) | ADR-017 3A | Working |
What ADR-017 Phase 3 does NOT deliver (this ADR's scope):
| Component | Why Not in ADR-017 | This ADR Phase |
|---|---|---|
artifactEdits store + trackArtifactEdit() | ADR-017 artifacts are write-once; no user editing tracked | Phase 1 |
| Edit context injection into LLM system message | No bidirectional editing in ADR-017 | Phase 1 |
Contextual actions from ArtifactDescriptor.actions[] | ADR-017 uses fixed Save/Cancel, not descriptor-declared actions | Phase 1 |
Action dispatch → resumeWithToolResult() | Not needed for mapping editor | Phase 1 |
render_artifact generic MCP tool | ADR-017 uses type-specific tools (conductor_set_mapping_editor) | Phase 1 |
mapping-table artifact type | Not in ADR-017 scope | Phase 2 |
velocity-curve artifact type | Exists as VelocityMappingSelector embedded in editor, not as standalone artifact | Phase 2 |
device-topology artifact type | Not in any ADR | Phase 3 |
markdown artifact type | Not in any ADR | Phase 3 |
action-sequence artifact type | Not in any ADR | Phase 3 |
| Artifact stack indicator UI | ADR-017 uses single-artifact rendering | Phase 1 |
| Rate limiting on LLM artifact projection | Not needed for ADR-017's fixed tool set | Phase 1 |
D9: Implementation Plan — Post-ADR-017
This plan assumes ADR-017 Phases 1-3 are complete. The artifact infrastructure exists; this ADR extends it with bidirectional editing, contextual actions, new artifact types, and the generic render_artifact tool.
Phase 1: Complete Artifact Framework + Generic Tool (6-8h)
1A: Bidirectional editing — artifactEdits store,
trackArtifactEdit(), edit context injection into
_buildSystemContext(). Artifact components emit
edit events, store compresses history. (2-3h)
1B: Contextual actions — render action buttons from
ArtifactDescriptor.actions[], wire dispatch to
resumeWithToolResult(), action event logging. (2-3h)
1C: Generic render_artifact MCP tool — single tool that
accepts any registered artifact type. ArtifactRender
tier. Rate limiting (max 3 projections per agentic
turn). Artifact stack indicator UI (badge showing
count, dropdown to switch). (2-3h)
Phase 2: First New Artifact Types (8-11h)
2A: mapping-table artifact — sortable/filterable
mapping overview. Columns: trigger, action, mode,
device, fire count. LLM renders via render_artifact.
Editable: inline cell editing with trackArtifactEdit().
Actions: "Apply Filter", "Export CSV". (4-5h)
2B: velocity-curve artifact — standalone SVG curve
editor (repurposed from VelocityMappingSelector
pattern but as an independent artifact). Draggable
control points, curve type selection, editable with
point-drag tracking. Actions: "Apply to Mapping",
"Reset to Linear". (4-6h)
Phase 3: Advanced Artifact Types (7-9h)
3A: device-topology artifact — SVG graph of devices →
modes → mapping counts. Read-only initially, LLM
can regenerate. Uses device colours from
device-colors.ts. Actions: "Refresh", "Add Device". (3-4h)
3B: markdown artifact — rendered markdown for guides,
explanations, onboarding content. Read-only, styled
with Conductor mono font and colour tokens. Support
code blocks, headings, lists, links. (2-3h)
3C: action-sequence artifact — step-by-step action
preview with timing. Shows each action in a
Sequence/Conditional chain with expected duration
and state transitions. Read-only. (2-3h)
Total: 21-28h across 3 phases (after ADR-017 Phase 3).
Phase 2A and 2B can run in parallel. Phase 3A/3B/3C are independent of each other.
D10: Detailed Component Specifications
mapping-table Artifact
The LLM projects this when asked about mapping overviews ("show me all my CC mappings", "what's mapped to the Mikro?"). Reuses MappingRow visual patterns from the existing mapping list.
// Example descriptor
{
id: "mt-001",
type: "mapping-table",
title: "CC Mappings — Mikro MK3",
data: {
mappings: [
{ id: "m1", trigger: "CC7 ch.1", action: "Keystroke ⌘C", mode: "Default", device: "Mikro MK3", fires: 31, triggerType: "cc" },
{ id: "m2", trigger: "CC1 ch.1", action: "VolumeControl", mode: "Default", device: "Mikro MK3", fires: 12, triggerType: "cc" },
],
sortable: true,
filterable: true,
columns: ["trigger", "action", "mode", "device", "fires"],
},
actions: [
{ id: "apply-filter", label: "Apply Filter", variant: "secondary" },
{ id: "export-csv", label: "Export CSV", variant: "secondary" },
],
editable: true,
}
Component: MappingTableArtifact.svelte (~300 LOC)
- Renders header row with sortable column headers (click to sort, arrow indicator)
- Each data row uses mapping-row styling: coloured dot (by triggerType), 7px 14px padding, 10px gap
- Editable cells: click to edit, Tab to advance, Escape to cancel
- Filter row: input fields per column for text filtering
- Emits
editevents viatrackArtifactEdit()on cell changes
velocity-curve Artifact
Standalone curve editor for velocity transforms. Repurposes the visual pattern from VelocityMappingSelector.svelte (694 LOC) but as an independent artifact — not embedded in SendMidiActionEditor.
{
id: "vc-001",
type: "velocity-curve",
title: "Velocity Curve — CC7 Transform",
data: {
curveType: "exponential", // fixed | passthrough | linear | curve
curveSubType: "exponential", // exponential | logarithmic | s-curve
points: [
{ x: 0, y: 0 }, { x: 32, y: 16 }, { x: 64, y: 40 },
{ x: 96, y: 80 }, { x: 127, y: 127 }
],
inputRange: [0, 127],
outputRange: [0, 127],
},
actions: [
{ id: "apply-curve", label: "Apply to Mapping", variant: "primary" },
{ id: "randomize", label: "Randomize", variant: "secondary" },
{ id: "reset-linear", label: "Reset to Linear", variant: "secondary" },
],
editable: true,
}
Component: VelocityCurveArtifact.svelte (~400 LOC)
- SVG canvas with 0-127 grid, curve path, draggable control points
- Curve type chips below canvas (Fixed, PassThrough, Linear, Curve)
- Point drag emits
trackArtifactEdit({ type: 'point-move', pointIndex, from: {x,y}, to: {x,y} }) - Curve type change emits
trackArtifactEdit({ type: 'curve-type-change', from, to }) - Edit history compressed: multiple small point drags within 500ms coalesced into single edit
device-topology Artifact
SVG graph showing signal flow: devices → modes → mapping counts. The LLM renders this for topology questions ("how are my devices configured?", "show the signal flow").
Component: DeviceTopologyArtifact.svelte (~350 LOC)
- Device nodes: rounded rectangles with device colour borders (from device-colors.ts)
- Mode nodes: chip-bg background with mapping count badges
- Connection lines: 1px solid, device colour
- Layout: auto-arranged left-to-right (devices → modes → optional action summary)
- Read-only initially; LLM regenerates the graph when topology changes
markdown Artifact
Rich text display for guides, explanations, and onboarding. The LLM renders this for tutorial-style content ("how do velocity curves work?", "explain compound triggers").
Component: MarkdownArtifact.svelte (~150 LOC)
- Parses markdown to HTML (use
markedorsnarkdownlibrary — keep lightweight) - Styled with Conductor tokens: headings in
--text-bright, body in--text, code blocks in--bg-cardwith--font-mono - Links open in external browser (Tauri shell.open)
- Read-only, not editable
action-sequence Artifact
Step-by-step action preview showing what a mapping's action chain does. The LLM renders this when explaining complex mappings ("what does this Sequence action do?").
Component: ActionSequenceArtifact.svelte (~200 LOC)
- Numbered steps with action type icons
- Timing annotations between steps (e.g., "50ms delay")
- Conditional branches shown as indented if/else blocks
- Highlight current step during simulation (if mapping is being simulated)
- Read-only
What This ADR Does NOT Cover
- Generic block editor — Not building Notion/Google Docs. Each artifact type has a purpose-built component for the MIDI mapping domain.
- Node-based graph editor — Device topology visualization uses a read-only (or LLM-editable) graph, not a drag-and-drop node editor. Users configure mappings through chat, not wiring.
- Multi-artifact split view — All phases show one artifact at a time (with a stack for multiple). A split/tabbed artifact view is a future enhancement if demand exists.
- Collaborative editing — Single-user desktop app. No multi-cursor, no conflict resolution.
- Plugin artifact types — The
customartifact type and plugin-defined components are future work (requires WASM plugin API for UI rendering, which is a separate architectural decision).
Consequences
Positive
- Eliminates per-view boilerplate. New LLM-projected content requires one Svelte component and one registry entry — no store changes, no workspace enum additions, no custom wiring.
- Preserves existing flows. ConfigDiff and MidiLearn continue working through backward-compatible wrappers. Zero breaking changes.
- Enables richer LLM interactions. The LLM can show velocity curves, mapping tables, device graphs — not just text responses and config diffs.
- Bidirectional editing creates a true canvas. User edits on artifacts feed back into the LLM's context, enabling iterative refinement loops ("make it more logarithmic" → user drags point → "I see you moved the midpoint, let me adjust the curve to match").
- Audit trail integration. All artifact renders and user actions on artifacts are logged through the existing audit system (ADR-007 Phase 4).
- Plugin-ready architecture. The component registry pattern extends naturally to plugin-defined artifact types in the future.
Negative
- Artifact stack complexity. Managing multiple overlapping artifacts (e.g., a config diff underneath a velocity curve) requires careful UX design — users need clear affordances for navigating the stack and understanding what's "underneath."
- LLM context budget. Injecting artifact edit history into the system context consumes tokens. For artifacts with frequent small edits (e.g., dragging curve points), edit history must be compressed or summarized to avoid context overflow.
- Component proliferation. Each artifact type is a separate Svelte component. While this is simpler than a generic renderer, it means the artifact library grows linearly with capability. Mitigation: start with 3-4 high-value types, add more only when user demand exists.
- Testing surface. Each artifact type needs its own interaction tests — the framework provides routing but not rendering guarantees. Mitigation: standardize the artifact component contract (
artifactprop,actionevent,editevent) so a single test harness validates all types.
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| LLM over-projects artifacts (workspace constantly switching) | Medium | Medium | Rate limiting on ArtifactRender tier; user can pin current view to prevent LLM interruption |
| Artifact edits create confusing LLM context | Medium | Low | Compress edit history to last N edits; summarize point-by-point drags as "user adjusted curve shape" |
| Velocity curve / topology renderers are complex to build | Low | Medium | Start with static renderers, add interactivity incrementally; SVG + Svelte is well-suited for this |
| Plugin artifact types need WASM UI rendering | Low | High | Defer to future ADR; base artifact types cover core needs |
Implementation Estimate
| Phase | Work | Hours | Dependencies |
|---|---|---|---|
| 1 | Complete artifact framework: bidirectional editing, contextual actions, generic tool, stack UI | 6-8h | ADR-017 Phase 3 |
| 2 | mapping-table + velocity-curve artifact types | 8-11h | Phase 1 |
| 3 | device-topology + markdown + action-sequence artifact types | 7-9h | Phase 1 |
| Total | 21-28h |
Phases 2 and 3 can run in parallel once Phase 1 is complete. Within each phase, sub-tasks (2A/2B, 3A/3B/3C) are independent.
Combined timeline with ADR-017:
ADR-017 Phase 1-2 → ADR-017 Phase 3 (delivers foundation)
↓
ADR-013 Phase 1 (framework completion)
↓
ADR-013 Phase 2 ←→ ADR-013 Phase 3
(can parallel)
References
- ADR-007: LLM Integration Architecture — Tool risk tiers, Plan/Apply workflow, agentic loop
- ADR-003: GUI State Management — Svelte stores, workspace view management
- ADR-012: Theme Architecture — CSS variable system used by artifact components
- ADR-017: Unified Mapping Experience — Phase 3 delivers minimum ADR-013 infrastructure (ArtifactDescriptor, registry, stack, host)
- ADR-016: LLM Signal Awareness — T1-T4 context tiers, signal pulse data used by device-topology artifact
- Mockup:
adr-013-artifact-mockups.html— companion mockups (A1-A5: projection, stack, editing, actions, markdown) - Conductor GUI v2 Implementation Spec — Three-zone layout, workspace views
- Gap Analysis v2 — Current workspace view inventory
conductor-gui/ui/src/lib/panels/WorkspacePanel.svelte— Current workspace view switching (if/else-if chain to be extended)conductor-gui/ui/src/lib/stores/workspace.js— WORKSPACE_VIEWS enum, pendingConfigChange, workspaceView storeconductor-gui/ui/src/lib/components/VelocityMappingSelector.svelte— SVG curve pattern reused by velocity-curve artifact (694 LOC)conductor-gui/ui/src/lib/components/MappingRow.svelte— Row styling reused by mapping-table artifact- Claude Artifacts Documentation — Tool-driven artifact projection pattern
- ChatGPT Canvas — Bidirectional editing pattern