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,534 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Command Runner
3
+ // Save and one-click execute terminal commands,
4
+ // organized by category groups.
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { emit, on } from './state.js';
8
+ import { KEYS, COLOR_PALETTE } from './constants.js';
9
+ import { storage } from './storage.js';
10
+
11
+ const $ = (id) => document.getElementById(id);
12
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
13
+ const genId = (prefix) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
14
+
15
+ // ── Module-local state ──
16
+ let _panel = null;
17
+ let _data = { categories: [], commands: [] };
18
+ let _editTarget = null; // { type: 'command'|'category', id: string|null } — null = creating new
19
+
20
+ const PANEL_KEY = KEYS.PANEL_PREFIX + 'command-runner';
21
+ const DATA_KEY = KEYS.COMMAND_RUNNER;
22
+
23
+ // ── SVGs ──
24
+ const SVG_PLAY = `<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><polygon points="4,2 14,8 4,14"/></svg>`;
25
+ const SVG_EDIT = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 2.5l2 2L5 13l-3 1 1-3z"/></svg>`;
26
+ const SVG_DELETE = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="4" x2="13" y2="4"/><path d="M6 4V3a1 1 0 011-1h2a1 1 0 011 1v1"/><path d="M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9"/></svg>`;
27
+ const SVG_CHEVRON = `<svg viewBox="0 0 16 16" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 4 10 8 6 12"/></svg>`;
28
+ const SVG_CLOSE = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>`;
29
+ const SVG_TERMINAL = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`;
30
+
31
+ // ═══════════════════════════════════════════
32
+ // INIT
33
+ // ═══════════════════════════════════════════
34
+
35
+ export function initCommandRunner() {
36
+ on('command-runner:open', () => openPanel());
37
+ }
38
+
39
+ // ═══════════════════════════════════════════
40
+ // PANEL LIFECYCLE
41
+ // ═══════════════════════════════════════════
42
+
43
+ function openPanel() {
44
+ if (_panel) { _panel.focus(); return; }
45
+
46
+ _panel = document.createElement('div');
47
+ _panel.className = 'command-runner-panel glass resizable';
48
+ _panel.id = 'command-runner-panel';
49
+ _panel.innerHTML = buildPanelHTML();
50
+ document.body.appendChild(_panel);
51
+
52
+ // Restore saved position
53
+ try {
54
+ const saved = JSON.parse(storage.getItem(PANEL_KEY));
55
+ if (saved) {
56
+ if (saved.x != null) _panel.style.left = saved.x + 'px';
57
+ if (saved.y != null) _panel.style.top = Math.max(48, saved.y) + 'px';
58
+ if (saved.w) _panel.style.width = saved.w + 'px';
59
+ if (saved.h) _panel.style.height = saved.h + 'px';
60
+ }
61
+ } catch {}
62
+
63
+ // Center if no saved position
64
+ if (!_panel.style.left) {
65
+ const vw = window.innerWidth, vh = window.innerHeight;
66
+ _panel.style.left = Math.max(20, (vw - 420) / 2) + 'px';
67
+ _panel.style.top = Math.max(48, (vh - 500) / 2) + 'px';
68
+ }
69
+
70
+ loadData();
71
+ wirePanel();
72
+ renderBody();
73
+
74
+ requestAnimationFrame(() => _panel.classList.add('open'));
75
+ }
76
+
77
+ function closePanel() {
78
+ if (!_panel) return;
79
+ // Save position
80
+ try {
81
+ const rect = _panel.getBoundingClientRect();
82
+ storage.setItem(PANEL_KEY, JSON.stringify({
83
+ x: Math.round(rect.left), y: Math.round(rect.top),
84
+ w: Math.round(rect.width), h: Math.round(rect.height),
85
+ }));
86
+ } catch {}
87
+
88
+ _panel.remove();
89
+ _panel = null;
90
+ _editTarget = null;
91
+ }
92
+
93
+ // ═══════════════════════════════════════════
94
+ // DATA
95
+ // ═══════════════════════════════════════════
96
+
97
+ function loadData() {
98
+ try {
99
+ const raw = storage.getItem(DATA_KEY);
100
+ if (raw) {
101
+ const parsed = JSON.parse(raw);
102
+ _data = {
103
+ categories: Array.isArray(parsed.categories) ? parsed.categories : [],
104
+ commands: Array.isArray(parsed.commands) ? parsed.commands : [],
105
+ };
106
+ } else {
107
+ _data = { categories: [], commands: [] };
108
+ }
109
+ } catch {
110
+ _data = { categories: [], commands: [] };
111
+ }
112
+ }
113
+
114
+ function saveData() {
115
+ storage.setItem(DATA_KEY, JSON.stringify(_data));
116
+ }
117
+
118
+ function getCategoryCommands(catId) {
119
+ return _data.commands
120
+ .filter(c => c.categoryId === catId)
121
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
122
+ }
123
+
124
+ // ═══════════════════════════════════════════
125
+ // HTML BUILDERS
126
+ // ═══════════════════════════════════════════
127
+
128
+ function buildPanelHTML() {
129
+ return `
130
+ <div class="resize-handle resize-handle-t" data-resize="t"></div>
131
+ <div class="resize-handle resize-handle-r" data-resize="r"></div>
132
+ <div class="resize-handle resize-handle-b" data-resize="b"></div>
133
+ <div class="resize-handle resize-handle-l" data-resize="l"></div>
134
+ <div class="resize-handle resize-handle-tl" data-resize="tl"></div>
135
+ <div class="resize-handle resize-handle-tr" data-resize="tr"></div>
136
+ <div class="resize-handle resize-handle-bl" data-resize="bl"></div>
137
+ <div class="resize-handle resize-handle-br" data-resize="br"></div>
138
+
139
+ <div class="cr-header drag-handle" data-drag="command-runner-panel">
140
+ <div class="cr-header-left">
141
+ <span class="cr-header-icon">${SVG_TERMINAL}</span>
142
+ <h3>Command Runner</h3>
143
+ <span class="cr-count" id="cr-total-count"></span>
144
+ </div>
145
+ <div class="cr-header-actions">
146
+ <button class="cr-header-btn" id="cr-add-cat-btn">+ Group</button>
147
+ <button class="cr-header-btn cr-primary" id="cr-add-cmd-btn">+ Command</button>
148
+ </div>
149
+ <button class="cr-close" id="cr-close-btn">${SVG_CLOSE}</button>
150
+ </div>
151
+
152
+ <div class="cr-body" id="cr-body"></div>
153
+
154
+ <div class="cr-modal-overlay hidden" id="cr-modal-overlay">
155
+ <div class="cr-modal glass" id="cr-modal"></div>
156
+ </div>
157
+ `;
158
+ }
159
+
160
+ function renderBody() {
161
+ const body = $('cr-body');
162
+ if (!body) return;
163
+
164
+ const countEl = $('cr-total-count');
165
+ if (countEl) countEl.textContent = _data.commands.length || '';
166
+
167
+ const sorted = [..._data.categories].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
168
+
169
+ if (sorted.length === 0) {
170
+ body.innerHTML = `
171
+ <div class="cr-empty">
172
+ <div class="cr-empty-icon">${SVG_TERMINAL}</div>
173
+ <p>No commands yet</p>
174
+ <p class="cr-empty-hint">Click <strong>+ Group</strong> to create a category,<br>then <strong>+ Command</strong> to add commands.</p>
175
+ </div>
176
+ `;
177
+ return;
178
+ }
179
+
180
+ let html = '';
181
+ for (const cat of sorted) {
182
+ const cmds = getCategoryCommands(cat.id);
183
+ const collapsed = cat.collapsed ? ' collapsed' : '';
184
+ const dotColor = cat.color || COLOR_PALETTE[0];
185
+
186
+ html += `<div class="cr-category${collapsed}" data-cat-id="${esc(cat.id)}">`;
187
+ html += ` <div class="cr-cat-header">`;
188
+ html += ` <span class="cr-cat-chevron">${SVG_CHEVRON}</span>`;
189
+ html += ` <span class="cr-cat-dot" style="background:${esc(dotColor)}"></span>`;
190
+ html += ` <span class="cr-cat-name">${esc(cat.name)}</span>`;
191
+ html += ` <span class="cr-cat-count">${cmds.length}</span>`;
192
+ html += ` <span class="cr-cat-actions">`;
193
+ html += ` <button class="cr-icon-btn" data-action="edit-cat" title="Edit group">${SVG_EDIT}</button>`;
194
+ html += ` <button class="cr-icon-btn cr-icon-btn--danger" data-action="delete-cat" title="Delete group">${SVG_DELETE}</button>`;
195
+ html += ` </span>`;
196
+ html += ` </div>`;
197
+ html += ` <div class="cr-cat-body">`;
198
+
199
+ if (cmds.length === 0) {
200
+ html += `<div class="cr-cat-empty">No commands in this group</div>`;
201
+ } else {
202
+ for (const cmd of cmds) {
203
+ html += `<div class="cr-cmd" data-cmd-id="${esc(cmd.id)}">`;
204
+ html += ` <button class="cr-run-btn" title="Run command">${SVG_PLAY}</button>`;
205
+ html += ` <div class="cr-cmd-info">`;
206
+ html += ` <div class="cr-cmd-name">${esc(cmd.name)}</div>`;
207
+ html += ` <div class="cr-cmd-text">${esc(cmd.command)}</div>`;
208
+ if (cmd.cwd) {
209
+ html += ` <div class="cr-cmd-cwd">${esc(cmd.cwd)}</div>`;
210
+ }
211
+ html += ` </div>`;
212
+ html += ` <span class="cr-cmd-actions">`;
213
+ html += ` <button class="cr-icon-btn" data-action="edit-cmd" title="Edit command">${SVG_EDIT}</button>`;
214
+ html += ` <button class="cr-icon-btn cr-icon-btn--danger" data-action="delete-cmd" title="Delete command">${SVG_DELETE}</button>`;
215
+ html += ` </span>`;
216
+ html += `</div>`;
217
+ }
218
+ }
219
+
220
+ html += ` </div>`;
221
+ html += `</div>`;
222
+ }
223
+
224
+ body.innerHTML = html;
225
+ }
226
+
227
+ // ═══════════════════════════════════════════
228
+ // MODAL (Add/Edit forms)
229
+ // ═══════════════════════════════════════════
230
+
231
+ function openModal(type, id) {
232
+ _editTarget = { type, id: id || null };
233
+ const overlay = $('cr-modal-overlay');
234
+ const modal = $('cr-modal');
235
+ if (!overlay || !modal) return;
236
+
237
+ if (type === 'category') {
238
+ const existing = id ? _data.categories.find(c => c.id === id) : null;
239
+ modal.innerHTML = buildCategoryForm(existing);
240
+ } else {
241
+ const existing = id ? _data.commands.find(c => c.id === id) : null;
242
+ modal.innerHTML = buildCommandForm(existing);
243
+ }
244
+
245
+ overlay.classList.remove('hidden');
246
+ }
247
+
248
+ function closeModal() {
249
+ const overlay = $('cr-modal-overlay');
250
+ if (overlay) overlay.classList.add('hidden');
251
+ _editTarget = null;
252
+ }
253
+
254
+ function buildCategoryForm(existing) {
255
+ const name = existing ? esc(existing.name) : '';
256
+ const activeColor = existing?.color || COLOR_PALETTE[0];
257
+
258
+ let swatches = '';
259
+ for (const c of COLOR_PALETTE.slice(0, 16)) {
260
+ const sel = c === activeColor ? ' selected' : '';
261
+ swatches += `<button type="button" class="cr-color-swatch${sel}" data-color="${c}" style="background:${c}"></button>`;
262
+ }
263
+
264
+ return `
265
+ <div class="cr-modal-header">
266
+ <h4>${existing ? 'Edit' : 'New'} Group</h4>
267
+ <button class="cr-icon-btn" id="cr-modal-close">${SVG_CLOSE}</button>
268
+ </div>
269
+ <div class="cr-modal-body">
270
+ <label class="cr-form-label">Name</label>
271
+ <input class="cr-form-input" id="cr-cat-name" type="text" value="${name}" placeholder="e.g. Dev Servers" autocomplete="off" />
272
+ <label class="cr-form-label" style="margin-top:12px">Color</label>
273
+ <div class="cr-color-picker" id="cr-cat-color">${swatches}</div>
274
+ <input type="hidden" id="cr-cat-color-val" value="${esc(activeColor)}" />
275
+ </div>
276
+ <div class="cr-modal-footer">
277
+ <button class="cr-modal-btn" id="cr-modal-cancel">Cancel</button>
278
+ <button class="cr-modal-btn cr-modal-btn--primary" id="cr-modal-save">${existing ? 'Save' : 'Create'}</button>
279
+ </div>
280
+ `;
281
+ }
282
+
283
+ function buildCommandForm(existing) {
284
+ const name = existing ? esc(existing.name) : '';
285
+ const command = existing ? esc(existing.command) : '';
286
+ const cwd = existing?.cwd ? esc(existing.cwd) : '';
287
+ const catId = existing?.categoryId || '';
288
+
289
+ let catOptions = '';
290
+ const sorted = [..._data.categories].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
291
+ for (const cat of sorted) {
292
+ const sel = cat.id === catId ? ' selected' : '';
293
+ catOptions += `<option value="${esc(cat.id)}"${sel}>${esc(cat.name)}</option>`;
294
+ }
295
+
296
+ return `
297
+ <div class="cr-modal-header">
298
+ <h4>${existing ? 'Edit' : 'New'} Command</h4>
299
+ <button class="cr-icon-btn" id="cr-modal-close">${SVG_CLOSE}</button>
300
+ </div>
301
+ <div class="cr-modal-body">
302
+ <label class="cr-form-label">Group</label>
303
+ <select class="cr-form-input cr-form-select" id="cr-cmd-cat">${catOptions}</select>
304
+
305
+ <label class="cr-form-label" style="margin-top:12px">Name</label>
306
+ <input class="cr-form-input" id="cr-cmd-name" type="text" value="${name}" placeholder="e.g. Start Dev Server" autocomplete="off" />
307
+
308
+ <label class="cr-form-label" style="margin-top:12px">Command</label>
309
+ <input class="cr-form-input cr-form-mono" id="cr-cmd-command" type="text" value="${command}" placeholder="e.g. npm run dev" autocomplete="off" />
310
+
311
+ <label class="cr-form-label" style="margin-top:12px">Working Directory <span class="cr-form-hint">(optional)</span></label>
312
+ <input class="cr-form-input cr-form-mono" id="cr-cmd-cwd" type="text" value="${cwd}" placeholder="e.g. /home/user/myproject" autocomplete="off" />
313
+ </div>
314
+ <div class="cr-modal-footer">
315
+ <button class="cr-modal-btn" id="cr-modal-cancel">Cancel</button>
316
+ <button class="cr-modal-btn cr-modal-btn--primary" id="cr-modal-save">${existing ? 'Save' : 'Create'}</button>
317
+ </div>
318
+ `;
319
+ }
320
+
321
+ // ═══════════════════════════════════════════
322
+ // CRUD
323
+ // ═══════════════════════════════════════════
324
+
325
+ function saveFromModal() {
326
+ if (!_editTarget) return;
327
+
328
+ if (_editTarget.type === 'category') {
329
+ const nameEl = $('cr-cat-name');
330
+ const colorEl = $('cr-cat-color-val');
331
+ const name = nameEl?.value.trim();
332
+ if (!name) { nameEl?.focus(); return; }
333
+ const color = colorEl?.value || COLOR_PALETTE[0];
334
+
335
+ if (_editTarget.id) {
336
+ // Update existing
337
+ const cat = _data.categories.find(c => c.id === _editTarget.id);
338
+ if (cat) { cat.name = name; cat.color = color; }
339
+ } else {
340
+ // Create new
341
+ _data.categories.push({
342
+ id: genId('cat'),
343
+ name,
344
+ color,
345
+ collapsed: false,
346
+ order: _data.categories.length,
347
+ });
348
+ }
349
+ } else {
350
+ const catEl = $('cr-cmd-cat');
351
+ const nameEl = $('cr-cmd-name');
352
+ const cmdEl = $('cr-cmd-command');
353
+ const cwdEl = $('cr-cmd-cwd');
354
+
355
+ const categoryId = catEl?.value;
356
+ const name = nameEl?.value.trim();
357
+ const command = cmdEl?.value.trim();
358
+ const cwd = cwdEl?.value.trim() || null;
359
+
360
+ if (!categoryId) { catEl?.focus(); return; }
361
+ if (!name) { nameEl?.focus(); return; }
362
+ if (!command) { cmdEl?.focus(); return; }
363
+
364
+ if (_editTarget.id) {
365
+ // Update existing
366
+ const cmd = _data.commands.find(c => c.id === _editTarget.id);
367
+ if (cmd) {
368
+ cmd.categoryId = categoryId;
369
+ cmd.name = name;
370
+ cmd.command = command;
371
+ cmd.cwd = cwd;
372
+ }
373
+ } else {
374
+ // Create new
375
+ const sibling = getCategoryCommands(categoryId);
376
+ _data.commands.push({
377
+ id: genId('cmd'),
378
+ categoryId,
379
+ name,
380
+ command,
381
+ cwd,
382
+ order: sibling.length,
383
+ });
384
+ }
385
+ }
386
+
387
+ saveData();
388
+ closeModal();
389
+ renderBody();
390
+ }
391
+
392
+ function deleteCommand(id) {
393
+ _data.commands = _data.commands.filter(c => c.id !== id);
394
+ saveData();
395
+ renderBody();
396
+ }
397
+
398
+ function deleteCategory(id) {
399
+ const cmds = getCategoryCommands(id);
400
+ if (cmds.length > 0) {
401
+ if (!confirm(`Delete group and its ${cmds.length} command${cmds.length > 1 ? 's' : ''}?`)) return;
402
+ }
403
+ _data.commands = _data.commands.filter(c => c.categoryId !== id);
404
+ _data.categories = _data.categories.filter(c => c.id !== id);
405
+ saveData();
406
+ renderBody();
407
+ }
408
+
409
+ function toggleCategoryCollapse(catId) {
410
+ const cat = _data.categories.find(c => c.id === catId);
411
+ if (cat) {
412
+ cat.collapsed = !cat.collapsed;
413
+ saveData();
414
+ renderBody();
415
+ }
416
+ }
417
+
418
+ // ═══════════════════════════════════════════
419
+ // RUN COMMAND
420
+ // ═══════════════════════════════════════════
421
+
422
+ function runCommand(cmdId) {
423
+ const cmd = _data.commands.find(c => c.id === cmdId);
424
+ if (!cmd) return;
425
+ emit('terminal:run-command', {
426
+ command: cmd.command,
427
+ cwd: cmd.cwd || null,
428
+ label: cmd.name,
429
+ });
430
+ }
431
+
432
+ // ═══════════════════════════════════════════
433
+ // EVENT WIRING
434
+ // ═══════════════════════════════════════════
435
+
436
+ function wirePanel() {
437
+ if (!_panel) return;
438
+
439
+ // ── Main click delegation ──
440
+ _panel.addEventListener('click', (e) => {
441
+ // Close button
442
+ if (e.target.closest('#cr-close-btn')) return closePanel();
443
+
444
+ // Header add buttons
445
+ if (e.target.closest('#cr-add-cat-btn')) return openModal('category');
446
+ if (e.target.closest('#cr-add-cmd-btn')) {
447
+ if (_data.categories.length === 0) {
448
+ // Need at least one category first
449
+ openModal('category');
450
+ } else {
451
+ openModal('command');
452
+ }
453
+ return;
454
+ }
455
+
456
+ // Run button
457
+ const runBtn = e.target.closest('.cr-run-btn');
458
+ if (runBtn) {
459
+ const cmdEl = runBtn.closest('.cr-cmd');
460
+ if (cmdEl) return runCommand(cmdEl.dataset.cmdId);
461
+ }
462
+
463
+ // Category actions
464
+ const editCat = e.target.closest('[data-action="edit-cat"]');
465
+ if (editCat) {
466
+ const catEl = editCat.closest('.cr-category');
467
+ if (catEl) return openModal('category', catEl.dataset.catId);
468
+ }
469
+ const delCat = e.target.closest('[data-action="delete-cat"]');
470
+ if (delCat) {
471
+ const catEl = delCat.closest('.cr-category');
472
+ if (catEl) return deleteCategory(catEl.dataset.catId);
473
+ }
474
+
475
+ // Command actions
476
+ const editCmd = e.target.closest('[data-action="edit-cmd"]');
477
+ if (editCmd) {
478
+ const cmdEl = editCmd.closest('.cr-cmd');
479
+ if (cmdEl) return openModal('command', cmdEl.dataset.cmdId);
480
+ }
481
+ const delCmd = e.target.closest('[data-action="delete-cmd"]');
482
+ if (delCmd) {
483
+ const cmdEl = delCmd.closest('.cr-cmd');
484
+ if (cmdEl) return deleteCommand(cmdEl.dataset.cmdId);
485
+ }
486
+
487
+ // Category header click → toggle collapse (but not if clicking actions)
488
+ const catHeader = e.target.closest('.cr-cat-header');
489
+ if (catHeader && !e.target.closest('.cr-cat-actions')) {
490
+ const catEl = catHeader.closest('.cr-category');
491
+ if (catEl) return toggleCategoryCollapse(catEl.dataset.catId);
492
+ }
493
+
494
+ // Modal buttons
495
+ if (e.target.closest('#cr-modal-close') || e.target.closest('#cr-modal-cancel')) return closeModal();
496
+ if (e.target.closest('#cr-modal-save')) return saveFromModal();
497
+
498
+ // Color swatch selection
499
+ const swatch = e.target.closest('.cr-color-swatch');
500
+ if (swatch) {
501
+ const picker = swatch.closest('.cr-color-picker');
502
+ if (picker) {
503
+ picker.querySelectorAll('.cr-color-swatch').forEach(s => s.classList.remove('selected'));
504
+ swatch.classList.add('selected');
505
+ const hidden = $('cr-cat-color-val');
506
+ if (hidden) hidden.value = swatch.dataset.color;
507
+ }
508
+ return;
509
+ }
510
+ });
511
+
512
+ // ── Modal overlay background click → close ──
513
+ const overlay = $('cr-modal-overlay');
514
+ if (overlay) {
515
+ overlay.addEventListener('click', (e) => {
516
+ if (e.target === overlay) closeModal();
517
+ });
518
+ }
519
+
520
+ // ── Enter key submits modal form ──
521
+ _panel.addEventListener('keydown', (e) => {
522
+ if (e.key === 'Enter' && _editTarget) {
523
+ e.preventDefault();
524
+ saveFromModal();
525
+ }
526
+ if (e.key === 'Escape') {
527
+ if (_editTarget) {
528
+ closeModal();
529
+ } else {
530
+ closePanel();
531
+ }
532
+ }
533
+ });
534
+ }