@plures/runebook 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/ANALYSIS_LADDER.md +231 -0
  2. package/CHANGELOG.md +124 -0
  3. package/INTEGRATIONS.md +242 -0
  4. package/LICENSE +21 -0
  5. package/MEMORY.md +253 -0
  6. package/NIXOS.md +357 -0
  7. package/QUICKSTART.md +157 -0
  8. package/README.md +295 -0
  9. package/RELEASE.md +190 -0
  10. package/ValidationChecklist.md +598 -0
  11. package/docs/demo.md +338 -0
  12. package/docs/llm-integration.md +300 -0
  13. package/docs/parallel-execution-plan.md +160 -0
  14. package/flake.nix +228 -0
  15. package/integrations/README.md +242 -0
  16. package/integrations/demo-steps.sh +64 -0
  17. package/integrations/nvim-runebook.lua +140 -0
  18. package/integrations/tmux-status.sh +51 -0
  19. package/integrations/vim-runebook.vim +77 -0
  20. package/integrations/wezterm-status-simple.lua +48 -0
  21. package/integrations/wezterm-status.lua +76 -0
  22. package/nixos-module.nix +156 -0
  23. package/package.json +76 -0
  24. package/packages/design-dojo/index.js +4 -0
  25. package/packages/design-dojo/package.json +20 -0
  26. package/packages/design-dojo/tokens.css +69 -0
  27. package/playwright.config.ts +16 -0
  28. package/scripts/check-versions.cjs +62 -0
  29. package/scripts/demo.sh +220 -0
  30. package/shell.nix +31 -0
  31. package/src/app.html +13 -0
  32. package/src/cli/index.ts +1050 -0
  33. package/src/lib/agent/analysis-pipeline.ts +347 -0
  34. package/src/lib/agent/analysis-service.ts +171 -0
  35. package/src/lib/agent/analysis.ts +159 -0
  36. package/src/lib/agent/analyzers/heuristic.ts +289 -0
  37. package/src/lib/agent/analyzers/index.ts +7 -0
  38. package/src/lib/agent/analyzers/llm.ts +204 -0
  39. package/src/lib/agent/analyzers/local-search.ts +215 -0
  40. package/src/lib/agent/capture.ts +123 -0
  41. package/src/lib/agent/index.ts +244 -0
  42. package/src/lib/agent/integration.ts +81 -0
  43. package/src/lib/agent/llm/providers/base.ts +99 -0
  44. package/src/lib/agent/llm/providers/index.ts +60 -0
  45. package/src/lib/agent/llm/providers/mock.ts +67 -0
  46. package/src/lib/agent/llm/providers/ollama.ts +151 -0
  47. package/src/lib/agent/llm/providers/openai.ts +153 -0
  48. package/src/lib/agent/llm/sanitizer.ts +170 -0
  49. package/src/lib/agent/llm/types.ts +118 -0
  50. package/src/lib/agent/memory.ts +363 -0
  51. package/src/lib/agent/node-status.ts +56 -0
  52. package/src/lib/agent/node-suggestions.ts +64 -0
  53. package/src/lib/agent/status.ts +80 -0
  54. package/src/lib/agent/suggestions.ts +169 -0
  55. package/src/lib/components/Canvas.svelte +124 -0
  56. package/src/lib/components/ConnectionLine.svelte +46 -0
  57. package/src/lib/components/DisplayNode.svelte +167 -0
  58. package/src/lib/components/InputNode.svelte +158 -0
  59. package/src/lib/components/TerminalNode.svelte +237 -0
  60. package/src/lib/components/Toolbar.svelte +359 -0
  61. package/src/lib/components/TransformNode.svelte +327 -0
  62. package/src/lib/core/index.ts +31 -0
  63. package/src/lib/core/observer.ts +278 -0
  64. package/src/lib/core/redaction.ts +158 -0
  65. package/src/lib/core/shell-adapters/base.ts +325 -0
  66. package/src/lib/core/shell-adapters/bash.ts +110 -0
  67. package/src/lib/core/shell-adapters/index.ts +62 -0
  68. package/src/lib/core/shell-adapters/zsh.ts +105 -0
  69. package/src/lib/core/storage.ts +360 -0
  70. package/src/lib/core/types.ts +176 -0
  71. package/src/lib/design-dojo/Box.svelte +47 -0
  72. package/src/lib/design-dojo/Button.svelte +75 -0
  73. package/src/lib/design-dojo/Input.svelte +65 -0
  74. package/src/lib/design-dojo/List.svelte +38 -0
  75. package/src/lib/design-dojo/Select.svelte +48 -0
  76. package/src/lib/design-dojo/SplitPane.svelte +43 -0
  77. package/src/lib/design-dojo/StatusBar.svelte +61 -0
  78. package/src/lib/design-dojo/Table.svelte +47 -0
  79. package/src/lib/design-dojo/Text.svelte +36 -0
  80. package/src/lib/design-dojo/Toggle.svelte +48 -0
  81. package/src/lib/design-dojo/index.ts +10 -0
  82. package/src/lib/stores/canvas-praxis.ts +268 -0
  83. package/src/lib/stores/canvas.ts +58 -0
  84. package/src/lib/types/agent.ts +78 -0
  85. package/src/lib/types/canvas.ts +71 -0
  86. package/src/lib/utils/storage.ts +326 -0
  87. package/src/lib/utils/yaml-loader.ts +52 -0
  88. package/src/routes/+layout.svelte +5 -0
  89. package/src/routes/+layout.ts +5 -0
  90. package/src/routes/+page.svelte +32 -0
  91. package/src-tauri/Cargo.lock +5735 -0
  92. package/src-tauri/Cargo.toml +38 -0
  93. package/src-tauri/build.rs +3 -0
  94. package/src-tauri/capabilities/default.json +10 -0
  95. package/src-tauri/icons/128x128.png +0 -0
  96. package/src-tauri/icons/128x128@2x.png +0 -0
  97. package/src-tauri/icons/32x32.png +0 -0
  98. package/src-tauri/icons/Square107x107Logo.png +0 -0
  99. package/src-tauri/icons/Square142x142Logo.png +0 -0
  100. package/src-tauri/icons/Square150x150Logo.png +0 -0
  101. package/src-tauri/icons/Square284x284Logo.png +0 -0
  102. package/src-tauri/icons/Square30x30Logo.png +0 -0
  103. package/src-tauri/icons/Square310x310Logo.png +0 -0
  104. package/src-tauri/icons/Square44x44Logo.png +0 -0
  105. package/src-tauri/icons/Square71x71Logo.png +0 -0
  106. package/src-tauri/icons/Square89x89Logo.png +0 -0
  107. package/src-tauri/icons/StoreLogo.png +0 -0
  108. package/src-tauri/icons/icon.icns +0 -0
  109. package/src-tauri/icons/icon.ico +0 -0
  110. package/src-tauri/icons/icon.png +0 -0
  111. package/src-tauri/src/agents/agent1.rs +66 -0
  112. package/src-tauri/src/agents/agent2.rs +80 -0
  113. package/src-tauri/src/agents/agent3.rs +73 -0
  114. package/src-tauri/src/agents/agent4.rs +66 -0
  115. package/src-tauri/src/agents/agent5.rs +68 -0
  116. package/src-tauri/src/agents/agent6.rs +75 -0
  117. package/src-tauri/src/agents/base.rs +52 -0
  118. package/src-tauri/src/agents/mod.rs +17 -0
  119. package/src-tauri/src/core/coordination.rs +117 -0
  120. package/src-tauri/src/core/mod.rs +12 -0
  121. package/src-tauri/src/core/ownership.rs +61 -0
  122. package/src-tauri/src/core/types.rs +132 -0
  123. package/src-tauri/src/execution/mod.rs +5 -0
  124. package/src-tauri/src/execution/runner.rs +143 -0
  125. package/src-tauri/src/lib.rs +161 -0
  126. package/src-tauri/src/main.rs +6 -0
  127. package/src-tauri/src/memory/api.rs +422 -0
  128. package/src-tauri/src/memory/client.rs +156 -0
  129. package/src-tauri/src/memory/encryption.rs +79 -0
  130. package/src-tauri/src/memory/migration.rs +110 -0
  131. package/src-tauri/src/memory/mod.rs +28 -0
  132. package/src-tauri/src/memory/schema.rs +275 -0
  133. package/src-tauri/src/memory/tests.rs +192 -0
  134. package/src-tauri/src/orchestrator/coordinator.rs +232 -0
  135. package/src-tauri/src/orchestrator/mod.rs +13 -0
  136. package/src-tauri/src/orchestrator/planner.rs +304 -0
  137. package/src-tauri/tauri.conf.json +35 -0
  138. package/static/examples/date-time-example.yaml +147 -0
  139. package/static/examples/hello-world.yaml +74 -0
  140. package/static/examples/transform-example.yaml +157 -0
  141. package/static/favicon.png +0 -0
  142. package/static/svelte.svg +1 -0
  143. package/static/tauri.svg +6 -0
  144. package/static/vite.svg +1 -0
  145. package/svelte.config.js +18 -0
  146. package/tsconfig.json +19 -0
  147. package/vite.config.js +45 -0
  148. package/vitest.config.ts +21 -0
