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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. 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 &amp; 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">&times;</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
+ }