synabun 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,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">×</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">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
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; }
|