ai-agent-session-center 1.0.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/README.md +618 -0
- package/bin/cli.js +20 -0
- package/hooks/dashboard-hook-codex.sh +67 -0
- package/hooks/dashboard-hook-gemini.sh +102 -0
- package/hooks/dashboard-hook.ps1 +147 -0
- package/hooks/dashboard-hook.sh +142 -0
- package/hooks/dashboard-hooks-backup.json +103 -0
- package/hooks/install-hooks.js +543 -0
- package/hooks/reset.js +357 -0
- package/hooks/setup-wizard.js +156 -0
- package/package.json +52 -0
- package/public/css/dashboard.css +10200 -0
- package/public/index.html +915 -0
- package/public/js/analyticsPanel.js +467 -0
- package/public/js/app.js +1148 -0
- package/public/js/browserDb.js +806 -0
- package/public/js/chartUtils.js +383 -0
- package/public/js/historyPanel.js +298 -0
- package/public/js/movementManager.js +155 -0
- package/public/js/navController.js +32 -0
- package/public/js/robotManager.js +526 -0
- package/public/js/sceneManager.js +7 -0
- package/public/js/sessionPanel.js +2477 -0
- package/public/js/settingsManager.js +924 -0
- package/public/js/soundManager.js +249 -0
- package/public/js/statsPanel.js +118 -0
- package/public/js/terminalManager.js +391 -0
- package/public/js/timelinePanel.js +278 -0
- package/public/js/wsClient.js +88 -0
- package/server/apiRouter.js +321 -0
- package/server/config.js +120 -0
- package/server/hookProcessor.js +55 -0
- package/server/hookRouter.js +18 -0
- package/server/hookStats.js +107 -0
- package/server/index.js +314 -0
- package/server/logger.js +67 -0
- package/server/mqReader.js +218 -0
- package/server/serverConfig.js +27 -0
- package/server/sessionStore.js +1049 -0
- package/server/sshManager.js +339 -0
- package/server/wsManager.js +83 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// soundManager.js — Per-action configurable sound effects via Web Audio API
|
|
2
|
+
import * as settingsManager from './settingsManager.js';
|
|
3
|
+
|
|
4
|
+
let audioCtx = null;
|
|
5
|
+
let enabled = true;
|
|
6
|
+
let volume = 0.5;
|
|
7
|
+
let actionSounds = {}; // action -> soundName mapping
|
|
8
|
+
|
|
9
|
+
function getCtx() {
|
|
10
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
11
|
+
return audioCtx;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---- Synthesis helpers ----
|
|
15
|
+
|
|
16
|
+
function playTone(freq, duration, type = 'sine', vol = 1) {
|
|
17
|
+
const ctx = getCtx();
|
|
18
|
+
const osc = ctx.createOscillator();
|
|
19
|
+
const gain = ctx.createGain();
|
|
20
|
+
osc.type = type;
|
|
21
|
+
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
|
22
|
+
gain.gain.setValueAtTime(vol * volume * 0.3, ctx.currentTime);
|
|
23
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
|
24
|
+
osc.connect(gain);
|
|
25
|
+
gain.connect(ctx.destination);
|
|
26
|
+
osc.start(ctx.currentTime);
|
|
27
|
+
osc.stop(ctx.currentTime + duration);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function playSequence(freqs, spacing = 0.1, duration = 0.15, type = 'sine') {
|
|
31
|
+
freqs.forEach((f, i) => {
|
|
32
|
+
setTimeout(() => playTone(f, duration, type), i * spacing * 1000);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- Sound Library ----
|
|
37
|
+
|
|
38
|
+
const soundLibrary = {
|
|
39
|
+
chirp: () => playTone(1200, 0.08, 'sine'),
|
|
40
|
+
ping: () => playTone(660, 0.2, 'sine'),
|
|
41
|
+
chime: () => playSequence([523, 659, 784], 0.08, 0.2),
|
|
42
|
+
ding: () => playTone(800, 0.25, 'triangle'),
|
|
43
|
+
blip: () => playTone(880, 0.05, 'square', 0.5),
|
|
44
|
+
swoosh: () => {
|
|
45
|
+
const ctx = getCtx();
|
|
46
|
+
const osc = ctx.createOscillator();
|
|
47
|
+
const gain = ctx.createGain();
|
|
48
|
+
osc.type = 'sine';
|
|
49
|
+
osc.frequency.setValueAtTime(300, ctx.currentTime);
|
|
50
|
+
osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.25);
|
|
51
|
+
gain.gain.setValueAtTime(volume * 0.25, ctx.currentTime);
|
|
52
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
|
53
|
+
osc.connect(gain);
|
|
54
|
+
gain.connect(ctx.destination);
|
|
55
|
+
osc.start(ctx.currentTime);
|
|
56
|
+
osc.stop(ctx.currentTime + 0.3);
|
|
57
|
+
},
|
|
58
|
+
click: () => playTone(1200, 0.03, 'square', 0.2),
|
|
59
|
+
beep: () => playTone(440, 0.15, 'square', 0.4),
|
|
60
|
+
warble: () => {
|
|
61
|
+
const ctx = getCtx();
|
|
62
|
+
const osc = ctx.createOscillator();
|
|
63
|
+
const lfo = ctx.createOscillator();
|
|
64
|
+
const lfoGain = ctx.createGain();
|
|
65
|
+
const gain = ctx.createGain();
|
|
66
|
+
osc.type = 'sine';
|
|
67
|
+
osc.frequency.setValueAtTime(600, ctx.currentTime);
|
|
68
|
+
lfo.type = 'sine';
|
|
69
|
+
lfo.frequency.setValueAtTime(12, ctx.currentTime);
|
|
70
|
+
lfoGain.gain.setValueAtTime(50, ctx.currentTime);
|
|
71
|
+
lfo.connect(lfoGain);
|
|
72
|
+
lfoGain.connect(osc.frequency);
|
|
73
|
+
gain.gain.setValueAtTime(volume * 0.25, ctx.currentTime);
|
|
74
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
|
75
|
+
osc.connect(gain);
|
|
76
|
+
gain.connect(ctx.destination);
|
|
77
|
+
osc.start(ctx.currentTime);
|
|
78
|
+
lfo.start(ctx.currentTime);
|
|
79
|
+
osc.stop(ctx.currentTime + 0.3);
|
|
80
|
+
lfo.stop(ctx.currentTime + 0.3);
|
|
81
|
+
},
|
|
82
|
+
buzz: () => playTone(200, 0.12, 'sawtooth', 0.4),
|
|
83
|
+
cascade: () => playSequence([784, 659, 523, 392], 0.1, 0.2),
|
|
84
|
+
fanfare: () => playSequence([523, 659, 784, 1047, 1319], 0.08, 0.2),
|
|
85
|
+
alarm: () => playSequence([880, 660, 880, 660], 0.15, 0.15, 'square'),
|
|
86
|
+
thud: () => {
|
|
87
|
+
const ctx = getCtx();
|
|
88
|
+
const osc = ctx.createOscillator();
|
|
89
|
+
const gain = ctx.createGain();
|
|
90
|
+
osc.type = 'sine';
|
|
91
|
+
osc.frequency.setValueAtTime(80, ctx.currentTime);
|
|
92
|
+
osc.frequency.exponentialRampToValueAtTime(30, ctx.currentTime + 0.3);
|
|
93
|
+
gain.gain.setValueAtTime(volume * 0.5, ctx.currentTime);
|
|
94
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
|
|
95
|
+
osc.connect(gain);
|
|
96
|
+
gain.connect(ctx.destination);
|
|
97
|
+
osc.start(ctx.currentTime);
|
|
98
|
+
osc.stop(ctx.currentTime + 0.35);
|
|
99
|
+
},
|
|
100
|
+
urgentAlarm: () => {
|
|
101
|
+
// Loud 3-burst urgent alarm — impossible to miss
|
|
102
|
+
const ctx = getCtx();
|
|
103
|
+
const t = ctx.currentTime;
|
|
104
|
+
for (let burst = 0; burst < 3; burst++) {
|
|
105
|
+
const offset = burst * 0.4;
|
|
106
|
+
// High tone
|
|
107
|
+
const osc1 = ctx.createOscillator();
|
|
108
|
+
const gain1 = ctx.createGain();
|
|
109
|
+
osc1.type = 'square';
|
|
110
|
+
osc1.frequency.setValueAtTime(1000, t + offset);
|
|
111
|
+
osc1.frequency.setValueAtTime(800, t + offset + 0.1);
|
|
112
|
+
osc1.frequency.setValueAtTime(1000, t + offset + 0.2);
|
|
113
|
+
gain1.gain.setValueAtTime(volume * 0.5, t + offset);
|
|
114
|
+
gain1.gain.setValueAtTime(volume * 0.5, t + offset + 0.25);
|
|
115
|
+
gain1.gain.exponentialRampToValueAtTime(0.001, t + offset + 0.3);
|
|
116
|
+
osc1.connect(gain1);
|
|
117
|
+
gain1.connect(ctx.destination);
|
|
118
|
+
osc1.start(t + offset);
|
|
119
|
+
osc1.stop(t + offset + 0.3);
|
|
120
|
+
// Low undertone
|
|
121
|
+
const osc2 = ctx.createOscillator();
|
|
122
|
+
const gain2 = ctx.createGain();
|
|
123
|
+
osc2.type = 'sawtooth';
|
|
124
|
+
osc2.frequency.setValueAtTime(200, t + offset);
|
|
125
|
+
gain2.gain.setValueAtTime(volume * 0.3, t + offset);
|
|
126
|
+
gain2.gain.exponentialRampToValueAtTime(0.001, t + offset + 0.3);
|
|
127
|
+
osc2.connect(gain2);
|
|
128
|
+
gain2.connect(ctx.destination);
|
|
129
|
+
osc2.start(t + offset);
|
|
130
|
+
osc2.stop(t + offset + 0.3);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
none: () => {}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ---- Action types and default mapping ----
|
|
137
|
+
|
|
138
|
+
const defaultActionSounds = {
|
|
139
|
+
// Session events
|
|
140
|
+
sessionStart: 'chime',
|
|
141
|
+
sessionEnd: 'cascade',
|
|
142
|
+
promptSubmit: 'ping',
|
|
143
|
+
taskComplete: 'fanfare',
|
|
144
|
+
// Tool calls
|
|
145
|
+
toolRead: 'click',
|
|
146
|
+
toolWrite: 'blip',
|
|
147
|
+
toolEdit: 'blip',
|
|
148
|
+
toolBash: 'buzz',
|
|
149
|
+
toolGrep: 'click',
|
|
150
|
+
toolGlob: 'click',
|
|
151
|
+
toolWebFetch: 'swoosh',
|
|
152
|
+
toolTask: 'ding',
|
|
153
|
+
toolOther: 'click',
|
|
154
|
+
// System
|
|
155
|
+
approvalNeeded: 'alarm',
|
|
156
|
+
inputNeeded: 'chime',
|
|
157
|
+
alert: 'alarm',
|
|
158
|
+
kill: 'thud',
|
|
159
|
+
archive: 'ding',
|
|
160
|
+
subagentStart: 'chirp',
|
|
161
|
+
subagentStop: 'ping'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const actionLabels = {
|
|
165
|
+
sessionStart: 'Session Start',
|
|
166
|
+
sessionEnd: 'Session End',
|
|
167
|
+
promptSubmit: 'Prompt Submit',
|
|
168
|
+
taskComplete: 'Task Complete',
|
|
169
|
+
toolRead: 'Tool: Read',
|
|
170
|
+
toolWrite: 'Tool: Write',
|
|
171
|
+
toolEdit: 'Tool: Edit',
|
|
172
|
+
toolBash: 'Tool: Bash',
|
|
173
|
+
toolGrep: 'Tool: Grep',
|
|
174
|
+
toolGlob: 'Tool: Glob',
|
|
175
|
+
toolWebFetch: 'Tool: WebFetch',
|
|
176
|
+
toolTask: 'Tool: Task',
|
|
177
|
+
toolOther: 'Tool: Other',
|
|
178
|
+
approvalNeeded: 'Approval Needed',
|
|
179
|
+
inputNeeded: 'Input Needed',
|
|
180
|
+
alert: 'Alert',
|
|
181
|
+
kill: 'Kill',
|
|
182
|
+
archive: 'Archive',
|
|
183
|
+
subagentStart: 'Subagent Start',
|
|
184
|
+
subagentStop: 'Subagent Stop'
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const actionCategories = {
|
|
188
|
+
'Session Events': ['sessionStart', 'sessionEnd', 'promptSubmit', 'taskComplete'],
|
|
189
|
+
'Tool Calls': ['toolRead', 'toolWrite', 'toolEdit', 'toolBash', 'toolGrep', 'toolGlob', 'toolWebFetch', 'toolTask', 'toolOther'],
|
|
190
|
+
'System': ['approvalNeeded', 'inputNeeded', 'alert', 'kill', 'archive', 'subagentStart', 'subagentStop']
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ---- Public API ----
|
|
194
|
+
|
|
195
|
+
export function play(actionName) {
|
|
196
|
+
if (!enabled) return;
|
|
197
|
+
const soundName = actionSounds[actionName] || defaultActionSounds[actionName] || 'none';
|
|
198
|
+
const fn = soundLibrary[soundName];
|
|
199
|
+
if (fn) fn();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function previewSound(soundName) {
|
|
203
|
+
const fn = soundLibrary[soundName];
|
|
204
|
+
if (fn) fn();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getSoundLibrary() {
|
|
208
|
+
return Object.keys(soundLibrary);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getActionSounds() {
|
|
212
|
+
return { ...defaultActionSounds, ...actionSounds };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getActionLabels() {
|
|
216
|
+
return { ...actionLabels };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getActionCategories() {
|
|
220
|
+
return actionCategories;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function setActionSound(action, soundName) {
|
|
224
|
+
actionSounds[action] = soundName;
|
|
225
|
+
settingsManager.set('soundActions', JSON.stringify(actionSounds));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function init() {
|
|
229
|
+
enabled = settingsManager.get('soundEnabled') === 'true';
|
|
230
|
+
volume = parseFloat(settingsManager.get('soundVolume')) || 0.5;
|
|
231
|
+
|
|
232
|
+
// Load per-action config
|
|
233
|
+
const saved = settingsManager.get('soundActions');
|
|
234
|
+
if (saved) {
|
|
235
|
+
try {
|
|
236
|
+
actionSounds = JSON.parse(saved);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
actionSounds = {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
settingsManager.onChange('soundEnabled', (val) => { enabled = val === 'true'; });
|
|
243
|
+
settingsManager.onChange('soundVolume', (val) => { volume = parseFloat(val) || 0.5; });
|
|
244
|
+
|
|
245
|
+
// Resume audio context on first user interaction
|
|
246
|
+
document.addEventListener('click', () => {
|
|
247
|
+
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
|
|
248
|
+
}, { once: true });
|
|
249
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
let wsConnected = false;
|
|
2
|
+
|
|
3
|
+
// Listen for WebSocket connection status events from wsClient
|
|
4
|
+
document.addEventListener('ws-status', (e) => {
|
|
5
|
+
wsConnected = e.detail === 'connected';
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
let historicalLoaded = false;
|
|
9
|
+
let lastHookStats = null;
|
|
10
|
+
let hookStatsVisible = false;
|
|
11
|
+
|
|
12
|
+
export async function loadHistoricalStats() {
|
|
13
|
+
// no-op: historical stats display removed from header
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function update(sessions) {
|
|
17
|
+
const el = document.getElementById('global-stats');
|
|
18
|
+
if (!el) return;
|
|
19
|
+
el.innerHTML = '';
|
|
20
|
+
// Re-add hook stats elements
|
|
21
|
+
if (lastHookStats) {
|
|
22
|
+
renderHookStatsBadge(el);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function updateHookStats(stats) {
|
|
27
|
+
lastHookStats = stats;
|
|
28
|
+
const el = document.getElementById('global-stats');
|
|
29
|
+
if (!el) return;
|
|
30
|
+
renderHookStatsBadge(el);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderHookStatsBadge(container) {
|
|
34
|
+
if (!lastHookStats) return;
|
|
35
|
+
|
|
36
|
+
// Remove existing hook stats elements
|
|
37
|
+
container.querySelector('.hook-stats-toggle')?.remove();
|
|
38
|
+
container.querySelector('.hook-stats-panel')?.remove();
|
|
39
|
+
|
|
40
|
+
const { totalHooks, hooksPerMin, events } = lastHookStats;
|
|
41
|
+
|
|
42
|
+
// Calculate overall avg processing time across all events
|
|
43
|
+
let totalProcessing = 0;
|
|
44
|
+
let processingCount = 0;
|
|
45
|
+
for (const ev of Object.values(events)) {
|
|
46
|
+
if (ev.processing.avg > 0) {
|
|
47
|
+
totalProcessing += ev.processing.avg * ev.count;
|
|
48
|
+
processingCount += ev.count;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const avgProcessing = processingCount > 0 ? Math.round(totalProcessing / processingCount) : 0;
|
|
52
|
+
|
|
53
|
+
// Badge in header
|
|
54
|
+
const badge = document.createElement('span');
|
|
55
|
+
badge.className = 'stat hook-stats-toggle';
|
|
56
|
+
badge.title = 'Click to toggle hook performance details';
|
|
57
|
+
badge.style.cursor = 'pointer';
|
|
58
|
+
badge.innerHTML = `
|
|
59
|
+
<span class="stat-label">Hooks</span>
|
|
60
|
+
<span class="stat-value">${totalHooks} <span class="hook-rate">(${hooksPerMin}/min)</span></span>
|
|
61
|
+
<span class="stat-label" style="margin-left:8px">Avg</span>
|
|
62
|
+
<span class="stat-value">${avgProcessing}ms</span>
|
|
63
|
+
`;
|
|
64
|
+
badge.addEventListener('click', () => {
|
|
65
|
+
hookStatsVisible = !hookStatsVisible;
|
|
66
|
+
const panel = container.querySelector('.hook-stats-panel');
|
|
67
|
+
if (panel) panel.classList.toggle('hidden', !hookStatsVisible);
|
|
68
|
+
});
|
|
69
|
+
container.appendChild(badge);
|
|
70
|
+
|
|
71
|
+
// Detailed dropdown panel
|
|
72
|
+
const panel = document.createElement('div');
|
|
73
|
+
panel.className = `hook-stats-panel ${hookStatsVisible ? '' : 'hidden'}`;
|
|
74
|
+
|
|
75
|
+
// Build table rows sorted by count descending
|
|
76
|
+
const sortedEvents = Object.entries(events).sort((a, b) => b[1].count - a[1].count);
|
|
77
|
+
const rows = sortedEvents.map(([name, ev]) => {
|
|
78
|
+
const latStr = ev.latency.avg > 0
|
|
79
|
+
? `${ev.latency.avg}ms <span class="hook-stat-dim">(p95: ${ev.latency.p95}ms)</span>`
|
|
80
|
+
: '<span class="hook-stat-dim">n/a</span>';
|
|
81
|
+
return `<tr>
|
|
82
|
+
<td class="hook-ev-name">${name}</td>
|
|
83
|
+
<td class="hook-ev-count">${ev.count}</td>
|
|
84
|
+
<td class="hook-ev-rate">${ev.rate}/min</td>
|
|
85
|
+
<td class="hook-ev-latency">${latStr}</td>
|
|
86
|
+
<td class="hook-ev-proc">${ev.processing.avg}ms <span class="hook-stat-dim">(p95: ${ev.processing.p95}ms)</span></td>
|
|
87
|
+
</tr>`;
|
|
88
|
+
}).join('');
|
|
89
|
+
|
|
90
|
+
panel.innerHTML = `
|
|
91
|
+
<div class="hook-stats-header">
|
|
92
|
+
<span>Hook Performance</span>
|
|
93
|
+
<button class="hook-stats-reset" title="Reset stats">Reset</button>
|
|
94
|
+
</div>
|
|
95
|
+
<table class="hook-stats-table">
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<th>Event</th>
|
|
99
|
+
<th>Count</th>
|
|
100
|
+
<th>Rate</th>
|
|
101
|
+
<th>Delivery Latency</th>
|
|
102
|
+
<th>Server Processing</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>${rows}</tbody>
|
|
106
|
+
</table>
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
panel.querySelector('.hook-stats-reset')?.addEventListener('click', async (e) => {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
await fetch('/api/hook-stats/reset', { method: 'POST' });
|
|
112
|
+
lastHookStats = null;
|
|
113
|
+
container.querySelector('.hook-stats-toggle')?.remove();
|
|
114
|
+
panel.remove();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
container.appendChild(panel);
|
|
118
|
+
}
|