@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,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
3
|
+
import { onMount, onDestroy } from 'svelte';
|
|
4
|
+
import type { TerminalNode } from '../types/canvas';
|
|
5
|
+
import { updateNodeData } from '../stores/canvas';
|
|
6
|
+
import { captureCommandStart, captureCommandResult, isAgentEnabled } from '../agent/integration';
|
|
7
|
+
import type { TerminalEvent } from '../types/agent';
|
|
8
|
+
import Box from '../design-dojo/Box.svelte';
|
|
9
|
+
import Button from '../design-dojo/Button.svelte';
|
|
10
|
+
import Text from '../design-dojo/Text.svelte';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
node: TerminalNode;
|
|
14
|
+
tui?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { node, tui = false }: Props = $props();
|
|
18
|
+
|
|
19
|
+
let output = $state<string[]>([]);
|
|
20
|
+
let isRunning = $state(false);
|
|
21
|
+
let error = $state<string | null>(null);
|
|
22
|
+
|
|
23
|
+
async function executeCommand() {
|
|
24
|
+
if (isRunning) return;
|
|
25
|
+
|
|
26
|
+
isRunning = true;
|
|
27
|
+
error = null;
|
|
28
|
+
output = [];
|
|
29
|
+
|
|
30
|
+
// Capture command start for agent
|
|
31
|
+
let agentEvent: TerminalEvent | null = null;
|
|
32
|
+
if (isAgentEnabled()) {
|
|
33
|
+
agentEvent = await captureCommandStart(
|
|
34
|
+
node.command,
|
|
35
|
+
node.args || [],
|
|
36
|
+
node.env || {},
|
|
37
|
+
node.cwd || ''
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Call Tauri backend to execute terminal command
|
|
43
|
+
const result = await invoke<string>('execute_terminal_command', {
|
|
44
|
+
command: node.command,
|
|
45
|
+
args: node.args || [],
|
|
46
|
+
env: node.env || {},
|
|
47
|
+
cwd: node.cwd || ''
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
output = [...output, result];
|
|
51
|
+
|
|
52
|
+
// Capture command result for agent
|
|
53
|
+
if (agentEvent) {
|
|
54
|
+
await captureCommandResult(agentEvent, result, '', 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update the node's output data for reactive flow
|
|
58
|
+
if (node.outputs.length > 0) {
|
|
59
|
+
updateNodeData(node.id, node.outputs[0].id, result);
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const errorMsg = String(e);
|
|
63
|
+
error = errorMsg;
|
|
64
|
+
|
|
65
|
+
// Capture error result for agent
|
|
66
|
+
if (agentEvent) {
|
|
67
|
+
await captureCommandResult(agentEvent, '', errorMsg, 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error('Terminal command error:', e);
|
|
71
|
+
} finally {
|
|
72
|
+
isRunning = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clearOutput() {
|
|
77
|
+
output = [];
|
|
78
|
+
error = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onMount(() => {
|
|
82
|
+
if (node.autoStart) {
|
|
83
|
+
executeCommand();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<Box class="terminal-node" surface={2} border radius={3} shadow={2} {tui}>
|
|
89
|
+
<Box class="node-header" surface={3} {tui}>
|
|
90
|
+
<span class="node-icon">⚡</span>
|
|
91
|
+
<Text class="node-title">{node.label || 'Terminal'}</Text>
|
|
92
|
+
</Box>
|
|
93
|
+
|
|
94
|
+
<Box class="node-body" pad={3}>
|
|
95
|
+
<Box class="command-display" surface={1} pad={2} radius={2}>
|
|
96
|
+
<Text mono class="command-text"><code>{node.command} {(node.args || []).join(' ')}</code></Text>
|
|
97
|
+
</Box>
|
|
98
|
+
|
|
99
|
+
<Box class="output-container" surface={1} pad={2} radius={2}>
|
|
100
|
+
{#if output.length > 0}
|
|
101
|
+
{#each output as line}
|
|
102
|
+
<Text mono variant={1} class="output-line">{line}</Text>
|
|
103
|
+
{/each}
|
|
104
|
+
{:else}
|
|
105
|
+
<Text variant={2} class="output-placeholder">No output yet</Text>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
{#if error}
|
|
109
|
+
<Text class="error-line">{error}</Text>
|
|
110
|
+
{/if}
|
|
111
|
+
</Box>
|
|
112
|
+
</Box>
|
|
113
|
+
|
|
114
|
+
<Box class="node-footer" pad={2}>
|
|
115
|
+
<Button {tui} variant="primary" onclick={executeCommand} disabled={isRunning} class="run-btn">
|
|
116
|
+
{isRunning ? '⏳ Running...' : '▶ Run'}
|
|
117
|
+
</Button>
|
|
118
|
+
<Button {tui} onclick={clearOutput} class="clear-btn">Clear</Button>
|
|
119
|
+
</Box>
|
|
120
|
+
|
|
121
|
+
<!-- Input/Output ports -->
|
|
122
|
+
<div class="ports">
|
|
123
|
+
{#each node.inputs as port}
|
|
124
|
+
<div class="port input-port" data-port-id={port.id}>
|
|
125
|
+
<span class="port-label">{port.name}</span>
|
|
126
|
+
</div>
|
|
127
|
+
{/each}
|
|
128
|
+
|
|
129
|
+
{#each node.outputs as port}
|
|
130
|
+
<div class="port output-port" data-port-id={port.id}>
|
|
131
|
+
<span class="port-label">{port.name}</span>
|
|
132
|
+
</div>
|
|
133
|
+
{/each}
|
|
134
|
+
</div>
|
|
135
|
+
</Box>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
:global(.terminal-node) {
|
|
139
|
+
min-width: 300px;
|
|
140
|
+
max-width: 500px;
|
|
141
|
+
font-family: var(--font-mono);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
:global(.terminal-node .node-header) {
|
|
145
|
+
padding: var(--space-2) var(--space-3);
|
|
146
|
+
border-bottom: 1px solid var(--border-color);
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: var(--space-2);
|
|
150
|
+
border-radius: var(--radius-3) var(--radius-3) 0 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.node-icon {
|
|
154
|
+
font-size: 18px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
:global(.terminal-node .node-title) {
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
font-size: var(--font-size-1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
:global(.terminal-node .node-body) {
|
|
163
|
+
padding: var(--space-3);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
:global(.terminal-node .command-display) {
|
|
167
|
+
margin-bottom: var(--space-2);
|
|
168
|
+
font-size: var(--font-size-0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
:global(.terminal-node .command-text) {
|
|
172
|
+
color: var(--brand);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:global(.terminal-node .output-container) {
|
|
176
|
+
max-height: 200px;
|
|
177
|
+
overflow-y: auto;
|
|
178
|
+
min-height: 60px;
|
|
179
|
+
font-size: var(--font-size-0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
:global(.terminal-node .output-line) {
|
|
183
|
+
display: block;
|
|
184
|
+
margin: 2px 0;
|
|
185
|
+
word-break: break-word;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:global(.terminal-node .output-placeholder) {
|
|
189
|
+
font-style: italic;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
:global(.terminal-node .error-line) {
|
|
193
|
+
display: block;
|
|
194
|
+
margin: 2px 0;
|
|
195
|
+
color: var(--error);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:global(.terminal-node .node-footer) {
|
|
199
|
+
border-top: 1px solid var(--border-color);
|
|
200
|
+
display: flex;
|
|
201
|
+
gap: var(--space-2);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
:global(.terminal-node .run-btn) {
|
|
205
|
+
flex: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ports {
|
|
209
|
+
position: relative;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.port {
|
|
213
|
+
position: absolute;
|
|
214
|
+
width: 12px;
|
|
215
|
+
height: 12px;
|
|
216
|
+
background: var(--brand);
|
|
217
|
+
border: 2px solid var(--surface-2);
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
cursor: crosshair;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.input-port {
|
|
223
|
+
left: -8px;
|
|
224
|
+
top: 50%;
|
|
225
|
+
transform: translateY(-50%);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.output-port {
|
|
229
|
+
right: -8px;
|
|
230
|
+
top: 50%;
|
|
231
|
+
transform: translateY(-50%);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.port-label {
|
|
235
|
+
display: none;
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { canvasStore } from '../stores/canvas';
|
|
3
|
+
import { loadCanvasFromFile, saveCanvasToYAML } from '../utils/yaml-loader';
|
|
4
|
+
import { saveCanvas, loadCanvas, listCanvases, useLocalStorage, usePluresDB, getCurrentAdapter } from '../utils/storage';
|
|
5
|
+
import type { TerminalNode, InputNode, DisplayNode, TransformNode } from '../types/canvas';
|
|
6
|
+
import StatusBar from '../design-dojo/StatusBar.svelte';
|
|
7
|
+
import Button from '../design-dojo/Button.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
tui?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { tui = false }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let savedCanvases = $state<{ id: string; name: string; timestamp: number }[]>([]);
|
|
16
|
+
let showSavedList = $state(false);
|
|
17
|
+
let showStorageSettings = $state(false);
|
|
18
|
+
let currentStorageType = $state<'localStorage' | 'pluresdb'>('localStorage');
|
|
19
|
+
|
|
20
|
+
// Load list of saved canvases on mount
|
|
21
|
+
$effect(() => {
|
|
22
|
+
listCanvases().then(list => {
|
|
23
|
+
savedCanvases = list;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function addTerminalNode() {
|
|
28
|
+
const node: TerminalNode = {
|
|
29
|
+
id: `terminal-${Date.now()}`,
|
|
30
|
+
type: 'terminal',
|
|
31
|
+
position: { x: 100, y: 100 },
|
|
32
|
+
label: 'Terminal',
|
|
33
|
+
command: 'echo',
|
|
34
|
+
args: ['Hello, RuneBook!'],
|
|
35
|
+
autoStart: false,
|
|
36
|
+
inputs: [],
|
|
37
|
+
outputs: [{ id: 'stdout', name: 'stdout', type: 'output' }]
|
|
38
|
+
};
|
|
39
|
+
canvasStore.addNode(node);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addInputNode() {
|
|
43
|
+
const node: InputNode = {
|
|
44
|
+
id: `input-${Date.now()}`,
|
|
45
|
+
type: 'input',
|
|
46
|
+
position: { x: 100, y: 300 },
|
|
47
|
+
label: 'Text Input',
|
|
48
|
+
inputType: 'text',
|
|
49
|
+
value: '',
|
|
50
|
+
inputs: [],
|
|
51
|
+
outputs: [{ id: 'value', name: 'value', type: 'output' }]
|
|
52
|
+
};
|
|
53
|
+
canvasStore.addNode(node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addDisplayNode() {
|
|
57
|
+
const node: DisplayNode = {
|
|
58
|
+
id: `display-${Date.now()}`,
|
|
59
|
+
type: 'display',
|
|
60
|
+
position: { x: 500, y: 200 },
|
|
61
|
+
label: 'Display',
|
|
62
|
+
displayType: 'text',
|
|
63
|
+
content: '',
|
|
64
|
+
inputs: [{ id: 'input', name: 'input', type: 'input' }],
|
|
65
|
+
outputs: []
|
|
66
|
+
};
|
|
67
|
+
canvasStore.addNode(node);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addTransformNode() {
|
|
71
|
+
const node: TransformNode = {
|
|
72
|
+
id: `transform-${Date.now()}`,
|
|
73
|
+
type: 'transform',
|
|
74
|
+
position: { x: 300, y: 200 },
|
|
75
|
+
label: 'Transform',
|
|
76
|
+
transformType: 'map',
|
|
77
|
+
code: 'item',
|
|
78
|
+
inputs: [{ id: 'input', name: 'input', type: 'input' }],
|
|
79
|
+
outputs: [{ id: 'output', name: 'output', type: 'output' }]
|
|
80
|
+
};
|
|
81
|
+
canvasStore.addNode(node);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadExample() {
|
|
85
|
+
try {
|
|
86
|
+
const canvas = await loadCanvasFromFile('/examples/hello-world.yaml');
|
|
87
|
+
canvasStore.loadCanvas(canvas);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to load example:', error);
|
|
90
|
+
alert('Failed to load example canvas');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function saveCanvasToStorage() {
|
|
95
|
+
try {
|
|
96
|
+
const canvas = $canvasStore;
|
|
97
|
+
await saveCanvas(canvas);
|
|
98
|
+
// Refresh the list
|
|
99
|
+
savedCanvases = await listCanvases();
|
|
100
|
+
alert(`Canvas "${canvas.name}" saved successfully!`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Failed to save canvas:', error);
|
|
103
|
+
alert('Failed to save canvas');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadCanvasFromStorage(id: string) {
|
|
108
|
+
try {
|
|
109
|
+
const canvas = await loadCanvas(id);
|
|
110
|
+
if (canvas) {
|
|
111
|
+
canvasStore.loadCanvas(canvas);
|
|
112
|
+
showSavedList = false;
|
|
113
|
+
} else {
|
|
114
|
+
alert('Canvas not found');
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Failed to load canvas:', error);
|
|
118
|
+
alert('Failed to load canvas');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function toggleSavedList() {
|
|
123
|
+
showSavedList = !showSavedList;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toggleStorageSettings() {
|
|
127
|
+
showStorageSettings = !showStorageSettings;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function switchStorageType(type: 'localStorage' | 'pluresdb') {
|
|
131
|
+
try {
|
|
132
|
+
if (type === 'pluresdb') {
|
|
133
|
+
usePluresDB();
|
|
134
|
+
currentStorageType = 'pluresdb';
|
|
135
|
+
alert('Switched to PluresDB storage. Make sure PluresDB server is running.');
|
|
136
|
+
} else {
|
|
137
|
+
useLocalStorage();
|
|
138
|
+
currentStorageType = 'localStorage';
|
|
139
|
+
alert('Switched to LocalStorage (browser storage).');
|
|
140
|
+
}
|
|
141
|
+
// Refresh the list
|
|
142
|
+
savedCanvases = await listCanvases();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to switch storage type:', error);
|
|
145
|
+
alert('Failed to switch storage type. Using LocalStorage as fallback.');
|
|
146
|
+
useLocalStorage();
|
|
147
|
+
currentStorageType = 'localStorage';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function saveCanvasToFile() {
|
|
152
|
+
const canvas = $canvasStore;
|
|
153
|
+
const yaml = saveCanvasToYAML(canvas);
|
|
154
|
+
|
|
155
|
+
// Create a download link
|
|
156
|
+
const blob = new Blob([yaml], { type: 'text/yaml' });
|
|
157
|
+
const url = URL.createObjectURL(blob);
|
|
158
|
+
const a = document.createElement('a');
|
|
159
|
+
a.href = url;
|
|
160
|
+
a.download = `${canvas.name.replace(/\s+/g, '-').toLowerCase()}.yaml`;
|
|
161
|
+
a.click();
|
|
162
|
+
URL.revokeObjectURL(url);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function clearCanvas() {
|
|
166
|
+
if (confirm('Are you sure you want to clear the canvas?')) {
|
|
167
|
+
canvasStore.clear();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<StatusBar {tui}>
|
|
173
|
+
<div class="toolbar-inner">
|
|
174
|
+
<div class="toolbar-section">
|
|
175
|
+
<h3>Add Nodes</h3>
|
|
176
|
+
<Button {tui} onclick={addTerminalNode} class="toolbar-btn">
|
|
177
|
+
⚡ Terminal
|
|
178
|
+
</Button>
|
|
179
|
+
<Button {tui} onclick={addInputNode} class="toolbar-btn">
|
|
180
|
+
📝 Input
|
|
181
|
+
</Button>
|
|
182
|
+
<Button {tui} onclick={addDisplayNode} class="toolbar-btn">
|
|
183
|
+
📊 Display
|
|
184
|
+
</Button>
|
|
185
|
+
<Button {tui} onclick={addTransformNode} class="toolbar-btn">
|
|
186
|
+
🔄 Transform
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="toolbar-section">
|
|
191
|
+
<h3>Canvas</h3>
|
|
192
|
+
<Button {tui} onclick={loadExample} class="toolbar-btn">
|
|
193
|
+
📂 Load Example
|
|
194
|
+
</Button>
|
|
195
|
+
<Button {tui} onclick={saveCanvasToStorage} class="toolbar-btn">
|
|
196
|
+
💾 Save to Storage
|
|
197
|
+
</Button>
|
|
198
|
+
<Button {tui} onclick={toggleSavedList} class="toolbar-btn">
|
|
199
|
+
📚 Saved Canvases {showSavedList ? '▼' : '▶'}
|
|
200
|
+
</Button>
|
|
201
|
+
{#if showSavedList}
|
|
202
|
+
<div class="saved-list">
|
|
203
|
+
{#if savedCanvases.length === 0}
|
|
204
|
+
<div class="empty-message">No saved canvases</div>
|
|
205
|
+
{:else}
|
|
206
|
+
{#each savedCanvases as saved}
|
|
207
|
+
<button
|
|
208
|
+
onclick={() => loadCanvasFromStorage(saved.id)}
|
|
209
|
+
class="saved-item"
|
|
210
|
+
>
|
|
211
|
+
{saved.name}
|
|
212
|
+
<span class="saved-time">
|
|
213
|
+
{new Date(saved.timestamp).toLocaleDateString()}
|
|
214
|
+
</span>
|
|
215
|
+
</button>
|
|
216
|
+
{/each}
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
219
|
+
{/if}
|
|
220
|
+
<Button {tui} onclick={saveCanvasToFile} class="toolbar-btn">
|
|
221
|
+
📥 Export YAML
|
|
222
|
+
</Button>
|
|
223
|
+
<Button {tui} onclick={toggleStorageSettings} class="toolbar-btn">
|
|
224
|
+
⚙️ Storage Settings {showStorageSettings ? '▼' : '▶'}
|
|
225
|
+
</Button>
|
|
226
|
+
{#if showStorageSettings}
|
|
227
|
+
<div class="storage-settings">
|
|
228
|
+
<label class="storage-option">
|
|
229
|
+
<input
|
|
230
|
+
type="radio"
|
|
231
|
+
name="storage"
|
|
232
|
+
value="localStorage"
|
|
233
|
+
checked={currentStorageType === 'localStorage'}
|
|
234
|
+
onchange={() => switchStorageType('localStorage')}
|
|
235
|
+
/>
|
|
236
|
+
Browser Storage
|
|
237
|
+
</label>
|
|
238
|
+
<label class="storage-option">
|
|
239
|
+
<input
|
|
240
|
+
type="radio"
|
|
241
|
+
name="storage"
|
|
242
|
+
value="pluresdb"
|
|
243
|
+
checked={currentStorageType === 'pluresdb'}
|
|
244
|
+
onchange={() => switchStorageType('pluresdb')}
|
|
245
|
+
/>
|
|
246
|
+
PluresDB (P2P)
|
|
247
|
+
</label>
|
|
248
|
+
<div class="storage-info">
|
|
249
|
+
Current: {currentStorageType === 'pluresdb' ? 'PluresDB' : 'Browser Storage'}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
<Button {tui} variant="danger" onclick={clearCanvas} class="toolbar-btn">
|
|
254
|
+
🗑️ Clear
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</StatusBar>
|
|
259
|
+
|
|
260
|
+
<style>
|
|
261
|
+
.toolbar-inner {
|
|
262
|
+
padding: var(--space-3);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.toolbar-section {
|
|
266
|
+
margin-bottom: var(--space-5);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.toolbar-section h3 {
|
|
270
|
+
color: var(--text-2);
|
|
271
|
+
font-size: var(--font-size-0);
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
margin: 0 0 var(--space-3) 0;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
:global(.toolbar-btn) {
|
|
278
|
+
width: 100%;
|
|
279
|
+
margin-bottom: var(--space-2);
|
|
280
|
+
text-align: left;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.saved-list {
|
|
284
|
+
margin-top: var(--space-2);
|
|
285
|
+
margin-bottom: var(--space-2);
|
|
286
|
+
max-height: 200px;
|
|
287
|
+
overflow-y: auto;
|
|
288
|
+
border: 1px solid var(--border-color);
|
|
289
|
+
border-radius: var(--radius-2);
|
|
290
|
+
background: var(--surface-1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.saved-item {
|
|
294
|
+
width: 100%;
|
|
295
|
+
padding: var(--space-2) var(--space-3);
|
|
296
|
+
background: transparent;
|
|
297
|
+
color: var(--text-1);
|
|
298
|
+
border: none;
|
|
299
|
+
border-bottom: 1px solid var(--border-color);
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
font-size: var(--font-size-0);
|
|
302
|
+
text-align: left;
|
|
303
|
+
transition: background-color var(--transition-base);
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
gap: var(--space-1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.saved-item:last-child {
|
|
310
|
+
border-bottom: none;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.saved-item:hover {
|
|
314
|
+
background: var(--surface-2);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.saved-time {
|
|
318
|
+
font-size: var(--font-size-0);
|
|
319
|
+
color: var(--text-3);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.empty-message {
|
|
323
|
+
padding: var(--space-3);
|
|
324
|
+
text-align: center;
|
|
325
|
+
color: var(--text-3);
|
|
326
|
+
font-size: var(--font-size-0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.storage-settings {
|
|
330
|
+
margin-top: var(--space-2);
|
|
331
|
+
margin-bottom: var(--space-2);
|
|
332
|
+
padding: var(--space-2);
|
|
333
|
+
border: 1px solid var(--border-color);
|
|
334
|
+
border-radius: var(--radius-2);
|
|
335
|
+
background: var(--surface-1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.storage-option {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: var(--space-2);
|
|
342
|
+
padding: var(--space-2) var(--space-1);
|
|
343
|
+
color: var(--text-1);
|
|
344
|
+
font-size: var(--font-size-0);
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.storage-option input[type="radio"] {
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.storage-info {
|
|
353
|
+
margin-top: var(--space-2);
|
|
354
|
+
padding-top: var(--space-2);
|
|
355
|
+
border-top: 1px solid var(--border-color);
|
|
356
|
+
font-size: var(--font-size-0);
|
|
357
|
+
color: var(--brand);
|
|
358
|
+
}
|
|
359
|
+
</style>
|