@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.
- package/ANALYSIS_LADDER.md +231 -0
- package/CHANGELOG.md +124 -0
- package/INTEGRATIONS.md +242 -0
- package/LICENSE +21 -0
- package/MEMORY.md +253 -0
- package/NIXOS.md +357 -0
- package/QUICKSTART.md +157 -0
- package/README.md +295 -0
- package/RELEASE.md +190 -0
- package/ValidationChecklist.md +598 -0
- package/docs/demo.md +338 -0
- package/docs/llm-integration.md +300 -0
- package/docs/parallel-execution-plan.md +160 -0
- package/flake.nix +228 -0
- package/integrations/README.md +242 -0
- package/integrations/demo-steps.sh +64 -0
- package/integrations/nvim-runebook.lua +140 -0
- package/integrations/tmux-status.sh +51 -0
- package/integrations/vim-runebook.vim +77 -0
- package/integrations/wezterm-status-simple.lua +48 -0
- package/integrations/wezterm-status.lua +76 -0
- package/nixos-module.nix +156 -0
- package/package.json +76 -0
- package/packages/design-dojo/index.js +4 -0
- package/packages/design-dojo/package.json +20 -0
- package/packages/design-dojo/tokens.css +69 -0
- package/playwright.config.ts +16 -0
- package/scripts/check-versions.cjs +62 -0
- package/scripts/demo.sh +220 -0
- package/shell.nix +31 -0
- package/src/app.html +13 -0
- package/src/cli/index.ts +1050 -0
- package/src/lib/agent/analysis-pipeline.ts +347 -0
- package/src/lib/agent/analysis-service.ts +171 -0
- package/src/lib/agent/analysis.ts +159 -0
- package/src/lib/agent/analyzers/heuristic.ts +289 -0
- package/src/lib/agent/analyzers/index.ts +7 -0
- package/src/lib/agent/analyzers/llm.ts +204 -0
- package/src/lib/agent/analyzers/local-search.ts +215 -0
- package/src/lib/agent/capture.ts +123 -0
- package/src/lib/agent/index.ts +244 -0
- package/src/lib/agent/integration.ts +81 -0
- package/src/lib/agent/llm/providers/base.ts +99 -0
- package/src/lib/agent/llm/providers/index.ts +60 -0
- package/src/lib/agent/llm/providers/mock.ts +67 -0
- package/src/lib/agent/llm/providers/ollama.ts +151 -0
- package/src/lib/agent/llm/providers/openai.ts +153 -0
- package/src/lib/agent/llm/sanitizer.ts +170 -0
- package/src/lib/agent/llm/types.ts +118 -0
- package/src/lib/agent/memory.ts +363 -0
- package/src/lib/agent/node-status.ts +56 -0
- package/src/lib/agent/node-suggestions.ts +64 -0
- package/src/lib/agent/status.ts +80 -0
- package/src/lib/agent/suggestions.ts +169 -0
- package/src/lib/components/Canvas.svelte +124 -0
- package/src/lib/components/ConnectionLine.svelte +46 -0
- package/src/lib/components/DisplayNode.svelte +167 -0
- package/src/lib/components/InputNode.svelte +158 -0
- package/src/lib/components/TerminalNode.svelte +237 -0
- package/src/lib/components/Toolbar.svelte +359 -0
- package/src/lib/components/TransformNode.svelte +327 -0
- package/src/lib/core/index.ts +31 -0
- package/src/lib/core/observer.ts +278 -0
- package/src/lib/core/redaction.ts +158 -0
- package/src/lib/core/shell-adapters/base.ts +325 -0
- package/src/lib/core/shell-adapters/bash.ts +110 -0
- package/src/lib/core/shell-adapters/index.ts +62 -0
- package/src/lib/core/shell-adapters/zsh.ts +105 -0
- package/src/lib/core/storage.ts +360 -0
- package/src/lib/core/types.ts +176 -0
- package/src/lib/design-dojo/Box.svelte +47 -0
- package/src/lib/design-dojo/Button.svelte +75 -0
- package/src/lib/design-dojo/Input.svelte +65 -0
- package/src/lib/design-dojo/List.svelte +38 -0
- package/src/lib/design-dojo/Select.svelte +48 -0
- package/src/lib/design-dojo/SplitPane.svelte +43 -0
- package/src/lib/design-dojo/StatusBar.svelte +61 -0
- package/src/lib/design-dojo/Table.svelte +47 -0
- package/src/lib/design-dojo/Text.svelte +36 -0
- package/src/lib/design-dojo/Toggle.svelte +48 -0
- package/src/lib/design-dojo/index.ts +10 -0
- package/src/lib/stores/canvas-praxis.ts +268 -0
- package/src/lib/stores/canvas.ts +58 -0
- package/src/lib/types/agent.ts +78 -0
- package/src/lib/types/canvas.ts +71 -0
- package/src/lib/utils/storage.ts +326 -0
- package/src/lib/utils/yaml-loader.ts +52 -0
- package/src/routes/+layout.svelte +5 -0
- package/src/routes/+layout.ts +5 -0
- package/src/routes/+page.svelte +32 -0
- package/src-tauri/Cargo.lock +5735 -0
- package/src-tauri/Cargo.toml +38 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +10 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/agents/agent1.rs +66 -0
- package/src-tauri/src/agents/agent2.rs +80 -0
- package/src-tauri/src/agents/agent3.rs +73 -0
- package/src-tauri/src/agents/agent4.rs +66 -0
- package/src-tauri/src/agents/agent5.rs +68 -0
- package/src-tauri/src/agents/agent6.rs +75 -0
- package/src-tauri/src/agents/base.rs +52 -0
- package/src-tauri/src/agents/mod.rs +17 -0
- package/src-tauri/src/core/coordination.rs +117 -0
- package/src-tauri/src/core/mod.rs +12 -0
- package/src-tauri/src/core/ownership.rs +61 -0
- package/src-tauri/src/core/types.rs +132 -0
- package/src-tauri/src/execution/mod.rs +5 -0
- package/src-tauri/src/execution/runner.rs +143 -0
- package/src-tauri/src/lib.rs +161 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/src/memory/api.rs +422 -0
- package/src-tauri/src/memory/client.rs +156 -0
- package/src-tauri/src/memory/encryption.rs +79 -0
- package/src-tauri/src/memory/migration.rs +110 -0
- package/src-tauri/src/memory/mod.rs +28 -0
- package/src-tauri/src/memory/schema.rs +275 -0
- package/src-tauri/src/memory/tests.rs +192 -0
- package/src-tauri/src/orchestrator/coordinator.rs +232 -0
- package/src-tauri/src/orchestrator/mod.rs +13 -0
- package/src-tauri/src/orchestrator/planner.rs +304 -0
- package/src-tauri/tauri.conf.json +35 -0
- package/static/examples/date-time-example.yaml +147 -0
- package/static/examples/hello-world.yaml +74 -0
- package/static/examples/transform-example.yaml +157 -0
- package/static/favicon.png +0 -0
- package/static/svelte.svg +1 -0
- package/static/tauri.svg +6 -0
- package/static/vite.svg +1 -0
- package/svelte.config.js +18 -0
- package/tsconfig.json +19 -0
- package/vite.config.js +45 -0
- 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>
|