@@ -0,0 +1,169 @@
1
+ // Suggestion rendering and management for Ambient Agent Mode
2
+
3
+ import type { Suggestion } from '../types/agent';
4
+
5
+ export interface SuggestionStore {
6
+ suggestions: Suggestion[];
7
+ add(suggestion: Suggestion): void;
8
+ remove(id: string): void;
9
+ clear(): void;
10
+ getByPriority(priority: 'low' | 'medium' | 'high'): Suggestion[];
11
+ getByType(type: Suggestion['type']): Suggestion[];
12
+ getForCommand(command: string): Suggestion[];
13
+ getTop(limit?: number): Suggestion[];
14
+ save(): Promise<void>;
15
+ load(): Promise<void>;
16
+ }
17
+
18
+ /**
19
+ * In-memory suggestion store
20
+ */
21
+ export class MemorySuggestionStore implements SuggestionStore {
22
+ suggestions: Suggestion[] = [];
23
+
24
+ add(suggestion: Suggestion): void {
25
+ // Avoid duplicates
26
+ if (!this.suggestions.find(s => s.id === suggestion.id)) {
27
+ this.suggestions.push(suggestion);
28
+ // Keep only recent suggestions (last 100)
29
+ if (this.suggestions.length > 100) {
30
+ this.suggestions = this.suggestions.slice(-100);
31
+ }
32
+ }
33
+ }
34
+
35
+ remove(id: string): void {
36
+ this.suggestions = this.suggestions.filter(s => s.id !== id);
37
+ }
38
+
39
+ clear(): void {
40
+ this.suggestions = [];
41
+ }
42
+
43
+ getByPriority(priority: 'low' | 'medium' | 'high'): Suggestion[] {
44
+ return this.suggestions.filter(s => s.priority === priority);
45
+ }
46
+
47
+ getByType(type: Suggestion['type']): Suggestion[] {
48
+ return this.suggestions.filter(s => s.type === type);
49
+ }
50
+
51
+ getForCommand(command: string): Suggestion[] {
52
+ return this.suggestions.filter(s =>
53
+ s.context?.command === command ||
54
+ s.command === command
55
+ );
56
+ }
57
+
58
+ getTop(limit: number = 1): Suggestion[] {
59
+ // Sort by priority (high > medium > low) and timestamp (newest first)
60
+ const priorityOrder = { high: 3, medium: 2, low: 1 };
61
+ const sorted = [...this.suggestions].sort((a, b) => {
62
+ const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority];
63
+ if (priorityDiff !== 0) return priorityDiff;
64
+ return b.timestamp - a.timestamp;
65
+ });
66
+ return sorted.slice(0, limit);
67
+ }
68
+
69
+ async save(): Promise<void> {
70
+ // No-op for in-memory store
71
+ }
72
+
73
+ async load(): Promise<void> {
74
+ // No-op for in-memory store
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Format suggestion for display
80
+ */
81
+ export function formatSuggestion(suggestion: Suggestion): string {
82
+ const priorityEmoji = {
83
+ low: '💡',
84
+ medium: '⚡',
85
+ high: '⚠️',
86
+ };
87
+
88
+ const typeEmoji = {
89
+ command: '▶️',
90
+ optimization: '⚡',
91
+ shortcut: '🔗',
92
+ warning: '⚠️',
93
+ tip: '💡',
94
+ };
95
+
96
+ const emoji = `${priorityEmoji[suggestion.priority]} ${typeEmoji[suggestion.type]}`;
97
+ let output = `${emoji} ${suggestion.title}\n`;
98
+ output += ` ${suggestion.description}\n`;
99
+
100
+ if (suggestion.command) {
101
+ const args = suggestion.args ? suggestion.args.join(' ') : '';
102
+ output += ` Command: ${suggestion.command} ${args}\n`;
103
+ }
104
+
105
+ return output;
106
+ }
107
+
108
+ /**
109
+ * Format suggestions for headless/CLI output
110
+ */
111
+ export function formatSuggestionsForCLI(suggestions: Suggestion[]): string {
112
+ if (suggestions.length === 0) {
113
+ return 'No suggestions available.\n';
114
+ }
115
+
116
+ let output = `\n=== Suggestions (${suggestions.length}) ===\n\n`;
117
+
118
+ // Group by priority
119
+ const high = suggestions.filter(s => s.priority === 'high');
120
+ const medium = suggestions.filter(s => s.priority === 'medium');
121
+ const low = suggestions.filter(s => s.priority === 'low');
122
+
123
+ if (high.length > 0) {
124
+ output += 'HIGH PRIORITY:\n';
125
+ high.forEach(s => {
126
+ output += formatSuggestion(s) + '\n';
127
+ });
128
+ }
129
+
130
+ if (medium.length > 0) {
131
+ output += 'MEDIUM PRIORITY:\n';
132
+ medium.forEach(s => {
133
+ output += formatSuggestion(s) + '\n';
134
+ });
135
+ }
136
+
137
+ if (low.length > 0) {
138
+ output += 'LOW PRIORITY:\n';
139
+ low.forEach(s => {
140
+ output += formatSuggestion(s) + '\n';
141
+ });
142
+ }
143
+
144
+ return output;
145
+ }
146
+
147
+ /**
148
+ * Format a single suggestion for compact display (for status lines)
149
+ */
150
+ export function formatSuggestionCompact(suggestion: Suggestion): string {
151
+ const prioritySymbol = {
152
+ low: '•',
153
+ medium: '▲',
154
+ high: '⚠',
155
+ };
156
+
157
+ return `${prioritySymbol[suggestion.priority]} ${suggestion.title}`;
158
+ }
159
+
160
+ /**
161
+ * Format top suggestion for status display
162
+ */
163
+ export function formatTopSuggestion(suggestion: Suggestion | null): string {
164
+ if (!suggestion) {
165
+ return '';
166
+ }
167
+ return formatSuggestionCompact(suggestion);
168
+ }
169
+
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import { canvasStore } from '../stores/canvas';
3
+ import TerminalNodeComponent from './TerminalNode.svelte';
4
+ import InputNodeComponent from './InputNode.svelte';
5
+ import DisplayNodeComponent from './DisplayNode.svelte';
6
+ import TransformNodeComponent from './TransformNode.svelte';
7
+ import ConnectionLine from './ConnectionLine.svelte';
8
+ import type { CanvasNode } from '../types/canvas';
9
+ import type { ComponentType, SvelteComponent } from 'svelte';
10
+ import Box from '../design-dojo/Box.svelte';
11
+
12
+ interface Props {
13
+ tui?: boolean;
14
+ }
15
+
16
+ let { tui = false }: Props = $props();
17
+
18
+ let isDragging = $state(false);
19
+ let draggedNodeId = $state<string | null>(null);
20
+ let dragOffset = $state({ x: 0, y: 0 });
21
+ let viewportOffset = $state({ x: 0, y: 0 });
22
+ let scale = $state(1.0);
23
+
24
+ const canvasData = $derived($canvasStore);
25
+
26
+ function handleNodeMouseDown(event: MouseEvent, nodeId: string) {
27
+ const node = canvasData.nodes.find(n => n.id === nodeId);
28
+ if (!node) return;
29
+
30
+ isDragging = true;
31
+ draggedNodeId = nodeId;
32
+ dragOffset = {
33
+ x: event.clientX - node.position.x,
34
+ y: event.clientY - node.position.y
35
+ };
36
+ event.preventDefault();
37
+ }
38
+
39
+ function handleMouseMove(event: MouseEvent) {
40
+ if (isDragging && draggedNodeId) {
41
+ const newX = event.clientX - dragOffset.x;
42
+ const newY = event.clientY - dragOffset.y;
43
+ canvasStore.updateNodePosition(draggedNodeId, newX, newY);
44
+ }
45
+ }
46
+
47
+ function handleMouseUp() {
48
+ isDragging = false;
49
+ draggedNodeId = null;
50
+ }
51
+ </script>
52
+
53
+ <svelte:window
54
+ onmousemove={handleMouseMove}
55
+ onmouseup={handleMouseUp}
56
+ />
57
+
58
+ <Box class="canvas-container" {tui}>
59
+ <svg class="connections-layer">
60
+ {#each canvasData.connections as connection}
61
+ <ConnectionLine {connection} nodes={canvasData.nodes} {tui} />
62
+ {/each}
63
+ </svg>
64
+
65
+ <div class="nodes-layer">
66
+ {#each canvasData.nodes as node (node.id)}
67
+ <div
68
+ class="node-wrapper"
69
+ role="button"
70
+ tabindex="0"
71
+ style="left: {node.position.x}px; top: {node.position.y}px;"
72
+ onmousedown={(e) => handleNodeMouseDown(e, node.id)}
73
+ >
74
+ {#if node.type === 'terminal'}
75
+ <TerminalNodeComponent node={node} {tui} />
76
+ {:else if node.type === 'input'}
77
+ <InputNodeComponent node={node} {tui} />
78
+ {:else if node.type === 'display'}
79
+ <DisplayNodeComponent node={node} {tui} />
80
+ {:else if node.type === 'transform'}
81
+ <TransformNodeComponent node={node} {tui} />
82
+ {/if}
83
+ </div>
84
+ {/each}
85
+ </div>
86
+ </Box>
87
+
88
+ <style>
89
+ :global(.canvas-container) {
90
+ position: relative;
91
+ width: 100%;
92
+ height: 100vh;
93
+ background-color: var(--surface-1);
94
+ background-image:
95
+ linear-gradient(var(--border-color) 1px, transparent 1px),
96
+ linear-gradient(90deg, var(--border-color) 1px, transparent 1px);
97
+ background-size: 20px 20px;
98
+ overflow: hidden;
99
+ }
100
+
101
+ .connections-layer {
102
+ position: absolute;
103
+ top: 0;
104
+ left: 0;
105
+ width: 100%;
106
+ height: 100%;
107
+ pointer-events: none;
108
+ z-index: 1;
109
+ }
110
+
111
+ .nodes-layer {
112
+ position: absolute;
113
+ top: 0;
114
+ left: 0;
115
+ width: 100%;
116
+ height: 100%;
117
+ z-index: 2;
118
+ }
119
+
120
+ .node-wrapper {
121
+ position: absolute;
122
+ cursor: move;
123
+ }
124
+ </style>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Connection, CanvasNode } from '../types/canvas';
3
+
4
+ interface Props {
5
+ connection: Connection;
6
+ nodes: CanvasNode[];
7
+ tui?: boolean;
8
+ }
9
+
10
+ let { connection, nodes, tui = false }: Props = $props();
11
+
12
+ function getNodePosition(nodeId: string) {
13
+ const node = nodes.find(n => n.id === nodeId);
14
+ return node ? node.position : { x: 0, y: 0 };
15
+ }
16
+
17
+ const fromPos = $derived(getNodePosition(connection.from));
18
+ const toPos = $derived(getNodePosition(connection.to));
19
+
20
+ // Calculate control points for a smooth curve
21
+ const path = $derived(() => {
22
+ const fromX = fromPos.x + 300; // Approximate node width
23
+ const fromY = fromPos.y + 50; // Approximate mid-height
24
+ const toX = toPos.x;
25
+ const toY = toPos.y + 50;
26
+
27
+ const controlOffset = Math.abs(toX - fromX) * 0.5;
28
+ const cp1x = fromX + controlOffset;
29
+ const cp1y = fromY;
30
+ const cp2x = toX - controlOffset;
31
+ const cp2y = toY;
32
+
33
+ return `M ${fromX} ${fromY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${toX} ${toY}`;
34
+ });
35
+ </script>
36
+
37
+ <path
38
+ d={path()}
39
+ fill="none"
40
+ stroke="var(--brand)"
41
+ stroke-width="2"
42
+ stroke-linecap="round"
43
+ data-tui={tui}
44
+ data-from={connection.from}
45
+ data-to={connection.to}
46
+ />
@@ -0,0 +1,167 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { DisplayNode } from '../types/canvas';
4
+ import { canvasStore, nodeDataStore, getNodeInputData } from '../stores/canvas';
5
+ import Box from '../design-dojo/Box.svelte';
6
+ import Text from '../design-dojo/Text.svelte';
7
+ import Table from '../design-dojo/Table.svelte';
8
+ import List from '../design-dojo/List.svelte';
9
+
10
+ interface Props {
11
+ node: DisplayNode;
12
+ tui?: boolean;
13
+ }
14
+
15
+ let { node, tui = false }: Props = $props();
16
+
17
+ // Initialize content from node prop (warning is expected as we need mutable state)
18
+ let content = $state<any>(node.content || '');
19
+
20
+ // Subscribe to node data changes to update content reactively
21
+ $effect(() => {
22
+ const canvas = $canvasStore;
23
+ const nodeData = $nodeDataStore;
24
+
25
+ // Get input data from connected nodes
26
+ if (node.inputs.length > 0) {
27
+ const inputData = getNodeInputData(node.id, node.inputs[0].id, canvas.connections, nodeData);
28
+ if (inputData !== undefined) {
29
+ content = inputData;
30
+ }
31
+ }
32
+ });
33
+
34
+ function formatContent() {
35
+ if (typeof content === 'object') {
36
+ return JSON.stringify(content, null, 2);
37
+ }
38
+ return String(content);
39
+ }
40
+ </script>
41
+
42
+ <Box class="display-node" surface={2} border radius={3} shadow={2} {tui}>
43
+ <Box class="node-header" surface={3} {tui}>
44
+ <span class="node-icon">📊</span>
45
+ <Text class="node-title">{node.label || 'Display'}</Text>
46
+ </Box>
47
+
48
+ <Box class="node-body" pad={3}>
49
+ {#if node.displayType === 'text' || node.displayType === 'json'}
50
+ <Box class="text-display" surface={1} pad={3} radius={2}>
51
+ <Text mono variant={1} class="display-pre">{formatContent()}</Text>
52
+ </Box>
53
+ {:else if node.displayType === 'table'}
54
+ <div class="table-display">
55
+ <Table {tui}>
56
+ {#if Array.isArray(content)}
57
+ <thead>
58
+ <tr>
59
+ {#each Object.keys(content[0] || {}) as key}
60
+ <th>{key}</th>
61
+ {/each}
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ {#each content as row}
66
+ <tr>
67
+ {#each Object.values(row) as val}
68
+ <td>{val}</td>
69
+ {/each}
70
+ </tr>
71
+ {/each}
72
+ </tbody>
73
+ {:else}
74
+ <tbody>
75
+ <tr>
76
+ <td>No table data</td>
77
+ </tr>
78
+ </tbody>
79
+ {/if}
80
+ </Table>
81
+ </div>
82
+ {:else}
83
+ <Box class="text-display" surface={1} pad={3} radius={2}>
84
+ <Text mono variant={1} class="display-pre">{formatContent()}</Text>
85
+ </Box>
86
+ {/if}
87
+ </Box>
88
+
89
+ <!-- Input ports -->
90
+ <div class="ports">
91
+ {#each node.inputs as port}
92
+ <div class="port input-port" data-port-id={port.id}>
93
+ <span class="port-label">{port.name}</span>
94
+ </div>
95
+ {/each}
96
+ </div>
97
+ </Box>
98
+
99
+ <style>
100
+ :global(.display-node) {
101
+ min-width: 300px;
102
+ max-width: 500px;
103
+ }
104
+
105
+ :global(.display-node .node-header) {
106
+ padding: var(--space-2) var(--space-3);
107
+ border-bottom: 1px solid var(--border-color);
108
+ display: flex;
109
+ align-items: center;
110
+ gap: var(--space-2);
111
+ border-radius: var(--radius-3) var(--radius-3) 0 0;
112
+ }
113
+
114
+ .node-icon {
115
+ font-size: 18px;
116
+ }
117
+
118
+ :global(.display-node .node-title) {
119
+ font-weight: 600;
120
+ font-size: var(--font-size-1);
121
+ }
122
+
123
+ :global(.display-node .node-body) {
124
+ padding: var(--space-3);
125
+ max-height: 400px;
126
+ overflow-y: auto;
127
+ }
128
+
129
+ :global(.display-node .text-display) {
130
+ font-family: var(--font-mono);
131
+ font-size: var(--font-size-0);
132
+ }
133
+
134
+ :global(.display-node .display-pre) {
135
+ display: block;
136
+ white-space: pre-wrap;
137
+ word-break: break-word;
138
+ }
139
+
140
+ .table-display {
141
+ overflow-x: auto;
142
+ }
143
+
144
+ .ports {
145
+ position: relative;
146
+ }
147
+
148
+ .port {
149
+ position: absolute;
150
+ width: 12px;
151
+ height: 12px;
152
+ background: var(--brand);
153
+ border: 2px solid var(--surface-2);
154
+ border-radius: 50%;
155
+ cursor: crosshair;
156
+ }
157
+
158
+ .input-port {
159
+ left: -8px;
160
+ top: 50%;
161
+ transform: translateY(-50%);
162
+ }
163
+
164
+ .port-label {
165
+ display: none;
166
+ }
167
+ </style>
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { InputNode } from '../types/canvas';
4
+ import { updateNodeData } from '../stores/canvas';
5
+ import Box from '../design-dojo/Box.svelte';
6
+ import Input from '../design-dojo/Input.svelte';
7
+ import Toggle from '../design-dojo/Toggle.svelte';
8
+ import Text from '../design-dojo/Text.svelte';
9
+
10
+ interface Props {
11
+ node: InputNode;
12
+ tui?: boolean;
13
+ }
14
+
15
+ let { node, tui = false }: Props = $props();
16
+
17
+ // Initialize value from node prop (warning is expected as we need mutable state)
18
+ let value = $state(node.value ?? '');
19
+
20
+ function handleValueChange() {
21
+ // Update the node's output data for reactive flow
22
+ if (node.outputs.length > 0) {
23
+ updateNodeData(node.id, node.outputs[0].id, value);
24
+ }
25
+ }
26
+
27
+ $effect(() => {
28
+ // Capture value outside untrack to establish it as the only reactive
29
+ // dependency. node properties are accessed inside untrack to prevent an
30
+ // infinite update cycle: Praxis deep-clones context on every dispatch,
31
+ // which creates a new node prop reference each time, which would otherwise
32
+ // re-trigger this effect indefinitely.
33
+ const currentValue = value;
34
+ untrack(() => {
35
+ if (node.outputs.length > 0) {
36
+ updateNodeData(node.id, node.outputs[0].id, currentValue);
37
+ }
38
+ });
39
+ });
40
+ </script>
41
+
42
+ <Box class="input-node" surface={2} border radius={3} shadow={2} {tui}>
43
+ <Box class="node-header" surface={3} {tui}>
44
+ <span class="node-icon">📝</span>
45
+ <Text class="node-title">{node.label || 'Input'}</Text>
46
+ </Box>
47
+
48
+ <Box class="node-body" pad={3}>
49
+ {#if node.inputType === 'text'}
50
+ <Input
51
+ {tui}
52
+ type="text"
53
+ bind:value
54
+ placeholder="Enter text..."
55
+ />
56
+ {:else if node.inputType === 'number'}
57
+ <Input
58
+ {tui}
59
+ type="number"
60
+ bind:value
61
+ min={node.min}
62
+ max={node.max}
63
+ step={node.step}
64
+ />
65
+ {:else if node.inputType === 'checkbox'}
66
+ <Toggle
67
+ {tui}
68
+ bind:checked={value}
69
+ label={node.label}
70
+ />
71
+ {:else if node.inputType === 'slider'}
72
+ <div class="slider-container">
73
+ <Input
74
+ {tui}
75
+ type="range"
76
+ bind:value
77
+ min={node.min ?? 0}
78
+ max={node.max ?? 100}
79
+ step={node.step ?? 1}
80
+ />
81
+ <Text variant={1} class="slider-value">{value}</Text>
82
+ </div>
83
+ {/if}
84
+ </Box>
85
+
86
+ <!-- Output ports -->
87
+ <div class="ports">
88
+ {#each node.outputs as port}
89
+ <div class="port output-port" data-port-id={port.id}>
90
+ <span class="port-label">{port.name}</span>
91
+ </div>
92
+ {/each}
93
+ </div>
94
+ </Box>
95
+
96
+ <style>
97
+ :global(.input-node) {
98
+ min-width: 250px;
99
+ }
100
+
101
+ :global(.input-node .node-header) {
102
+ padding: var(--space-2) var(--space-3);
103
+ border-bottom: 1px solid var(--border-color);
104
+ display: flex;
105
+ align-items: center;
106
+ gap: var(--space-2);
107
+ border-radius: var(--radius-3) var(--radius-3) 0 0;
108
+ }
109
+
110
+ .node-icon {
111
+ font-size: 18px;
112
+ }
113
+
114
+ :global(.input-node .node-title) {
115
+ font-weight: 600;
116
+ font-size: var(--font-size-1);
117
+ }
118
+
119
+ :global(.input-node .node-body) {
120
+ padding: var(--space-3);
121
+ }
122
+
123
+ .slider-container {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: var(--space-2);
127
+ }
128
+
129
+ :global(.input-node .slider-value) {
130
+ text-align: center;
131
+ font-weight: 600;
132
+ color: var(--brand);
133
+ }
134
+
135
+ .ports {
136
+ position: relative;
137
+ }
138
+
139
+ .port {
140
+ position: absolute;
141
+ width: 12px;
142
+ height: 12px;
143
+ background: var(--brand);
144
+ border: 2px solid var(--surface-2);
145
+ border-radius: 50%;
146
+ cursor: crosshair;
147
+ }
148
+
149
+ .output-port {
150
+ right: -8px;
151
+ top: 50%;
152
+ transform: translateY(-50%);
153
+ }
154
+
155
+ .port-label {
156
+ display: none;
157
+ }
158
+ </style>