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,222 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Navbar
3
+ // Initializes the 2D/3D view toggle, right-side icon buttons, and menubar action buttons.
4
+ // Menu items (View, Graph, Bookmarks, Layouts) are handled by ui-menubar.js.
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { state, emit, on } from './state.js';
8
+ import { getVariant } from './registry.js';
9
+ import { KEYS } from './constants.js';
10
+ import { loadIfaceConfig, saveIfaceConfig, applyIfaceConfig } from './ui-settings.js';
11
+ import { registerAction } from './ui-keybinds.js';
12
+ import { startTutorial } from './ui-tutorial.js';
13
+ import { toggleClaudePanel } from './ui-claude-panel.js';
14
+ import { toggleSessionMonitor } from './ui-sessions.js';
15
+ import { toggleImageGallery } from './ui-image-gallery.js';
16
+ import { initUpdate } from './ui-update.js';
17
+
18
+ const $ = (id) => document.getElementById(id);
19
+
20
+ export function initNavbar() {
21
+ const variant = getVariant() || '3d';
22
+
23
+ // ── View toggle ──
24
+ const nav2d = $('nav-2d-link');
25
+ const nav3d = $('nav-3d-link');
26
+
27
+ const activeLink = variant === '3d' ? nav3d : nav2d;
28
+ const inactiveLink = variant === '3d' ? nav2d : nav3d;
29
+
30
+ if (activeLink) {
31
+ activeLink.addEventListener('click', (e) => e.preventDefault());
32
+ }
33
+ if (inactiveLink) {
34
+ inactiveLink.addEventListener('click', (e) => {
35
+ e.preventDefault();
36
+ if (state.selectedNodeId) {
37
+ sessionStorage.setItem(KEYS.SELECTED_SWITCH, state.selectedNodeId);
38
+ }
39
+ window.location.href = inactiveLink.getAttribute('href');
40
+ });
41
+ }
42
+
43
+ // ── Claude panel toggle (topright workspace toolbar) ──
44
+ const claudePanelBtn = $('topright-claude-panel-btn');
45
+ if (claudePanelBtn) {
46
+ claudePanelBtn.addEventListener('click', (e) => {
47
+ e.preventDefault();
48
+ toggleClaudePanel();
49
+ claudePanelBtn.classList.toggle('active');
50
+ });
51
+ registerAction('toggle-claude-panel', () => claudePanelBtn.click());
52
+ }
53
+
54
+ // ── Sidebar close button ──
55
+ const sidebar = $('category-sidebar');
56
+ const sidebarClose = $('sidebar-close');
57
+ if (sidebarClose && sidebar) {
58
+ sidebarClose.addEventListener('click', () => {
59
+ sidebar.style.display = 'none';
60
+ emit('sidebar:closed');
61
+ });
62
+ }
63
+
64
+ // ── Settings ──
65
+ // Note: Settings button click is handled by initSettings() in ui-settings.js.
66
+ // Do NOT add a click handler here — it would cause duplicate openSettingsModal() calls.
67
+
68
+ // ── Trash ──
69
+ // Note: Trash button click is handled by initTrash() in ui-trash.js.
70
+ // Do NOT add a click handler here — it would conflict with the toggle logic.
71
+
72
+ // ── Visualization toggle ──
73
+ const vizBtn = $('titlebar-viz-toggle');
74
+ const searchWrapper = $('search-wrapper');
75
+ if (vizBtn) {
76
+ const cfg = loadIfaceConfig();
77
+ const vizOn = cfg.visualizationEnabled !== false;
78
+ vizBtn.classList.toggle('active', vizOn);
79
+ if (searchWrapper) searchWrapper.classList.toggle('viz-hidden', !vizOn);
80
+
81
+ vizBtn.addEventListener('click', () => {
82
+ const cfg = loadIfaceConfig();
83
+ cfg.visualizationEnabled = !cfg.visualizationEnabled;
84
+ applyIfaceConfig(cfg);
85
+ saveIfaceConfig(cfg);
86
+ vizBtn.classList.toggle('active', cfg.visualizationEnabled);
87
+ if (searchWrapper) searchWrapper.classList.toggle('viz-hidden', !cfg.visualizationEnabled);
88
+ });
89
+
90
+ on('viz:toggle', (enabled) => {
91
+ vizBtn.classList.toggle('active', enabled);
92
+ if (searchWrapper) searchWrapper.classList.toggle('viz-hidden', !enabled);
93
+ });
94
+
95
+ registerAction('toggle-focus-mode', () => vizBtn.click());
96
+ }
97
+
98
+ // ── Session monitor toggle ──
99
+ const sessionsBtn = $('titlebar-sessions-btn');
100
+ if (sessionsBtn) {
101
+ sessionsBtn.addEventListener('click', () => {
102
+ toggleSessionMonitor();
103
+ sessionsBtn.classList.toggle('active');
104
+ });
105
+ // Sync button state when window is closed via its own close button or backdrop
106
+ on('session-monitor:closed', () => sessionsBtn.classList.remove('active'));
107
+ registerAction('toggle-session-monitor', () => sessionsBtn.click());
108
+ }
109
+
110
+ // ── Image gallery toggle ──
111
+ const galleryBtn = $('titlebar-gallery-btn');
112
+ if (galleryBtn) {
113
+ galleryBtn.addEventListener('click', () => {
114
+ toggleImageGallery();
115
+ galleryBtn.classList.toggle('active');
116
+ });
117
+ on('image-gallery:closed', () => galleryBtn.classList.remove('active'));
118
+ registerAction('toggle-image-gallery', () => galleryBtn.click());
119
+ }
120
+
121
+ // ── Cost tracker dock button ──
122
+ const costBtn = $('titlebar-cost-btn');
123
+ if (costBtn) {
124
+ costBtn.addEventListener('click', () => emit('cost:dock-toggle'));
125
+ // Update label when cost changes
126
+ on('cost:updated', (data) => {
127
+ if (!data) return;
128
+ const label = costBtn.querySelector('.bar-cost-label');
129
+ if (label && typeof data.sessionCost === 'number') {
130
+ label.textContent = '$' + data.sessionCost.toFixed(2);
131
+ }
132
+ });
133
+ on('cost:dock-state', (docked) => {
134
+ costBtn.classList.toggle('active', docked);
135
+ });
136
+ }
137
+
138
+ // ── Tutorial toggle (?) ──
139
+ const tutBtn = $('titlebar-tutorial-btn');
140
+ if (tutBtn) {
141
+ tutBtn.addEventListener('click', () => startTutorial(true));
142
+ }
143
+
144
+ // ── Fullscreen button ──
145
+ const fsBtn = $('titlebar-fullscreen-btn');
146
+ if (fsBtn) {
147
+ const enterIcon = '<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>';
148
+ const exitIcon = '<svg viewBox="0 0 24 24"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>';
149
+
150
+ function updateFsIcon() {
151
+ const isFs = !!document.fullscreenElement;
152
+ fsBtn.innerHTML = isFs ? exitIcon : enterIcon;
153
+ fsBtn.classList.toggle('active', isFs);
154
+ // Lock height so macOS menu bar overlays instead of pushing content down
155
+ if (isFs) {
156
+ document.body.classList.add('is-fullscreen');
157
+ document.documentElement.style.setProperty('--fs-locked-height', window.screen.height + 'px');
158
+ } else {
159
+ document.body.classList.remove('is-fullscreen');
160
+ document.documentElement.style.removeProperty('--fs-locked-height');
161
+ }
162
+ }
163
+
164
+ fsBtn.addEventListener('click', async () => {
165
+ try {
166
+ if (document.fullscreenElement) {
167
+ await document.exitFullscreen();
168
+ } else {
169
+ await document.documentElement.requestFullscreen();
170
+ }
171
+ } catch (err) {
172
+ console.warn('Fullscreen toggle failed:', err);
173
+ }
174
+ });
175
+
176
+ document.addEventListener('fullscreenchange', updateFsIcon);
177
+ }
178
+
179
+ // ── Window Controls Overlay (PWA standalone) ──
180
+ // Toggles body.wco-active when the overlay chevron is clicked (traffic lights move into content).
181
+ if ('windowControlsOverlay' in navigator) {
182
+ const wco = navigator.windowControlsOverlay;
183
+ const updateWco = () => document.body.classList.toggle('wco-active', wco.visible);
184
+ updateWco();
185
+ wco.addEventListener('geometrychange', updateWco);
186
+ }
187
+
188
+ // ── Clock ──
189
+ const clockEl = $('titlebar-clock');
190
+ if (clockEl) {
191
+ function updateClock() {
192
+ const now = new Date();
193
+ const h = now.getHours().toString().padStart(2, '0');
194
+ const m = now.getMinutes().toString().padStart(2, '0');
195
+ clockEl.textContent = `${h}:${m}`;
196
+ }
197
+ updateClock();
198
+ setInterval(updateClock, 15000);
199
+ }
200
+
201
+ // ── Workspace toolbar collapse toggle ──
202
+ const collapseBtn = $('topright-collapse-btn');
203
+ const controls = $('topright-controls');
204
+ if (collapseBtn && controls) {
205
+ // Restore persisted state
206
+ const wasCollapsed = localStorage.getItem('synabun-toolbar-collapsed') === '1';
207
+ if (wasCollapsed) controls.classList.add('collapsed');
208
+
209
+ collapseBtn.addEventListener('click', (e) => {
210
+ e.stopPropagation();
211
+ const isCollapsed = controls.classList.toggle('collapsed');
212
+ localStorage.setItem('synabun-toolbar-collapsed', isCollapsed ? '1' : '0');
213
+ // Close any open dropdowns when collapsing
214
+ if (isCollapsed) emit('panel:close-all-dropdowns');
215
+ });
216
+
217
+ registerAction('toggle-toolbar', () => collapseBtn.click());
218
+ }
219
+
220
+ // ── Update alert ──
221
+ initUpdate();
222
+ }
@@ -0,0 +1,287 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Notification Engine
3
+ // Shared notification system for CLI terminal + Claude Panel
4
+ // ═══════════════════════════════════════════
5
+
6
+ import { KEYS } from './constants.js';
7
+ import { storage } from './storage.js';
8
+ import { emit } from './state.js';
9
+
10
+ // ── Sound presets ──
11
+ // Each preset defines tones for 'action' and 'done' events
12
+ const SOUND_PRESETS = {
13
+ beep: {
14
+ action: [{ freq: 880, dur: 0.12, volMul: 0.35 }, { freq: 880, dur: 0.12, volMul: 0.35, delay: 180 }],
15
+ done: [{ freq: 660, dur: 0.18, volMul: 0.25 }],
16
+ ask: [{ freq: 600, dur: 0.15, volMul: 0.30 }, { freq: 750, dur: 0.15, volMul: 0.30, delay: 200 }],
17
+ error: [{ freq: 300, dur: 0.25, volMul: 0.35 }, { freq: 250, dur: 0.30, volMul: 0.35, delay: 300 }],
18
+ },
19
+ chime: {
20
+ action: [{ freq: 784, dur: 0.15, volMul: 0.30 }, { freq: 1047, dur: 0.20, volMul: 0.25, delay: 160 }],
21
+ done: [{ freq: 523, dur: 0.12, volMul: 0.20 }, { freq: 659, dur: 0.12, volMul: 0.20, delay: 140 }, { freq: 784, dur: 0.18, volMul: 0.18, delay: 280 }],
22
+ ask: [{ freq: 659, dur: 0.15, volMul: 0.25 }, { freq: 784, dur: 0.18, volMul: 0.22, delay: 180 }],
23
+ error: [{ freq: 392, dur: 0.20, volMul: 0.30 }, { freq: 330, dur: 0.25, volMul: 0.30, delay: 250 }],
24
+ },
25
+ ping: {
26
+ action: [{ freq: 1200, dur: 0.08, volMul: 0.25 }, { freq: 1200, dur: 0.08, volMul: 0.25, delay: 120 }],
27
+ done: [{ freq: 1000, dur: 0.10, volMul: 0.20 }],
28
+ ask: [{ freq: 900, dur: 0.10, volMul: 0.22 }, { freq: 1100, dur: 0.10, volMul: 0.22, delay: 150 }],
29
+ error: [{ freq: 400, dur: 0.15, volMul: 0.28 }, { freq: 350, dur: 0.18, volMul: 0.28, delay: 200 }],
30
+ },
31
+ subtle: {
32
+ action: [{ freq: 600, dur: 0.10, volMul: 0.15 }],
33
+ done: [{ freq: 500, dur: 0.12, volMul: 0.12 }],
34
+ ask: [{ freq: 550, dur: 0.10, volMul: 0.13 }],
35
+ error: [{ freq: 350, dur: 0.15, volMul: 0.15 }],
36
+ },
37
+ };
38
+
39
+ // ── Notification types ──
40
+ export const NOTIF_TYPE = {
41
+ DONE: 'done',
42
+ ACTION: 'action',
43
+ ASK: 'ask',
44
+ ERROR: 'error',
45
+ };
46
+
47
+ // ── Audio context (lazy-init) ──
48
+ let _audioCtx = null;
49
+
50
+ function _playTone(freq, duration = 0.15, vol = 0.3) {
51
+ try {
52
+ if (!_audioCtx) _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
53
+ const osc = _audioCtx.createOscillator();
54
+ const gain = _audioCtx.createGain();
55
+ osc.connect(gain);
56
+ gain.connect(_audioCtx.destination);
57
+ osc.frequency.value = freq;
58
+ osc.type = 'sine';
59
+ gain.gain.setValueAtTime(vol, _audioCtx.currentTime);
60
+ gain.gain.exponentialRampToValueAtTime(0.001, _audioCtx.currentTime + duration);
61
+ osc.start();
62
+ osc.stop(_audioCtx.currentTime + duration);
63
+ } catch {}
64
+ }
65
+
66
+ // ── Settings readers ──
67
+
68
+ export function getNotifSettings() {
69
+ return {
70
+ enabled: storage.getItem(KEYS.TERMINAL_NOTIFICATIONS) !== 'off',
71
+ sound: storage.getItem(KEYS.NOTIF_SOUND) !== 'off',
72
+ volume: parseInt(storage.getItem(KEYS.NOTIF_SOUND_VOLUME) || '50', 10) / 100,
73
+ soundType: storage.getItem(KEYS.NOTIF_SOUND_TYPE) || 'beep',
74
+ banner: storage.getItem(KEYS.NOTIF_BANNER) !== 'off',
75
+ bannerFocused: storage.getItem(KEYS.NOTIF_BANNER_FOCUSED) === 'on',
76
+ sourceCli: storage.getItem(KEYS.NOTIF_SOURCE_CLI) !== 'off',
77
+ sourcePanel: storage.getItem(KEYS.NOTIF_SOURCE_PANEL) !== 'off',
78
+ triggerDone: storage.getItem(KEYS.NOTIF_TRIGGER_DONE) !== 'off',
79
+ triggerAction: storage.getItem(KEYS.NOTIF_TRIGGER_ACTION) !== 'off',
80
+ triggerAsk: storage.getItem(KEYS.NOTIF_TRIGGER_ASK) !== 'off',
81
+ triggerError: storage.getItem(KEYS.NOTIF_TRIGGER_ERROR) !== 'off',
82
+ toast: storage.getItem(KEYS.NOTIF_TOAST) !== 'off',
83
+ toastDuration: parseInt(storage.getItem(KEYS.NOTIF_TOAST_DURATION) || '5', 10),
84
+ toastPosition: storage.getItem(KEYS.NOTIF_TOAST_POSITION) || 'top-right',
85
+ // Legacy compat
86
+ actionOnly: storage.getItem(KEYS.NOTIF_ACTION_ONLY) === 'on',
87
+ };
88
+ }
89
+
90
+ // ── Sound playback ──
91
+
92
+ export function playNotifSound(type = NOTIF_TYPE.DONE) {
93
+ const s = getNotifSettings();
94
+ if (!s.enabled || !s.sound) return;
95
+ const preset = SOUND_PRESETS[s.soundType] || SOUND_PRESETS.beep;
96
+ const tones = preset[type] || preset.done;
97
+ for (const t of tones) {
98
+ const vol = s.volume * t.volMul;
99
+ if (t.delay) {
100
+ setTimeout(() => _playTone(t.freq, t.dur, vol), t.delay);
101
+ } else {
102
+ _playTone(t.freq, t.dur, vol);
103
+ }
104
+ }
105
+ }
106
+
107
+ /** Play a specific preset+type for the test buttons in settings */
108
+ export function playTestSound(presetName, type = NOTIF_TYPE.ACTION) {
109
+ const vol = parseInt(storage.getItem(KEYS.NOTIF_SOUND_VOLUME) || '50', 10) / 100;
110
+ const preset = SOUND_PRESETS[presetName] || SOUND_PRESETS.beep;
111
+ const tones = preset[type] || preset.action;
112
+ for (const t of tones) {
113
+ const v = vol * t.volMul;
114
+ if (t.delay) {
115
+ setTimeout(() => _playTone(t.freq, t.dur, v), t.delay);
116
+ } else {
117
+ _playTone(t.freq, t.dur, v);
118
+ }
119
+ }
120
+ }
121
+
122
+ // ── Banner (OS) notifications via service worker ──
123
+
124
+ function _sendBanner(title, body, tag, routing) {
125
+ const sw = navigator.serviceWorker?.controller;
126
+ if (sw) {
127
+ sw.postMessage({ type: 'SHOW_NOTIFICATION', title, body, tag, routing });
128
+ } else if (navigator.serviceWorker?.ready) {
129
+ navigator.serviceWorker.ready.then(reg => {
130
+ reg.showNotification(title, { body, tag, silent: true, data: routing });
131
+ });
132
+ }
133
+ }
134
+
135
+ // ── In-app toast notifications ──
136
+
137
+ const _TOAST_ICONS = {
138
+ done: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M5.5 8.5l2 2 3.5-4"/></svg>',
139
+ action: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M8 5v3.5"/><circle cx="8" cy="11" r="0.5" fill="currentColor"/></svg>',
140
+ ask: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M6.5 6.5a1.5 1.5 0 013 0c0 1-1.5 1.2-1.5 2.5"/><circle cx="8" cy="11.5" r="0.5" fill="currentColor"/></svg>',
141
+ error: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M10 6L6 10M6 6l4 4"/></svg>',
142
+ };
143
+
144
+ const _TOAST_ACCENTS = {
145
+ done: 'rgba(255,255,255,0.18)',
146
+ action: 'rgba(220,180,100,0.50)',
147
+ ask: 'rgba(140,170,220,0.45)',
148
+ error: 'rgba(200,120,120,0.50)',
149
+ };
150
+
151
+ const _TOAST_TITLES = {
152
+ done: 'Task Complete',
153
+ action: 'Action Required',
154
+ ask: 'Question',
155
+ error: 'Error',
156
+ };
157
+
158
+ let _toastContainer = null;
159
+ const MAX_TOASTS = 3;
160
+
161
+ function _ensureToastContainer(position) {
162
+ if (_toastContainer && document.body.contains(_toastContainer)) {
163
+ // Update position class if changed
164
+ const posClass = `pos-${position || 'top-right'}`;
165
+ if (!_toastContainer.classList.contains(posClass)) {
166
+ _toastContainer.className = '';
167
+ _toastContainer.id = 'notif-toast-container';
168
+ _toastContainer.classList.add(posClass);
169
+ }
170
+ return _toastContainer;
171
+ }
172
+ _toastContainer = document.createElement('div');
173
+ _toastContainer.id = 'notif-toast-container';
174
+ _toastContainer.classList.add(`pos-${position || 'top-right'}`);
175
+ document.body.appendChild(_toastContainer);
176
+ return _toastContainer;
177
+ }
178
+
179
+ function _showToast(type, title, body, source, opts) {
180
+ const s = getNotifSettings();
181
+ if (!s.toast) return;
182
+ const container = _ensureToastContainer(s.toastPosition);
183
+ const accent = _TOAST_ACCENTS[type] || _TOAST_ACCENTS.done;
184
+ const icon = _TOAST_ICONS[type] || _TOAST_ICONS.done;
185
+
186
+ const toast = document.createElement('div');
187
+ toast.className = 'notif-toast';
188
+ toast.style.setProperty('--notif-accent', accent);
189
+ toast.innerHTML = `
190
+ <div class="notif-toast-accent"></div>
191
+ <div class="notif-toast-icon">${icon}</div>
192
+ <div class="notif-toast-body">
193
+ <div class="notif-toast-title">${title}</div>
194
+ <div class="notif-toast-msg">${body}</div>
195
+ <div class="notif-toast-source">${source === 'panel' ? 'Side Panel' : 'CLI Terminal'}</div>
196
+ </div>
197
+ <button class="notif-toast-close">&times;</button>
198
+ `;
199
+
200
+ // Click toast body → show the source panel/terminal (with session/tab specificity)
201
+ toast.addEventListener('click', (e) => {
202
+ if (e.target.closest('.notif-toast-close')) return;
203
+ const event = source === 'panel' ? 'claude-panel:show' : 'terminal:show';
204
+ emit(event, { sessionId: opts?.sessionId, tabId: opts?.tabId });
205
+ _dismissToast(toast);
206
+ });
207
+
208
+ // Dismiss on close button
209
+ toast.querySelector('.notif-toast-close').addEventListener('click', () => _dismissToast(toast));
210
+
211
+ // Add to container
212
+ container.appendChild(toast);
213
+ // Trigger animation
214
+ requestAnimationFrame(() => requestAnimationFrame(() => toast.classList.add('visible')));
215
+
216
+ // Auto-dismiss
217
+ const dur = s.toastDuration * 1000;
218
+ toast._timeout = setTimeout(() => _dismissToast(toast), dur);
219
+
220
+ // Enforce max toasts
221
+ const toasts = container.querySelectorAll('.notif-toast');
222
+ if (toasts.length > MAX_TOASTS) {
223
+ _dismissToast(toasts[0]);
224
+ }
225
+ }
226
+
227
+ function _dismissToast(toast) {
228
+ if (toast._dismissed) return;
229
+ toast._dismissed = true;
230
+ if (toast._timeout) clearTimeout(toast._timeout);
231
+ toast.classList.remove('visible');
232
+ toast.classList.add('dismissing');
233
+ setTimeout(() => toast.remove(), 300);
234
+ }
235
+
236
+ // ── Main notification dispatcher ──
237
+ // source: 'cli' | 'panel'
238
+ // type: NOTIF_TYPE value
239
+ // label: session label for the notification body
240
+
241
+ export function notify(source, type, label, opts) {
242
+ const s = getNotifSettings();
243
+ if (!s.enabled) return;
244
+
245
+ // Source filter
246
+ if (source === 'cli' && !s.sourceCli) return;
247
+ if (source === 'panel' && !s.sourcePanel) return;
248
+
249
+ // Trigger filter
250
+ if (type === NOTIF_TYPE.DONE && !s.triggerDone) return;
251
+ if (type === NOTIF_TYPE.ACTION && !s.triggerAction) return;
252
+ if (type === NOTIF_TYPE.ASK && !s.triggerAsk) return;
253
+ if (type === NOTIF_TYPE.ERROR && !s.triggerError) return;
254
+
255
+ // Legacy: if actionOnly is enabled, only notify on ACTION (overrides granular triggers)
256
+ if (s.actionOnly && type !== NOTIF_TYPE.ACTION) return;
257
+
258
+ const title = _TOAST_TITLES[type] || 'Notification';
259
+ const body = label || 'Claude Code';
260
+
261
+ // Sound
262
+ if (s.sound) playNotifSound(type);
263
+
264
+ // In-app toast
265
+ _showToast(type, title, body, source, opts);
266
+
267
+ // OS banner (only when tab hidden, unless bannerFocused)
268
+ if (s.banner && Notification.permission === 'granted') {
269
+ if (document.hidden || s.bannerFocused) {
270
+ _sendBanner(title, body, `synabun-${source}-${Date.now()}`, { source, ...opts });
271
+ }
272
+ }
273
+ }
274
+
275
+ // ── OS banner click routing (service worker → client) ──
276
+
277
+ if (navigator.serviceWorker) {
278
+ navigator.serviceWorker.addEventListener('message', (e) => {
279
+ if (e.data?.type !== 'NOTIFICATION_CLICK') return;
280
+ const { source, sessionId, tabId } = e.data;
281
+ const event = source === 'panel' ? 'claude-panel:show' : 'terminal:show';
282
+ emit(event, { sessionId, tabId });
283
+ });
284
+ }
285
+
286
+ // ── Exports for settings test buttons ──
287
+ export { _sendBanner as sendTestBanner, SOUND_PRESETS };