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,145 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Search
3
+ // Debounced semantic search with result highlighting
4
+ // ═══════════════════════════════════════════
5
+
6
+ import { state, emit } from './state.js';
7
+ import { searchMemories } from './api.js';
8
+ import { t, tp } from './i18n.js';
9
+ import { registerAction } from './ui-keybinds.js';
10
+
11
+ const $ = (id) => document.getElementById(id);
12
+
13
+ // ── Internal state ──
14
+ let _searchTimeout = null;
15
+ const DEBOUNCE_MS = 400;
16
+
17
+ // ═══════════════════════════════════════════
18
+ // PUBLIC API
19
+ // ═══════════════════════════════════════════
20
+
21
+ /**
22
+ * Execute a semantic search against the API and update shared state.
23
+ * Emits `search:apply` with the Set of matching IDs on success.
24
+ * Variants should listen for this event to handle camera/centering.
25
+ *
26
+ * @param {string} query The search query string
27
+ */
28
+ export async function performSearch(query) {
29
+ const $searchBadge = $('search-badge');
30
+ const $searchClear = $('search-clear');
31
+ const $statSearch = $('stat-search-status');
32
+
33
+ try {
34
+ const data = await searchMemories(query);
35
+
36
+ if (data.results && data.results.length > 0) {
37
+ const ids = new Set(data.results.map(r => r.id));
38
+ state.searchResults = ids;
39
+ state.searchQuery = query;
40
+
41
+ if ($searchBadge) {
42
+ $searchBadge.textContent = data.results.length;
43
+ $searchBadge.classList.add('visible');
44
+ }
45
+ if ($searchClear) $searchClear.style.display = 'none'; // badge takes its place
46
+ if ($statSearch) $statSearch.textContent = tp('search.resultCount', data.results.length);
47
+
48
+ // Emit for variant-specific camera handling
49
+ emit('search:apply', { ids, results: data.results });
50
+ } else {
51
+ state.searchResults = new Set(); // empty set = no matches
52
+ state.searchQuery = query;
53
+
54
+ if ($searchBadge) {
55
+ $searchBadge.textContent = '0';
56
+ $searchBadge.classList.add('visible');
57
+ }
58
+ if ($searchClear) $searchClear.style.display = 'none';
59
+ if ($statSearch) $statSearch.textContent = t('search.noResults');
60
+
61
+ emit('search:apply', { ids: new Set(), results: [] });
62
+ }
63
+
64
+ emit('stats:update');
65
+ } catch (err) {
66
+ console.error('Search error:', err);
67
+ if ($statSearch) $statSearch.textContent = t('search.searchError');
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Clear the current search — reset state, hide badges, and
73
+ * emit `search:clear` so variants can restore full visibility.
74
+ */
75
+ export function clearSearch() {
76
+ state.searchResults = null;
77
+ state.searchQuery = '';
78
+
79
+ const $searchInput = $('search-input');
80
+ const $searchBadge = $('search-badge');
81
+ const $searchClear = $('search-clear');
82
+ const $statSearch = $('stat-search-status');
83
+
84
+ if ($searchInput) $searchInput.value = '';
85
+ if ($searchBadge) $searchBadge.classList.remove('visible');
86
+ if ($searchClear) $searchClear.style.display = 'none';
87
+ if ($statSearch) $statSearch.textContent = '';
88
+
89
+ emit('search:clear');
90
+ emit('stats:update');
91
+ }
92
+
93
+ // ═══════════════════════════════════════════
94
+ // INIT
95
+ // ═══════════════════════════════════════════
96
+
97
+ /**
98
+ * Initialize the search UI. Wires up:
99
+ * - Input focus/blur styling on the search wrapper
100
+ * - Debounced input handler that triggers performSearch
101
+ * - Clear button that resets search state
102
+ */
103
+ export function initSearch() {
104
+ const $searchInput = $('search-input');
105
+ const $searchWrapper = $('search-wrapper');
106
+ const $searchClear = $('search-clear');
107
+ const $searchBadge = $('search-badge');
108
+ const $statSearch = $('stat-search-status');
109
+
110
+ if (!$searchInput) return;
111
+
112
+ // ── Focus styling ──
113
+ if ($searchWrapper) {
114
+ $searchInput.addEventListener('focus', () => $searchWrapper.classList.add('focused'));
115
+ $searchInput.addEventListener('blur', () => $searchWrapper.classList.remove('focused'));
116
+ }
117
+
118
+ // ── Debounced input ──
119
+ $searchInput.addEventListener('input', () => {
120
+ clearTimeout(_searchTimeout);
121
+ const query = $searchInput.value.trim();
122
+
123
+ if (!query) {
124
+ clearSearch();
125
+ return;
126
+ }
127
+
128
+ if ($searchClear) $searchClear.style.display = 'block';
129
+ if ($searchBadge) $searchBadge.classList.remove('visible');
130
+ if ($statSearch) $statSearch.textContent = t('search.searching');
131
+
132
+ _searchTimeout = setTimeout(() => performSearch(query), DEBOUNCE_MS);
133
+ });
134
+
135
+ // ── Clear button ──
136
+ if ($searchClear) {
137
+ $searchClear.addEventListener('click', () => {
138
+ clearSearch();
139
+ $searchInput.focus();
140
+ });
141
+ }
142
+
143
+ // Global focus-search shortcut (via central keybinds)
144
+ registerAction('focus-search', () => $searchInput.focus());
145
+ }
@@ -0,0 +1,531 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — Session Monitor Window
3
+ // Real-time view of active Claude Code sessions, agents, loops, and leak detection.
4
+ // Proper draggable/resizable window with tabbed sections.
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { emit, on } from './state.js';
8
+
9
+ // ── State ──
10
+ let ws = null;
11
+ let reconnectTimer = null;
12
+ let sessions = [];
13
+ let leaks = [];
14
+ let agents = [];
15
+ let _panel = null;
16
+ let _backdrop = null;
17
+ let _activeTab = 'sessions'; // 'sessions' | 'agents' | 'leaks'
18
+ let isVisible = false;
19
+
20
+ // ── Icons ──
21
+ const ICON_TERMINAL = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2" width="13" height="12" rx="2"/><path d="M4.5 6l2.5 2-2.5 2M8.5 10h3"/></svg>';
22
+ const ICON_FLOATING = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6h12"/><rect x="5" y="1" width="6" height="3" rx="1" fill="none"/></svg>';
23
+ const ICON_SIDEPANEL = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2" width="13" height="12" rx="2"/><path d="M10.5 2v12"/></svg>';
24
+ const ICON_EXTERNAL = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2h4v4"/><path d="M14 2L7 9"/><path d="M12 9v4.5a1.5 1.5 0 01-1.5 1.5h-8A1.5 1.5 0 011 13.5v-8A1.5 1.5 0 012.5 4H7"/></svg>';
25
+ const ICON_WARNING = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1L1 14h14L8 1z"/><path d="M8 6v4M8 12v.5"/></svg>';
26
+ const ICON_CRITICAL = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5"/></svg>';
27
+ const ICON_CLEANUP = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V2.5h6V4M3.5 4v9.5a1 1 0 001 1h7a1 1 0 001-1V4"/></svg>';
28
+ const ICON_REFRESH = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 019.2-4M13.5 8a5.5 5.5 0 01-9.2 4"/><path d="M11.5 2v2.5H14M4.5 14v-2.5H2"/></svg>';
29
+ const ICON_AGENT = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6"/></svg>';
30
+ const ICON_STOP = '<svg viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="8" height="8" rx="1"/></svg>';
31
+ const ICON_SESSIONS = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2" width="13" height="10" rx="2"/><path d="M5.5 14h5M8 12v2"/></svg>';
32
+ const ICON_LEAKS = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v3M8 11v3"/><path d="M4 5l1.5 2M10.5 9L12 11"/><path d="M2 8h3M11 8h3"/><path d="M4 11l1.5-2M10.5 7L12 5"/></svg>';
33
+
34
+ const TYPE_ICONS = { terminal: ICON_TERMINAL, floating: ICON_FLOATING, sidepanel: ICON_SIDEPANEL, external: ICON_EXTERNAL };
35
+ const TYPE_LABELS = { terminal: 'Terminal', floating: 'Floating', sidepanel: 'Side Panel', external: 'External' };
36
+
37
+ // ── WebSocket ──
38
+
39
+ function connectWs() {
40
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
41
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
42
+ ws = new WebSocket(`${proto}//${location.host}/ws/sessions`);
43
+ ws.addEventListener('open', () => clearTimeout(reconnectTimer));
44
+ ws.addEventListener('message', (e) => { try { handleWsMessage(JSON.parse(e.data)); } catch {} });
45
+ ws.addEventListener('close', () => { reconnectTimer = setTimeout(connectWs, 5000); });
46
+ ws.addEventListener('error', () => { try { ws.close(); } catch {} });
47
+ }
48
+
49
+ function handleWsMessage(msg) {
50
+ switch (msg.type) {
51
+ case 'session:init':
52
+ sessions = msg.sessions || [];
53
+ leaks = msg.leaks || [];
54
+ fetch('/api/agents').then(r => r.json()).then(r => { agents = r.agents || []; render(); }).catch(() => render());
55
+ return;
56
+ case 'session:registered': {
57
+ const idx = sessions.findIndex(s => s.claudeSessionId === msg.session.claudeSessionId);
58
+ if (idx >= 0) sessions[idx] = msg.session;
59
+ else sessions.push(msg.session);
60
+ render();
61
+ break;
62
+ }
63
+ case 'session:unregistered':
64
+ sessions = sessions.filter(s => s.claudeSessionId !== msg.claudeSessionId);
65
+ render();
66
+ break;
67
+ case 'session:leaks':
68
+ leaks = msg.leaks || [];
69
+ render();
70
+ break;
71
+ }
72
+ }
73
+
74
+ // ── Panel HTML ──
75
+
76
+ function buildPanelHTML() {
77
+ const critCount = leaks.filter(l => l.severity === 'critical').length;
78
+ const warnCount = leaks.filter(l => l.severity === 'warning').length;
79
+ const infoCount = leaks.filter(l => l.severity === 'info').length;
80
+ const leakTotal = critCount + warnCount + infoCount;
81
+ const runningAgents = agents.filter(a => a.status === 'running').length;
82
+
83
+ return `
84
+ <div class="resize-handle resize-handle-t" data-resize="t"></div>
85
+ <div class="resize-handle resize-handle-r" data-resize="r"></div>
86
+ <div class="resize-handle resize-handle-b" data-resize="b"></div>
87
+ <div class="resize-handle resize-handle-l" data-resize="l"></div>
88
+ <div class="resize-handle resize-handle-tl" data-resize="tl"></div>
89
+ <div class="resize-handle resize-handle-tr" data-resize="tr"></div>
90
+ <div class="resize-handle resize-handle-bl" data-resize="bl"></div>
91
+ <div class="resize-handle resize-handle-br" data-resize="br"></div>
92
+
93
+ <div class="sm-header drag-handle" data-drag="session-monitor-panel">
94
+ <div class="sm-header-left">
95
+ <h3>Session Monitor</h3>
96
+ <span class="sm-header-badge" id="sm-session-count">${sessions.length} sessions</span>
97
+ ${critCount > 0 ? `<span class="sm-header-badge sm-badge-critical">${critCount} critical</span>` : ''}
98
+ </div>
99
+ <div class="sm-header-actions">
100
+ <button class="sm-header-btn" id="sm-refresh" title="Refresh">${ICON_REFRESH}</button>
101
+ <button class="backdrop-toggle-btn" id="sm-backdrop-toggle" data-tooltip="Toggle backdrop">
102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
103
+ </button>
104
+ <button class="sm-close" id="sm-close">&times;</button>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="sm-tabs" id="sm-tabs">
109
+ <button class="sm-tab active" data-tab="sessions">
110
+ ${ICON_SESSIONS}
111
+ <span>Sessions</span>
112
+ <span class="sm-tab-count">${sessions.length}</span>
113
+ </button>
114
+ <button class="sm-tab" data-tab="agents">
115
+ ${ICON_AGENT}
116
+ <span>Agents</span>
117
+ ${runningAgents > 0 ? `<span class="sm-tab-count sm-tab-count-active">${runningAgents}</span>` : `<span class="sm-tab-count">${agents.length}</span>`}
118
+ </button>
119
+ <button class="sm-tab" data-tab="leaks">
120
+ ${ICON_LEAKS}
121
+ <span>Health</span>
122
+ ${critCount > 0 ? `<span class="sm-tab-count sm-tab-count-critical">${leakTotal}</span>` : warnCount > 0 ? `<span class="sm-tab-count sm-tab-count-warning">${leakTotal}</span>` : `<span class="sm-tab-count">${leakTotal}</span>`}
123
+ </button>
124
+ </div>
125
+
126
+ <div class="sm-body" id="sm-body"></div>
127
+ `;
128
+ }
129
+
130
+ // ── Tab renderers ──
131
+
132
+ function renderSessionsTab() {
133
+ if (sessions.length === 0) {
134
+ return `<div class="sm-empty-state">
135
+ <div class="sm-empty-icon">${ICON_SESSIONS}</div>
136
+ <div class="sm-empty-title">No active sessions</div>
137
+ <div class="sm-empty-desc">Claude Code sessions will appear here when they connect.</div>
138
+ </div>`;
139
+ }
140
+
141
+ let html = '<div class="sm-list">';
142
+ for (const s of sessions) {
143
+ const typeIcon = TYPE_ICONS[s.terminalType] || ICON_EXTERNAL;
144
+ const typeLabel = TYPE_LABELS[s.terminalType] || s.terminalType || 'Unknown';
145
+ const alive = s.isAlive !== false;
146
+ const age = formatAge(s.connectedAt);
147
+ const lastAct = formatAge(s.lastActivity);
148
+ const projectName = s.project || shortPath(s.cwd) || 'unknown';
149
+ const hasLeaks = leaks.some(l => l.sessions?.includes(s.claudeSessionId) || l.sessionId === s.claudeSessionId);
150
+
151
+ html += `<div class="sm-card ${alive ? '' : 'sm-card-dead'} ${hasLeaks ? 'sm-card-leak' : ''}">`;
152
+ html += `<div class="sm-card-header">`;
153
+ html += `<span class="sm-card-type" title="${typeLabel}">${typeIcon}</span>`;
154
+ html += `<span class="sm-card-project">${esc(projectName)}</span>`;
155
+ html += `<span class="sm-card-type-label">${typeLabel}</span>`;
156
+ html += `<span class="sm-card-pulse ${alive ? 'sm-pulse-active' : 'sm-pulse-dead'}"></span>`;
157
+ html += `</div>`;
158
+ html += `<div class="sm-card-details">`;
159
+ html += `<div class="sm-card-row"><span class="sm-card-key">ID</span><span class="sm-card-val mono">${s.claudeSessionId.slice(0, 8)}</span></div>`;
160
+ html += `<div class="sm-card-row"><span class="sm-card-key">Uptime</span><span class="sm-card-val">${age}</span></div>`;
161
+ html += `<div class="sm-card-row"><span class="sm-card-key">Last active</span><span class="sm-card-val">${lastAct} ago</span></div>`;
162
+ if (s.pid) html += `<div class="sm-card-row"><span class="sm-card-key">PID</span><span class="sm-card-val mono">${s.pid}</span></div>`;
163
+ if (s.cwd) html += `<div class="sm-card-row"><span class="sm-card-key">CWD</span><span class="sm-card-val mono" title="${esc(s.cwd)}">${esc(shortPath(s.cwd))}</span></div>`;
164
+ html += `</div>`;
165
+ if (hasLeaks) {
166
+ html += `<div class="sm-card-warning">Has associated leaks</div>`;
167
+ }
168
+ html += `</div>`;
169
+ }
170
+ html += '</div>';
171
+ return html;
172
+ }
173
+
174
+ function renderAgentsTab() {
175
+ const runningAgents = agents.filter(a => a.status === 'running');
176
+ const finishedAgents = agents.filter(a => a.status !== 'running');
177
+
178
+ if (agents.length === 0) {
179
+ return `<div class="sm-empty-state">
180
+ <div class="sm-empty-icon">${ICON_AGENT}</div>
181
+ <div class="sm-empty-title">No agents</div>
182
+ <div class="sm-empty-desc">Running and recent agents will appear here.</div>
183
+ </div>`;
184
+ }
185
+
186
+ let html = '';
187
+
188
+ if (runningAgents.length > 0) {
189
+ html += `<div class="sm-section-label">Running <span class="sm-section-count">${runningAgents.length}</span></div>`;
190
+ html += '<div class="sm-list">';
191
+ for (const a of runningAgents) {
192
+ html += renderAgentCard(a);
193
+ }
194
+ html += '</div>';
195
+ }
196
+
197
+ if (finishedAgents.length > 0) {
198
+ html += `<div class="sm-section-label">Recent <span class="sm-section-count">${finishedAgents.length}</span></div>`;
199
+ html += '<div class="sm-list">';
200
+ for (const a of finishedAgents) {
201
+ html += renderAgentCard(a);
202
+ }
203
+ html += '</div>';
204
+ }
205
+
206
+ return html;
207
+ }
208
+
209
+ function renderAgentCard(a) {
210
+ const isRunning = a.status === 'running';
211
+ const statusCls = isRunning ? 'sm-agent-running' : a.status === 'completed' ? 'sm-agent-done' : a.status === 'failed' ? 'sm-agent-fail' : 'sm-agent-stopped';
212
+ const statusLabel = a.status.charAt(0).toUpperCase() + a.status.slice(1);
213
+ const iterLabel = a.mode === 'loop' && a.totalIterations > 1 ? `${a.currentIteration}/${a.totalIterations}` : '';
214
+ const duration = a.startedAt ? formatAge(a.startedAt) : '';
215
+
216
+ let html = `<div class="sm-card ${statusCls}">`;
217
+ html += `<div class="sm-card-header">`;
218
+ html += `<span class="sm-agent-badge ${statusCls}">${statusLabel}</span>`;
219
+ if (iterLabel) html += `<span class="sm-agent-iter">${iterLabel}</span>`;
220
+ html += `<span class="sm-card-spacer"></span>`;
221
+ if (isRunning) {
222
+ html += `<button class="sm-btn sm-btn-stop" data-agent-stop="${a.id}" title="Stop agent">${ICON_STOP} Stop</button>`;
223
+ } else {
224
+ html += `<button class="sm-btn sm-btn-remove" data-agent-remove="${a.id}" title="Remove">&times;</button>`;
225
+ }
226
+ html += `</div>`;
227
+ html += `<div class="sm-card-details">`;
228
+ if (a.task) html += `<div class="sm-card-row"><span class="sm-card-key">Task</span><span class="sm-card-val">${esc(a.task)}</span></div>`;
229
+ if (a.mode) html += `<div class="sm-card-row"><span class="sm-card-key">Mode</span><span class="sm-card-val">${a.mode}</span></div>`;
230
+ if (duration) html += `<div class="sm-card-row"><span class="sm-card-key">${isRunning ? 'Running' : 'Ran'}</span><span class="sm-card-val">${duration}</span></div>`;
231
+ if (a.costUsd != null) html += `<div class="sm-card-row"><span class="sm-card-key">Cost</span><span class="sm-card-val">$${a.costUsd.toFixed(4)}</span></div>`;
232
+ html += `</div>`;
233
+ html += `</div>`;
234
+ return html;
235
+ }
236
+
237
+ function renderLeaksTab() {
238
+ const criticalLeaks = leaks.filter(l => l.severity === 'critical');
239
+ const warningLeaks = leaks.filter(l => l.severity === 'warning');
240
+ const infoLeaks = leaks.filter(l => l.severity === 'info');
241
+
242
+ if (leaks.length === 0) {
243
+ return `<div class="sm-empty-state">
244
+ <div class="sm-empty-icon sm-empty-ok">${ICON_LEAKS}</div>
245
+ <div class="sm-empty-title">All clear</div>
246
+ <div class="sm-empty-desc">No leaks or orphaned files detected.</div>
247
+ </div>`;
248
+ }
249
+
250
+ let html = '';
251
+
252
+ if (criticalLeaks.length > 0) {
253
+ html += `<div class="sm-section-label sm-section-critical">Critical <span class="sm-section-count">${criticalLeaks.length}</span></div>`;
254
+ html += '<div class="sm-list">';
255
+ for (const l of criticalLeaks) html += renderLeakCard(l);
256
+ html += '</div>';
257
+ }
258
+
259
+ if (warningLeaks.length > 0) {
260
+ html += `<div class="sm-section-label sm-section-warning">Warnings <span class="sm-section-count">${warningLeaks.length}</span></div>`;
261
+ html += '<div class="sm-list">';
262
+ for (const l of warningLeaks) html += renderLeakCard(l);
263
+ html += '</div>';
264
+ }
265
+
266
+ if (infoLeaks.length > 0) {
267
+ html += `<div class="sm-section-label">Orphaned files <span class="sm-section-count">${infoLeaks.length}</span></div>`;
268
+ html += '<div class="sm-list">';
269
+ for (const l of infoLeaks) html += renderLeakCard(l);
270
+ html += '</div>';
271
+ html += `<div class="sm-cleanup-bar">
272
+ <span>${infoLeaks.length} orphaned file${infoLeaks.length > 1 ? 's' : ''} can be cleaned up</span>
273
+ <button class="sm-btn sm-btn-cleanup" id="sm-cleanup-btn">${ICON_CLEANUP} Clean All</button>
274
+ </div>`;
275
+ }
276
+
277
+ return html;
278
+ }
279
+
280
+ function renderLeakCard(l) {
281
+ const icon = l.severity === 'critical' ? ICON_CRITICAL : l.severity === 'warning' ? ICON_WARNING : ICON_CLEANUP;
282
+ const cls = l.severity === 'critical' ? 'sm-card-critical' : l.severity === 'warning' ? 'sm-card-warning' : 'sm-card-info';
283
+ const age = l.ageMs ? formatDuration(l.ageMs) : '';
284
+
285
+ let html = `<div class="sm-card ${cls}">`;
286
+ html += `<div class="sm-leak-header">`;
287
+ html += `<span class="sm-leak-icon">${icon}</span>`;
288
+ html += `<span class="sm-leak-desc">${esc(l.description)}</span>`;
289
+ html += `</div>`;
290
+ html += `<div class="sm-card-details">`;
291
+ html += `<div class="sm-card-row"><span class="sm-card-key">Type</span><span class="sm-card-val">${esc(l.type)}</span></div>`;
292
+ if (age) html += `<div class="sm-card-row"><span class="sm-card-key">Age</span><span class="sm-card-val">${age}</span></div>`;
293
+ if (l.file) html += `<div class="sm-card-row"><span class="sm-card-key">File</span><span class="sm-card-val mono" title="${esc(l.file)}">${esc(shortPath(l.file))}</span></div>`;
294
+ if (l.cwd) html += `<div class="sm-card-row"><span class="sm-card-key">CWD</span><span class="sm-card-val mono" title="${esc(l.cwd)}">${esc(shortPath(l.cwd))}</span></div>`;
295
+ if (l.sessions) html += `<div class="sm-card-row"><span class="sm-card-key">Sessions</span><span class="sm-card-val mono">${l.sessions.map(s => s.slice(0, 8)).join(', ')}</span></div>`;
296
+ if (l.loopData) {
297
+ html += `<div class="sm-card-row"><span class="sm-card-key">Loop</span><span class="sm-card-val">${esc(l.loopData.template)} (${l.loopData.currentIteration}/${l.loopData.maxIterations})</span></div>`;
298
+ }
299
+ html += `</div>`;
300
+ html += `</div>`;
301
+ return html;
302
+ }
303
+
304
+ // ── Master render ──
305
+
306
+ function render() {
307
+ if (!_panel || !isVisible) return;
308
+ const body = _panel.querySelector('#sm-body');
309
+ if (!body) return;
310
+
311
+ // Update header badges
312
+ const countBadge = _panel.querySelector('#sm-session-count');
313
+ if (countBadge) countBadge.textContent = `${sessions.length} session${sessions.length !== 1 ? 's' : ''}`;
314
+
315
+ // Update tab counts
316
+ const critCount = leaks.filter(l => l.severity === 'critical').length;
317
+ const warnCount = leaks.filter(l => l.severity === 'warning').length;
318
+ const leakTotal = leaks.length;
319
+ const runningAgents = agents.filter(a => a.status === 'running').length;
320
+
321
+ const tabs = _panel.querySelectorAll('.sm-tab');
322
+ tabs.forEach(tab => {
323
+ const tabName = tab.dataset.tab;
324
+ const countEl = tab.querySelector('.sm-tab-count');
325
+ if (!countEl) return;
326
+ if (tabName === 'sessions') {
327
+ countEl.textContent = sessions.length;
328
+ } else if (tabName === 'agents') {
329
+ countEl.textContent = runningAgents > 0 ? runningAgents : agents.length;
330
+ countEl.className = `sm-tab-count${runningAgents > 0 ? ' sm-tab-count-active' : ''}`;
331
+ } else if (tabName === 'leaks') {
332
+ countEl.textContent = leakTotal;
333
+ countEl.className = `sm-tab-count${critCount > 0 ? ' sm-tab-count-critical' : warnCount > 0 ? ' sm-tab-count-warning' : ''}`;
334
+ }
335
+ });
336
+
337
+ // Render active tab content
338
+ let html = '';
339
+ switch (_activeTab) {
340
+ case 'sessions': html = renderSessionsTab(); break;
341
+ case 'agents': html = renderAgentsTab(); break;
342
+ case 'leaks': html = renderLeaksTab(); break;
343
+ }
344
+ body.innerHTML = html;
345
+
346
+ // Wire event listeners
347
+ body.querySelectorAll('[data-agent-stop]').forEach(btn => {
348
+ btn.addEventListener('click', () => stopAgent(btn.dataset.agentStop));
349
+ });
350
+ body.querySelectorAll('[data-agent-remove]').forEach(btn => {
351
+ btn.addEventListener('click', () => removeAgent(btn.dataset.agentRemove));
352
+ });
353
+ const cleanupBtn = body.querySelector('#sm-cleanup-btn');
354
+ if (cleanupBtn) cleanupBtn.addEventListener('click', cleanupOrphans);
355
+ }
356
+
357
+ // ── Actions ──
358
+
359
+ async function refreshSessions() {
360
+ try {
361
+ const [sessRes, leakRes, agentRes] = await Promise.all([
362
+ fetch('/api/sessions/active').then(r => r.json()),
363
+ fetch('/api/sessions/leaks').then(r => r.json()),
364
+ fetch('/api/agents').then(r => r.json()).catch(() => ({ agents: [] })),
365
+ ]);
366
+ sessions = sessRes.sessions || [];
367
+ leaks = leakRes.leaks || [];
368
+ agents = agentRes.agents || [];
369
+ render();
370
+ } catch {}
371
+ }
372
+
373
+ async function stopAgent(agentId) {
374
+ try {
375
+ await fetch(`/api/agents/${agentId}/stop`, { method: 'POST' });
376
+ await refreshSessions();
377
+ } catch {}
378
+ }
379
+
380
+ async function removeAgent(agentId) {
381
+ try {
382
+ await fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
383
+ await refreshSessions();
384
+ } catch {}
385
+ }
386
+
387
+ async function cleanupOrphans() {
388
+ try {
389
+ const res = await fetch('/api/sessions/cleanup', {
390
+ method: 'POST',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify({ types: ['orphaned-state', 'stale-precompact'] }),
393
+ }).then(r => r.json());
394
+ if (res.ok) await refreshSessions();
395
+ } catch {}
396
+ }
397
+
398
+ // ── Helpers ──
399
+
400
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
401
+
402
+ function shortPath(p) {
403
+ if (!p) return '';
404
+ const parts = p.replace(/\\/g, '/').split('/');
405
+ return parts.slice(-2).join('/');
406
+ }
407
+
408
+ function formatAge(ts) {
409
+ if (!ts) return '?';
410
+ const diff = Date.now() - ts;
411
+ if (diff < 60000) return '<1m';
412
+ if (diff < 3600000) return Math.round(diff / 60000) + 'm';
413
+ if (diff < 86400000) return Math.round(diff / 3600000) + 'h';
414
+ return Math.round(diff / 86400000) + 'd';
415
+ }
416
+
417
+ function formatDuration(ms) {
418
+ if (ms < 60000) return Math.round(ms / 1000) + 's';
419
+ if (ms < 3600000) return Math.round(ms / 60000) + 'm';
420
+ return Math.round(ms / 3600000) + 'h';
421
+ }
422
+
423
+ // ── Panel open/close ──
424
+
425
+ function openPanel() {
426
+ if (_panel) { _panel.focus(); return; }
427
+
428
+ _backdrop = document.createElement('div');
429
+ _backdrop.className = 'sm-backdrop';
430
+ // Backdrop click disabled — close only via ESC or close button
431
+ document.body.appendChild(_backdrop);
432
+
433
+ _panel = document.createElement('div');
434
+ _panel.className = 'session-monitor-panel glass resizable';
435
+ _panel.id = 'session-monitor-panel';
436
+ _panel.innerHTML = buildPanelHTML();
437
+ document.body.appendChild(_panel);
438
+
439
+ // Center at default size
440
+ _panel.style.left = Math.max(20, (window.innerWidth - 640) / 2) + 'px';
441
+ _panel.style.top = Math.max(48, (window.innerHeight - 480) / 2) + 'px';
442
+
443
+ wirePanel();
444
+ isVisible = true;
445
+ connectWs();
446
+ refreshSessions();
447
+
448
+ // ESC key to close
449
+ const onEsc = (e) => {
450
+ if (e.key === 'Escape' && _panel) { closePanel(); document.removeEventListener('keydown', onEsc); }
451
+ };
452
+ document.addEventListener('keydown', onEsc);
453
+
454
+ requestAnimationFrame(() => {
455
+ _backdrop.classList.add('open');
456
+ _panel.classList.add('open');
457
+ });
458
+ }
459
+
460
+ function closePanel() {
461
+ if (!_panel) return;
462
+ if (_backdrop) { _backdrop.remove(); _backdrop = null; }
463
+ _panel.remove();
464
+ _panel = null;
465
+ isVisible = false;
466
+ _activeTab = 'sessions';
467
+ emit('session-monitor:closed');
468
+ }
469
+
470
+ function wirePanel() {
471
+ const closeBtn = _panel.querySelector('#sm-close');
472
+ if (closeBtn) closeBtn.addEventListener('click', closePanel);
473
+
474
+ const refreshBtn = _panel.querySelector('#sm-refresh');
475
+ if (refreshBtn) refreshBtn.addEventListener('click', refreshSessions);
476
+
477
+ const bdToggle = _panel.querySelector('#sm-backdrop-toggle');
478
+ if (bdToggle) {
479
+ bdToggle.addEventListener('click', () => {
480
+ if (_backdrop) {
481
+ _backdrop.classList.toggle('backdrop-hidden');
482
+ bdToggle.classList.toggle('active', _backdrop.classList.contains('backdrop-hidden'));
483
+ }
484
+ });
485
+ }
486
+
487
+ // Tab switching
488
+ const tabBar = _panel.querySelector('#sm-tabs');
489
+ if (tabBar) {
490
+ tabBar.addEventListener('click', (e) => {
491
+ const tab = e.target.closest('.sm-tab');
492
+ if (!tab) return;
493
+ const name = tab.dataset.tab;
494
+ if (name === _activeTab) return;
495
+ _activeTab = name;
496
+ tabBar.querySelectorAll('.sm-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
497
+ render();
498
+ });
499
+ }
500
+ }
501
+
502
+ // ── Exports ──
503
+
504
+ export function showSessionMonitor() { openPanel(); }
505
+ export function hideSessionMonitor() { closePanel(); }
506
+ export function toggleSessionMonitor() {
507
+ if (_panel) closePanel();
508
+ else openPanel();
509
+ }
510
+
511
+ // Widget mode for claude-chat skin
512
+ export function mountSessionWidget(container) {
513
+ const widget = document.createElement('div');
514
+ widget.className = 'session-widget';
515
+ widget.innerHTML = `<div class="sm-body" id="sm-body"></div>`;
516
+ container.appendChild(widget);
517
+ _panel = widget;
518
+ _activeTab = 'sessions';
519
+ isVisible = true;
520
+ connectWs();
521
+ return widget;
522
+ }
523
+
524
+ // Kept for backwards compat
525
+ export function createSessionMonitorPanel() { return null; }
526
+
527
+ // Public state getters
528
+ export function getActiveSessions() { return sessions; }
529
+ export function getLeaks() { return leaks; }
530
+ export function getSessionCount() { return sessions.length; }
531
+ export function getCriticalLeakCount() { return leaks.filter(l => l.severity === 'critical').length; }