synabun 1.3.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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Workspaces
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Save/load full scene snapshots: open cards, graph node positions,
|
|
6
|
+
// camera state (3D), active category filters, and selection.
|
|
7
|
+
// Floating overlay in top-right of the canvas.
|
|
8
|
+
// Export/import as JSON for backup.
|
|
9
|
+
//
|
|
10
|
+
// Events emitted:
|
|
11
|
+
// workspace:get-scene — request variant to provide { nodePositions, camera }
|
|
12
|
+
// workspace:restore-scene — tell variant to restore { nodePositions, camera }
|
|
13
|
+
// workspace:loaded — after a workspace has been fully restored
|
|
14
|
+
// graph:refresh — re-filter graph after category change
|
|
15
|
+
//
|
|
16
|
+
// Events listened:
|
|
17
|
+
// panel:close-all-dropdowns — close workspace dropdown
|
|
18
|
+
// viz:toggled — dim overlay when viz is off
|
|
19
|
+
|
|
20
|
+
import { state, emit, on } from './state.js';
|
|
21
|
+
import { KEYS } from './constants.js';
|
|
22
|
+
import { storage } from './storage.js';
|
|
23
|
+
import { getVariant } from './registry.js';
|
|
24
|
+
import {
|
|
25
|
+
getOpenCardsSnapshot,
|
|
26
|
+
closeAllCards,
|
|
27
|
+
openMemoryCard,
|
|
28
|
+
} from './ui-detail.js';
|
|
29
|
+
import {
|
|
30
|
+
getTerminalSnapshot,
|
|
31
|
+
restoreTerminalSnapshot,
|
|
32
|
+
tileFloatingTerminals,
|
|
33
|
+
} from './ui-terminal.js';
|
|
34
|
+
import {
|
|
35
|
+
getWhiteboardSnapshot,
|
|
36
|
+
restoreWhiteboardSnapshot,
|
|
37
|
+
clearWhiteboard,
|
|
38
|
+
} from './ui-whiteboard.js';
|
|
39
|
+
|
|
40
|
+
const $ = (id) => document.getElementById(id);
|
|
41
|
+
|
|
42
|
+
let _isOpen = false;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// ═══════════════════════════════════════════
|
|
46
|
+
// STORAGE
|
|
47
|
+
// ═══════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
function getWorkspaces() {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(storage.getItem(KEYS.WORKSPACES) || '[]');
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveWorkspacesToStorage(list) {
|
|
58
|
+
storage.setItem(KEYS.WORKSPACES, JSON.stringify(list));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _readStoredPositions(key) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(storage.getItem(key) || '{}');
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getActiveId() {
|
|
70
|
+
try {
|
|
71
|
+
return sessionStorage.getItem(KEYS.ACTIVE_WORKSPACE) || null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setActiveId(id) {
|
|
78
|
+
try {
|
|
79
|
+
if (id) {
|
|
80
|
+
sessionStorage.setItem(KEYS.ACTIVE_WORKSPACE, id);
|
|
81
|
+
} else {
|
|
82
|
+
sessionStorage.removeItem(KEYS.ACTIVE_WORKSPACE);
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════
|
|
89
|
+
// CORE — SAVE
|
|
90
|
+
// ═══════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
function saveWorkspace(name) {
|
|
93
|
+
if (!name || !name.trim()) return;
|
|
94
|
+
name = name.trim();
|
|
95
|
+
|
|
96
|
+
const currentVariant = getVariant() || '3d';
|
|
97
|
+
|
|
98
|
+
// Request scene data from the active variant
|
|
99
|
+
emit('workspace:get-scene', (sceneData) => {
|
|
100
|
+
const { nodePositions, camera } = sceneData || {};
|
|
101
|
+
|
|
102
|
+
const cards = getOpenCardsSnapshot();
|
|
103
|
+
const terminal = getTerminalSnapshot();
|
|
104
|
+
const whiteboard = getWhiteboardSnapshot();
|
|
105
|
+
|
|
106
|
+
// Check if updating an existing workspace — preserve other variant's positions
|
|
107
|
+
const list = getWorkspaces();
|
|
108
|
+
const existingIdx = list.findIndex(w => w.name === name);
|
|
109
|
+
const existing = existingIdx >= 0 ? list[existingIdx] : null;
|
|
110
|
+
|
|
111
|
+
// Store positions per-variant so 2D and 3D layouts are independent.
|
|
112
|
+
// Active variant: captured from scene. Inactive variant: preserved from
|
|
113
|
+
// existing workspace, or read from persistent storage for new workspaces.
|
|
114
|
+
const pos2d = currentVariant === '2d'
|
|
115
|
+
? (nodePositions || {})
|
|
116
|
+
: (existing?.nodePositions2d || _readStoredPositions(KEYS.NODE_POS_2D));
|
|
117
|
+
const pos3d = currentVariant === '3d'
|
|
118
|
+
? (nodePositions || {})
|
|
119
|
+
: (existing?.nodePositions3d || _readStoredPositions(KEYS.NODE_POS_3D));
|
|
120
|
+
|
|
121
|
+
const workspace = {
|
|
122
|
+
id: String(Date.now()),
|
|
123
|
+
name,
|
|
124
|
+
created: new Date().toISOString(),
|
|
125
|
+
updated: new Date().toISOString(),
|
|
126
|
+
variant: currentVariant,
|
|
127
|
+
cards,
|
|
128
|
+
terminal,
|
|
129
|
+
whiteboard,
|
|
130
|
+
nodePositions2d: pos2d,
|
|
131
|
+
nodePositions3d: pos3d,
|
|
132
|
+
camera: currentVariant === '3d' ? (camera || null) : (existing?.camera || null),
|
|
133
|
+
activeCategories: [...state.activeCategories],
|
|
134
|
+
selectedNodeId: state.selectedNodeId,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (existingIdx >= 0) {
|
|
138
|
+
workspace.id = existing.id;
|
|
139
|
+
workspace.created = existing.created;
|
|
140
|
+
list[existingIdx] = workspace;
|
|
141
|
+
} else {
|
|
142
|
+
list.unshift(workspace);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveWorkspacesToStorage(list);
|
|
146
|
+
setActiveId(workspace.id);
|
|
147
|
+
updateIndicator();
|
|
148
|
+
renderList();
|
|
149
|
+
|
|
150
|
+
// Clear input
|
|
151
|
+
const input = $('ws-name-input');
|
|
152
|
+
if (input) input.value = '';
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// ═══════════════════════════════════════════
|
|
158
|
+
// CORE — LOAD
|
|
159
|
+
// ═══════════════════════════════════════════
|
|
160
|
+
|
|
161
|
+
function loadWorkspace(workspace) {
|
|
162
|
+
// Close all current cards
|
|
163
|
+
closeAllCards();
|
|
164
|
+
|
|
165
|
+
const currentVariant = getVariant() || '3d';
|
|
166
|
+
|
|
167
|
+
// Pick positions for the active variant only.
|
|
168
|
+
// Legacy workspaces have a single nodePositions — use workspace.variant to assign.
|
|
169
|
+
let positions;
|
|
170
|
+
if (currentVariant === '2d') {
|
|
171
|
+
positions = workspace.nodePositions2d
|
|
172
|
+
|| (workspace.variant === '2d' ? workspace.nodePositions : null)
|
|
173
|
+
|| {};
|
|
174
|
+
} else {
|
|
175
|
+
positions = workspace.nodePositions3d
|
|
176
|
+
|| (workspace.variant === '3d' ? workspace.nodePositions : null)
|
|
177
|
+
|| {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Wait for close animation, then restore
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
// Restore scene — only positions matching the active variant
|
|
183
|
+
emit('workspace:restore-scene', {
|
|
184
|
+
nodePositions: positions,
|
|
185
|
+
camera: currentVariant === '3d' ? (workspace.camera || null) : null,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Restore active categories
|
|
189
|
+
if (workspace.activeCategories && workspace.activeCategories.length > 0) {
|
|
190
|
+
state.activeCategories = new Set(workspace.activeCategories);
|
|
191
|
+
emit('graph:refresh');
|
|
192
|
+
emit('sidebar:rebuild');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Restore open cards
|
|
196
|
+
if (workspace.cards && workspace.cards.length > 0) {
|
|
197
|
+
for (const card of workspace.cards) {
|
|
198
|
+
const node = state.allNodes.find(n => n.id === card.memoryId);
|
|
199
|
+
if (node) {
|
|
200
|
+
openMemoryCard(node, {
|
|
201
|
+
left: card.left,
|
|
202
|
+
top: card.top,
|
|
203
|
+
width: card.width,
|
|
204
|
+
height: card.height,
|
|
205
|
+
isCompact: card.isCompact,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Restore selection
|
|
212
|
+
if (workspace.selectedNodeId) {
|
|
213
|
+
state.selectedNodeId = workspace.selectedNodeId;
|
|
214
|
+
storage.setItem(KEYS.SELECTED_NODE, workspace.selectedNodeId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Restore terminal state (position, detached/docked, floating tab positions)
|
|
218
|
+
if (workspace.terminal) {
|
|
219
|
+
restoreTerminalSnapshot(workspace.terminal);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Restore whiteboard state (pass null for old workspaces — clears the board)
|
|
223
|
+
restoreWhiteboardSnapshot(workspace.whiteboard || null);
|
|
224
|
+
|
|
225
|
+
setActiveId(workspace.id);
|
|
226
|
+
updateIndicator();
|
|
227
|
+
emit('workspace:loaded', workspace);
|
|
228
|
+
}, 250);
|
|
229
|
+
|
|
230
|
+
closeDropdown();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
// ═══════════════════════════════════════════
|
|
235
|
+
// CORE — CLEAR WORKSPACE (with confirmation)
|
|
236
|
+
// ═══════════════════════════════════════════
|
|
237
|
+
|
|
238
|
+
function performClear() {
|
|
239
|
+
closeAllCards();
|
|
240
|
+
clearWhiteboard();
|
|
241
|
+
setActiveId(null);
|
|
242
|
+
updateIndicator();
|
|
243
|
+
emit('layout:reset');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function showClearDialog() {
|
|
247
|
+
closeDropdown();
|
|
248
|
+
|
|
249
|
+
// Build overlay + modal using the tag-delete pattern
|
|
250
|
+
const overlay = document.createElement('div');
|
|
251
|
+
overlay.className = 'tag-delete-overlay';
|
|
252
|
+
|
|
253
|
+
const autoName = 'Layout ' + new Date().toLocaleString('en-US', {
|
|
254
|
+
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
overlay.innerHTML = `
|
|
258
|
+
<div class="tag-delete-modal" style="min-width:300px;">
|
|
259
|
+
<div class="tag-delete-modal-title">Clear workspace?</div>
|
|
260
|
+
<div style="color:var(--t-muted);font-size:12px;margin-bottom:14px;">
|
|
261
|
+
This will close all cards and reset the layout.
|
|
262
|
+
</div>
|
|
263
|
+
<div class="ws-clear-save-row">
|
|
264
|
+
<input type="text" id="ws-clear-name" placeholder="${autoName}" autocomplete="off" spellcheck="false">
|
|
265
|
+
<button id="ws-clear-save-btn" class="ws-clear-action save" title="Save current layout, then clear">Save & Clear</button>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="tag-delete-modal-actions" style="margin-top:10px;">
|
|
268
|
+
<button id="ws-clear-discard" class="ws-clear-action discard">Clear without saving</button>
|
|
269
|
+
<button id="ws-clear-cancel" class="ws-clear-action cancel">Cancel</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
document.body.appendChild(overlay);
|
|
275
|
+
|
|
276
|
+
const nameInput = overlay.querySelector('#ws-clear-name');
|
|
277
|
+
const saveBtn = overlay.querySelector('#ws-clear-save-btn');
|
|
278
|
+
const discardBtn = overlay.querySelector('#ws-clear-discard');
|
|
279
|
+
const cancelBtn = overlay.querySelector('#ws-clear-cancel');
|
|
280
|
+
|
|
281
|
+
const dismiss = () => {
|
|
282
|
+
overlay.style.opacity = '0';
|
|
283
|
+
setTimeout(() => overlay.remove(), 150);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Save & Clear
|
|
287
|
+
saveBtn.addEventListener('click', () => {
|
|
288
|
+
const name = (nameInput.value.trim()) || autoName;
|
|
289
|
+
saveWorkspace(name);
|
|
290
|
+
// Small delay so save completes before clearing
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
performClear();
|
|
293
|
+
dismiss();
|
|
294
|
+
}, 100);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Clear without saving
|
|
298
|
+
discardBtn.addEventListener('click', () => {
|
|
299
|
+
performClear();
|
|
300
|
+
dismiss();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Cancel
|
|
304
|
+
cancelBtn.addEventListener('click', dismiss);
|
|
305
|
+
|
|
306
|
+
// Overlay click = cancel
|
|
307
|
+
overlay.addEventListener('click', (e) => {
|
|
308
|
+
if (e.target === overlay) dismiss();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Keyboard
|
|
312
|
+
const onKey = (e) => {
|
|
313
|
+
if (e.key === 'Escape') {
|
|
314
|
+
dismiss();
|
|
315
|
+
document.removeEventListener('keydown', onKey);
|
|
316
|
+
} else if (e.key === 'Enter' && document.activeElement === nameInput) {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
saveBtn.click();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
document.addEventListener('keydown', onKey);
|
|
322
|
+
|
|
323
|
+
// Focus the name input
|
|
324
|
+
requestAnimationFrame(() => nameInput.focus());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// ═══════════════════════════════════════════
|
|
329
|
+
// CORE — DELETE / RENAME
|
|
330
|
+
// ═══════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
function deleteWorkspace(id) {
|
|
333
|
+
const list = getWorkspaces().filter(w => w.id !== id);
|
|
334
|
+
saveWorkspacesToStorage(list);
|
|
335
|
+
if (getActiveId() === id) {
|
|
336
|
+
setActiveId(null);
|
|
337
|
+
updateIndicator();
|
|
338
|
+
}
|
|
339
|
+
renderList();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function confirmDeleteWorkspace(id, name) {
|
|
343
|
+
const overlay = document.createElement('div');
|
|
344
|
+
overlay.className = 'tag-delete-overlay';
|
|
345
|
+
|
|
346
|
+
overlay.innerHTML = `
|
|
347
|
+
<div class="tag-delete-modal" style="min-width:280px;">
|
|
348
|
+
<div class="tag-delete-modal-title">Delete workspace?</div>
|
|
349
|
+
<div style="color:var(--t-bright);font-size:13px;font-weight:600;margin-bottom:6px;">${name}</div>
|
|
350
|
+
<div style="color:var(--t-muted);font-size:12px;margin-bottom:16px;">
|
|
351
|
+
This cannot be undone.
|
|
352
|
+
</div>
|
|
353
|
+
<div class="tag-delete-modal-actions">
|
|
354
|
+
<button class="ws-clear-action discard" id="ws-confirm-delete">Delete</button>
|
|
355
|
+
<button class="ws-clear-action" id="ws-confirm-cancel">Cancel</button>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
document.body.appendChild(overlay);
|
|
361
|
+
|
|
362
|
+
const deleteBtn = overlay.querySelector('#ws-confirm-delete');
|
|
363
|
+
const cancelBtn = overlay.querySelector('#ws-confirm-cancel');
|
|
364
|
+
|
|
365
|
+
const dismiss = () => {
|
|
366
|
+
overlay.style.opacity = '0';
|
|
367
|
+
setTimeout(() => overlay.remove(), 150);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
deleteBtn.addEventListener('click', () => {
|
|
371
|
+
dismiss();
|
|
372
|
+
deleteWorkspace(id);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
cancelBtn.addEventListener('click', dismiss);
|
|
376
|
+
|
|
377
|
+
overlay.addEventListener('click', (e) => {
|
|
378
|
+
if (e.target === overlay) dismiss();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const onKey = (e) => {
|
|
382
|
+
if (e.key === 'Escape') { dismiss(); document.removeEventListener('keydown', onKey); }
|
|
383
|
+
};
|
|
384
|
+
document.addEventListener('keydown', onKey);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function renameWorkspace(id, newName) {
|
|
388
|
+
if (!newName || !newName.trim()) return;
|
|
389
|
+
const list = getWorkspaces();
|
|
390
|
+
const ws = list.find(w => w.id === id);
|
|
391
|
+
if (ws) {
|
|
392
|
+
ws.name = newName.trim();
|
|
393
|
+
ws.updated = new Date().toISOString();
|
|
394
|
+
saveWorkspacesToStorage(list);
|
|
395
|
+
updateIndicator();
|
|
396
|
+
renderList();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
// ═══════════════════════════════════════════
|
|
402
|
+
// BACKUP — EXPORT / IMPORT
|
|
403
|
+
// ═══════════════════════════════════════════
|
|
404
|
+
|
|
405
|
+
function exportWorkspaces() {
|
|
406
|
+
const workspaces = getWorkspaces();
|
|
407
|
+
if (workspaces.length === 0) return;
|
|
408
|
+
|
|
409
|
+
const envelope = {
|
|
410
|
+
version: 1,
|
|
411
|
+
exported: new Date().toISOString(),
|
|
412
|
+
workspaces,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const blob = new Blob([JSON.stringify(envelope, null, 2)], { type: 'application/json' });
|
|
416
|
+
const url = URL.createObjectURL(blob);
|
|
417
|
+
const a = document.createElement('a');
|
|
418
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
419
|
+
a.href = url;
|
|
420
|
+
a.download = `synabun-workspaces-${date}.json`;
|
|
421
|
+
document.body.appendChild(a);
|
|
422
|
+
a.click();
|
|
423
|
+
document.body.removeChild(a);
|
|
424
|
+
URL.revokeObjectURL(url);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function importWorkspaces(file) {
|
|
428
|
+
if (!file) return;
|
|
429
|
+
const reader = new FileReader();
|
|
430
|
+
reader.onload = (e) => {
|
|
431
|
+
try {
|
|
432
|
+
const data = JSON.parse(e.target.result);
|
|
433
|
+
|
|
434
|
+
// Validate structure
|
|
435
|
+
if (!data || !Array.isArray(data.workspaces)) {
|
|
436
|
+
console.warn('SynaBun: Invalid workspace file format');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const existing = getWorkspaces();
|
|
441
|
+
const existingNames = new Set(existing.map(w => w.name));
|
|
442
|
+
|
|
443
|
+
// Merge imported workspaces
|
|
444
|
+
for (const ws of data.workspaces) {
|
|
445
|
+
// Basic validation
|
|
446
|
+
if (!ws.name || !ws.id) continue;
|
|
447
|
+
|
|
448
|
+
// Handle name collisions
|
|
449
|
+
if (existingNames.has(ws.name)) {
|
|
450
|
+
ws.name = ws.name + ' (imported)';
|
|
451
|
+
ws.id = String(Date.now()) + Math.random().toString(36).slice(2, 6);
|
|
452
|
+
}
|
|
453
|
+
existingNames.add(ws.name);
|
|
454
|
+
existing.push(ws);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
saveWorkspacesToStorage(existing);
|
|
458
|
+
renderList();
|
|
459
|
+
updateIndicator();
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.warn('SynaBun: Failed to parse workspace file', err);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
reader.readAsText(file);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
// ═══════════════════════════════════════════
|
|
469
|
+
// RENDERING
|
|
470
|
+
// ═══════════════════════════════════════════
|
|
471
|
+
|
|
472
|
+
function renderList() {
|
|
473
|
+
const listEl = $('ws-list');
|
|
474
|
+
if (!listEl) return;
|
|
475
|
+
|
|
476
|
+
const workspaces = getWorkspaces();
|
|
477
|
+
const activeId = getActiveId();
|
|
478
|
+
|
|
479
|
+
if (workspaces.length === 0) {
|
|
480
|
+
listEl.innerHTML = '<div class="ws-empty">No saved workspaces</div>';
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
listEl.innerHTML = '';
|
|
485
|
+
|
|
486
|
+
for (const ws of workspaces) {
|
|
487
|
+
const row = document.createElement('div');
|
|
488
|
+
row.className = 'ws-row' + (ws.id === activeId ? ' active' : '');
|
|
489
|
+
|
|
490
|
+
const cardCount = ws.cards ? ws.cards.length : 0;
|
|
491
|
+
const hasTerm = ws.terminal?.visible;
|
|
492
|
+
const date = new Date(ws.updated || ws.created);
|
|
493
|
+
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
494
|
+
|
|
495
|
+
const meta = [`${cardCount} cards`];
|
|
496
|
+
if (hasTerm) meta.push('CLI');
|
|
497
|
+
meta.push(dateStr);
|
|
498
|
+
|
|
499
|
+
row.innerHTML = `
|
|
500
|
+
<span class="ws-row-name" title="${ws.name}">${ws.name}</span>
|
|
501
|
+
<span class="ws-row-meta">${meta.join(' · ')}</span>
|
|
502
|
+
<button class="ws-row-delete" title="Delete">×</button>
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
// Load on click
|
|
506
|
+
row.addEventListener('click', (e) => {
|
|
507
|
+
if (e.target.closest('.ws-row-delete')) return;
|
|
508
|
+
loadWorkspace(ws);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Delete button
|
|
512
|
+
row.querySelector('.ws-row-delete').addEventListener('click', (e) => {
|
|
513
|
+
e.stopPropagation();
|
|
514
|
+
confirmDeleteWorkspace(ws.id, ws.name);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
listEl.appendChild(row);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function updateIndicator() {
|
|
522
|
+
const indicator = $('ws-indicator');
|
|
523
|
+
const nameEl = $('ws-active-name');
|
|
524
|
+
if (!indicator || !nameEl) return;
|
|
525
|
+
|
|
526
|
+
const activeId = getActiveId();
|
|
527
|
+
if (activeId) {
|
|
528
|
+
const workspaces = getWorkspaces();
|
|
529
|
+
const active = workspaces.find(w => w.id === activeId);
|
|
530
|
+
if (active) {
|
|
531
|
+
nameEl.textContent = active.name;
|
|
532
|
+
indicator.classList.add('has-workspace');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
nameEl.textContent = 'No workspace';
|
|
538
|
+
indicator.classList.remove('has-workspace');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
// ═══════════════════════════════════════════
|
|
543
|
+
// DROPDOWN TOGGLE
|
|
544
|
+
// ═══════════════════════════════════════════
|
|
545
|
+
|
|
546
|
+
function openDropdown() {
|
|
547
|
+
const dropdown = $('ws-dropdown');
|
|
548
|
+
const indicator = $('ws-indicator');
|
|
549
|
+
if (!dropdown) return;
|
|
550
|
+
|
|
551
|
+
_isOpen = true;
|
|
552
|
+
dropdown.style.display = '';
|
|
553
|
+
indicator?.classList.add('open');
|
|
554
|
+
renderList();
|
|
555
|
+
|
|
556
|
+
emit('panel:close-all-dropdowns-except', 'workspace');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function closeDropdown() {
|
|
560
|
+
const dropdown = $('ws-dropdown');
|
|
561
|
+
const indicator = $('ws-indicator');
|
|
562
|
+
if (!dropdown) return;
|
|
563
|
+
|
|
564
|
+
_isOpen = false;
|
|
565
|
+
dropdown.style.display = 'none';
|
|
566
|
+
indicator?.classList.remove('open');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function toggleDropdown() {
|
|
570
|
+
if (_isOpen) {
|
|
571
|
+
closeDropdown();
|
|
572
|
+
} else {
|
|
573
|
+
openDropdown();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
// ═══════════════════════════════════════════
|
|
579
|
+
// INITIALIZATION
|
|
580
|
+
// ═══════════════════════════════════════════
|
|
581
|
+
|
|
582
|
+
export function initWorkspaces() {
|
|
583
|
+
const indicator = $('ws-indicator');
|
|
584
|
+
const saveBtn = $('ws-save-btn');
|
|
585
|
+
const nameInput = $('ws-name-input');
|
|
586
|
+
const exportBtn = $('ws-export-btn');
|
|
587
|
+
const importBtn = $('ws-import-btn');
|
|
588
|
+
const importFile = $('ws-import-file');
|
|
589
|
+
|
|
590
|
+
// Toggle dropdown
|
|
591
|
+
if (indicator) {
|
|
592
|
+
indicator.addEventListener('click', (e) => {
|
|
593
|
+
e.stopPropagation();
|
|
594
|
+
toggleDropdown();
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Clear workspace (with confirmation dialog)
|
|
599
|
+
const clearBtn = $('ws-clear-btn');
|
|
600
|
+
if (clearBtn) {
|
|
601
|
+
clearBtn.addEventListener('click', (e) => {
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
showClearDialog();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Save workspace
|
|
608
|
+
if (saveBtn) {
|
|
609
|
+
saveBtn.addEventListener('click', (e) => {
|
|
610
|
+
e.stopPropagation();
|
|
611
|
+
const name = nameInput ? nameInput.value.trim() : '';
|
|
612
|
+
if (name) saveWorkspace(name);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Enter key in name input
|
|
617
|
+
if (nameInput) {
|
|
618
|
+
nameInput.addEventListener('keydown', (e) => {
|
|
619
|
+
if (e.key === 'Enter') {
|
|
620
|
+
e.preventDefault();
|
|
621
|
+
const name = nameInput.value.trim();
|
|
622
|
+
if (name) saveWorkspace(name);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
// Stop propagation to prevent dropdown close on clicks inside
|
|
626
|
+
nameInput.addEventListener('click', (e) => e.stopPropagation());
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Export
|
|
630
|
+
if (exportBtn) {
|
|
631
|
+
exportBtn.addEventListener('click', (e) => {
|
|
632
|
+
e.stopPropagation();
|
|
633
|
+
exportWorkspaces();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Import
|
|
638
|
+
if (importBtn) {
|
|
639
|
+
importBtn.addEventListener('click', (e) => {
|
|
640
|
+
e.stopPropagation();
|
|
641
|
+
importFile?.click();
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (importFile) {
|
|
646
|
+
importFile.addEventListener('change', (e) => {
|
|
647
|
+
const file = e.target.files?.[0];
|
|
648
|
+
if (file) importWorkspaces(file);
|
|
649
|
+
// Reset so same file can be re-imported
|
|
650
|
+
importFile.value = '';
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Quick-save button
|
|
655
|
+
const quickSaveBtn = $('ws-quick-save');
|
|
656
|
+
if (quickSaveBtn) {
|
|
657
|
+
quickSaveBtn.addEventListener('click', (e) => {
|
|
658
|
+
e.stopPropagation();
|
|
659
|
+
const activeId = getActiveId();
|
|
660
|
+
if (!activeId) return;
|
|
661
|
+
const workspaces = getWorkspaces();
|
|
662
|
+
const active = workspaces.find(w => w.id === activeId);
|
|
663
|
+
if (!active) return;
|
|
664
|
+
saveWorkspace(active.name);
|
|
665
|
+
quickSaveBtn.classList.add('saved');
|
|
666
|
+
setTimeout(() => quickSaveBtn.classList.remove('saved'), 800);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Grid snap toggle
|
|
671
|
+
const gridBtn = $('ws-grid-toggle');
|
|
672
|
+
if (gridBtn) {
|
|
673
|
+
const saved = storage.getItem(KEYS.GRID_SNAP);
|
|
674
|
+
if (saved === 'true') {
|
|
675
|
+
state.gridSnap = true;
|
|
676
|
+
gridBtn.classList.add('active');
|
|
677
|
+
document.body.classList.add('grid-active');
|
|
678
|
+
}
|
|
679
|
+
gridBtn.addEventListener('click', (e) => {
|
|
680
|
+
e.stopPropagation();
|
|
681
|
+
state.gridSnap = !state.gridSnap;
|
|
682
|
+
gridBtn.classList.toggle('active', state.gridSnap);
|
|
683
|
+
document.body.classList.toggle('grid-active', state.gridSnap);
|
|
684
|
+
storage.setItem(KEYS.GRID_SNAP, state.gridSnap);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Tile floating terminals
|
|
689
|
+
const tileBtn = $('ws-tile-btn');
|
|
690
|
+
if (tileBtn) {
|
|
691
|
+
tileBtn.addEventListener('click', (e) => {
|
|
692
|
+
e.stopPropagation();
|
|
693
|
+
tileFloatingTerminals();
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Close dropdown on click outside
|
|
698
|
+
document.addEventListener('click', (e) => {
|
|
699
|
+
if (!_isOpen) return;
|
|
700
|
+
const container = $('topright-controls') || $('workspace-overlay');
|
|
701
|
+
if (container && !container.contains(e.target)) {
|
|
702
|
+
closeDropdown();
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// Close on global dropdown close event
|
|
707
|
+
on('panel:close-all-dropdowns', () => closeDropdown());
|
|
708
|
+
on('panel:close-all-dropdowns-except', (name) => {
|
|
709
|
+
if (name !== 'workspace') closeDropdown();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Dim when visualization is toggled off
|
|
713
|
+
on('viz:toggled', (visible) => {
|
|
714
|
+
const container = $('topright-controls');
|
|
715
|
+
if (container) {
|
|
716
|
+
container.classList.toggle('viz-hidden', !visible);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Restore indicator state on init
|
|
721
|
+
updateIndicator();
|
|
722
|
+
}
|