personal memory agent
1<style>
2.dev-container {
3 max-width: 1200px;
4 margin: 0 auto;
5 padding: 2rem;
6}
7
8.dev-section {
9 background: white;
10 border: 1px solid #e5e7eb;
11 border-radius: 8px;
12 padding: 1.5rem;
13 margin-bottom: 1.5rem;
14}
15
16.dev-section h2 {
17 margin: 0 0 1rem 0;
18 font-size: 1.25rem;
19 font-weight: 600;
20 color: #1f2937;
21}
22
23.dev-section p {
24 margin: 0 0 1rem 0;
25 color: #6b7280;
26 font-size: 0.9rem;
27}
28
29.dev-controls {
30 display: flex;
31 flex-wrap: wrap;
32 gap: 0.75rem;
33}
34
35.dev-button {
36 padding: 0.5rem 1rem;
37 background: #667eea;
38 color: white;
39 border: none;
40 border-radius: 6px;
41 cursor: pointer;
42 font-size: 0.9rem;
43 font-weight: 500;
44 transition: background 0.2s, transform 0.1s;
45}
46
47.dev-button:hover {
48 background: #5568d3;
49 transform: translateY(-1px);
50}
51
52.dev-button:active {
53 transform: translateY(0);
54}
55
56.dev-button.secondary {
57 background: #6b7280;
58}
59
60.dev-button.secondary:hover {
61 background: #4b5563;
62}
63
64.dev-button.danger {
65 background: #ef4444;
66}
67
68.dev-button.danger:hover {
69 background: #dc2626;
70}
71
72.dev-form {
73 display: flex;
74 flex-direction: column;
75 gap: 1rem;
76 max-width: 500px;
77}
78
79.dev-form-row {
80 display: flex;
81 flex-direction: column;
82 gap: 0.25rem;
83}
84
85.dev-form-row label {
86 font-size: 0.85rem;
87 font-weight: 500;
88 color: #374151;
89}
90
91.dev-form-row input,
92.dev-form-row textarea,
93.dev-form-row select {
94 padding: 0.5rem;
95 border: 1px solid #d1d5db;
96 border-radius: 4px;
97 font-size: 0.9rem;
98}
99
100.dev-form-row textarea {
101 resize: vertical;
102 min-height: 60px;
103}
104
105.dev-output {
106 background: #f9fafb;
107 border: 1px solid #e5e7eb;
108 border-radius: 4px;
109 padding: 1rem;
110 font-family: monospace;
111 font-size: 0.85rem;
112 color: #374151;
113 max-height: 200px;
114 overflow-y: auto;
115}
116
117.dev-stats {
118 display: grid;
119 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
120 gap: 1rem;
121 margin-top: 1rem;
122}
123
124.dev-stat {
125 background: #f9fafb;
126 border: 1px solid #e5e7eb;
127 border-radius: 4px;
128 padding: 1rem;
129 text-align: center;
130}
131
132.dev-stat-value {
133 font-size: 2rem;
134 font-weight: 600;
135 color: #667eea;
136}
137
138.dev-stat-label {
139 font-size: 0.75rem;
140 color: #6b7280;
141 text-transform: uppercase;
142 letter-spacing: 0.05em;
143 margin-top: 0.25rem;
144}
145
146/* Events Card */
147.events-card {
148 background: white;
149 border-radius: 12px;
150 padding: 1.5em;
151 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
152}
153
154.events-card.paused {
155 border-left: 4px solid #f59e0b;
156}
157
158.events-header {
159 display: flex;
160 justify-content: space-between;
161 align-items: center;
162 margin-bottom: 1em;
163}
164
165.events-title {
166 font-size: 1.1em;
167 font-weight: 600;
168}
169
170.events-controls {
171 display: flex;
172 align-items: center;
173 gap: 1em;
174}
175
176.pause-btn {
177 background: #3b82f6;
178 color: white;
179 border: none;
180 padding: 0.5em 1em;
181 border-radius: 6px;
182 cursor: pointer;
183 font-size: 0.85em;
184 font-weight: 500;
185 display: flex;
186 align-items: center;
187 gap: 0.4em;
188 transition: background 0.2s;
189}
190
191.pause-btn:hover {
192 background: #2563eb;
193}
194
195.pause-btn.paused {
196 background: #f59e0b;
197}
198
199.pause-btn.paused:hover {
200 background: #d97706;
201}
202
203.missed-count {
204 font-size: 0.85em;
205 color: #f59e0b;
206 font-weight: 500;
207}
208
209.event-log {
210 height: 400px;
211 overflow-y: auto;
212 font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
213 font-size: 12px;
214 border: 1px solid #e5e7eb;
215 border-radius: 6px;
216 padding: 0.5em;
217 background: #f9fafb;
218}
219
220.event-line {
221 white-space: nowrap;
222 overflow: hidden;
223 text-overflow: ellipsis;
224 padding: 3px 0;
225 border-bottom: 1px solid #f3f4f6;
226 color: #374151;
227}
228
229.event-line:last-child {
230 border-bottom: none;
231}
232
233.event-line.error {
234 color: #dc2626;
235 background: #fef2f2;
236}
237
238.event-log:empty::before {
239 content: 'Waiting for events...';
240 color: #9ca3af;
241 font-style: italic;
242}
243</style>
244
245<div class="dev-container">
246 <h1>🛠️ Dev Tools</h1>
247
248 <!-- Quick Tests -->
249 <div class="dev-section">
250 <h2>Quick Tests</h2>
251 <p>Click buttons to trigger different notification types</p>
252 <div class="dev-controls">
253 <button class="dev-button" onclick="testBasic()">Basic Notification</button>
254 <button class="dev-button" onclick="testWithBadge()">With Badge (5)</button>
255 <button class="dev-button" onclick="testAutoDismiss()">Auto-Dismiss (5s)</button>
256 <button class="dev-button" onclick="testNonDismissible()">Non-Dismissible</button>
257 <button class="dev-button" onclick="testLongMessage()">Long Message</button>
258 <button class="dev-button" onclick="testNoAction()">No Click Action</button>
259 <button class="dev-button" onclick="testWithFacet()">With Facet (work)</button>
260 </div>
261 </div>
262
263 <!-- Custom Notification -->
264 <div class="dev-section">
265 <h2>Custom Notification</h2>
266 <p>Build your own notification with custom parameters</p>
267 <div class="dev-form">
268 <div class="dev-form-row">
269 <label>App Name</label>
270 <input type="text" id="custom-app" value="dev" />
271 </div>
272 <div class="dev-form-row">
273 <label>Icon (emoji)</label>
274 <input type="text" id="custom-icon" value="🛠️" maxlength="2" />
275 </div>
276 <div class="dev-form-row">
277 <label>Title</label>
278 <input type="text" id="custom-title" value="Custom Notification" />
279 </div>
280 <div class="dev-form-row">
281 <label>Message</label>
282 <textarea id="custom-message">This is a custom notification message</textarea>
283 </div>
284 <div class="dev-form-row">
285 <label>Action URL</label>
286 <input type="text" id="custom-action" value="/app/dev" />
287 </div>
288 <div class="dev-form-row">
289 <label>Facet (optional)</label>
290 <select id="custom-facet">
291 <option value="">None</option>
292 </select>
293 </div>
294 <div class="dev-form-row">
295 <label>Badge Count (0 = none)</label>
296 <input type="number" id="custom-badge" value="0" min="0" />
297 </div>
298 <div class="dev-form-row">
299 <label>Auto-Dismiss (ms, 0 = never)</label>
300 <input type="number" id="custom-autodismiss" value="0" min="0" step="1000" />
301 </div>
302 <div class="dev-form-row">
303 <label>
304 <input type="checkbox" id="custom-dismissible" checked /> Dismissible (show X button)
305 </label>
306 </div>
307 <button class="dev-button" onclick="testCustom()">Show Custom Notification</button>
308 </div>
309 </div>
310
311 <!-- Notification Management -->
312 <div class="dev-section">
313 <h2>Notification Management</h2>
314 <p>Test dismissing and managing notifications</p>
315 <div class="dev-controls">
316 <button class="dev-button secondary" onclick="updateLast()">Update Last Notification</button>
317 <button class="dev-button secondary" onclick="dismissLast()">Dismiss Last</button>
318 <button class="dev-button danger" onclick="dismissAll()">Dismiss All</button>
319 <button class="dev-button secondary" onclick="showStats()">Show Stats</button>
320 </div>
321 <div class="dev-stats" id="stats"></div>
322 </div>
323
324 <!-- Stress Test -->
325 <div class="dev-section">
326 <h2>Stress Test</h2>
327 <p>Generate multiple notifications to test stacking and performance</p>
328 <div class="dev-controls">
329 <button class="dev-button" onclick="spawnThree()">Spawn 3 Notifications</button>
330 <button class="dev-button" onclick="spawnTen()">Spawn 10 Notifications</button>
331 <button class="dev-button" onclick="spawnSequential()">Spawn Sequential (5s intervals)</button>
332 </div>
333 </div>
334
335 <!-- Console Log -->
336 <div class="dev-section">
337 <h2>Console Log</h2>
338 <p>Notification IDs and events</p>
339 <div class="dev-output" id="console"></div>
340 </div>
341
342 <!-- Callosum Event Viewer -->
343 <div class="dev-section">
344 <h2>Callosum Event Viewer</h2>
345 <p>Raw callosum event stream with pause/resume</p>
346 <div class="events-card" id="eventsCard">
347 <div class="events-header">
348 <div class="events-title">EVENTS</div>
349 <div class="events-controls">
350 <span class="missed-count" id="missedCount" style="display: none;"></span>
351 <button class="pause-btn" id="pauseBtn">
352 <span>||</span>
353 <span>Pause</span>
354 </button>
355 </div>
356 </div>
357 <div id="eventLog" class="event-log"></div>
358 </div>
359 </div>
360</div>
361
362<script>
363let lastNotificationId = null;
364let sequentialTimer = null;
365
366// Helper to log to console
367function log(message) {
368 const consoleEl = document.getElementById('console');
369 const timestamp = new Date().toLocaleTimeString();
370 consoleEl.innerHTML = `[${timestamp}] ${message}<br>` + consoleEl.innerHTML;
371}
372
373// Quick Tests
374function testBasic() {
375 const id = window.AppServices.notifications.show({
376 app: 'dev',
377 icon: '🛠️',
378 title: 'Basic Notification',
379 message: 'This is a basic notification with all default settings',
380 action: '/app/dev'
381 });
382 lastNotificationId = id;
383 log(`Created basic notification (ID: ${id})`);
384}
385
386function testWithBadge() {
387 const id = window.AppServices.notifications.show({
388 app: 'dev',
389 icon: '🔔',
390 title: 'Notification with Badge',
391 message: 'This notification has a badge count',
392 badge: 5,
393 action: '/app/dev'
394 });
395 lastNotificationId = id;
396 log(`Created notification with badge (ID: ${id})`);
397}
398
399function testAutoDismiss() {
400 const id = window.AppServices.notifications.show({
401 app: 'dev',
402 icon: '⏱️',
403 title: 'Auto-Dismiss Test',
404 message: 'This notification will disappear after 5 seconds',
405 action: '/app/dev',
406 autoDismiss: 5000
407 });
408 lastNotificationId = id;
409 log(`Created auto-dismiss notification (ID: ${id})`);
410}
411
412function testNonDismissible() {
413 const id = window.AppServices.notifications.show({
414 app: 'dev',
415 icon: '🔒',
416 title: 'Non-Dismissible',
417 message: 'This notification has no X button',
418 dismissible: false,
419 action: '/app/dev'
420 });
421 lastNotificationId = id;
422 log(`Created non-dismissible notification (ID: ${id})`);
423}
424
425function testLongMessage() {
426 const id = window.AppServices.notifications.show({
427 app: 'dev',
428 icon: '📝',
429 title: 'Long Message Test',
430 message: 'This is a very long notification message that should be truncated with ellipsis after two lines to prevent the card from becoming too tall and taking up too much space in the notification center.',
431 action: '/app/dev'
432 });
433 lastNotificationId = id;
434 log(`Created long message notification (ID: ${id})`);
435}
436
437function testNoAction() {
438 const id = window.AppServices.notifications.show({
439 app: 'dev',
440 icon: '🚫',
441 title: 'No Click Action',
442 message: 'Clicking this card does nothing (no action URL)',
443 action: null
444 });
445 lastNotificationId = id;
446 log(`Created no-action notification (ID: ${id})`);
447}
448
449function testWithFacet() {
450 const id = window.AppServices.notifications.show({
451 app: 'dev',
452 icon: '🎯',
453 title: 'Facet Navigation Test',
454 message: 'Clicking this will navigate to /app/home and select "work" facet',
455 action: '/app/home',
456 facet: 'work'
457 });
458 lastNotificationId = id;
459 log(`Created facet notification (ID: ${id})`);
460}
461
462// Custom Notification
463function testCustom() {
464 const app = document.getElementById('custom-app').value;
465 const icon = document.getElementById('custom-icon').value;
466 const title = document.getElementById('custom-title').value;
467 const message = document.getElementById('custom-message').value;
468 const action = document.getElementById('custom-action').value || null;
469 const facet = document.getElementById('custom-facet').value || null;
470 const badge = parseInt(document.getElementById('custom-badge').value) || null;
471 const autoDismiss = parseInt(document.getElementById('custom-autodismiss').value) || null;
472 const dismissible = document.getElementById('custom-dismissible').checked;
473
474 const id = window.AppServices.notifications.show({
475 app,
476 icon,
477 title,
478 message,
479 action,
480 facet,
481 badge: badge > 0 ? badge : null,
482 autoDismiss: autoDismiss > 0 ? autoDismiss : null,
483 dismissible
484 });
485 lastNotificationId = id;
486 log(`Created custom notification (ID: ${id})`);
487}
488
489// Management
490function updateLast() {
491 if (!lastNotificationId) {
492 log('No notification to update');
493 return;
494 }
495
496 window.AppServices.notifications.update(lastNotificationId, {
497 title: 'Updated Notification',
498 message: 'This notification was updated!',
499 badge: 99
500 });
501 log(`Updated notification (ID: ${lastNotificationId})`);
502}
503
504function dismissLast() {
505 if (!lastNotificationId) {
506 log('No notification to dismiss');
507 return;
508 }
509
510 window.AppServices.notifications.dismiss(lastNotificationId);
511 log(`Dismissed notification (ID: ${lastNotificationId})`);
512 lastNotificationId = null;
513}
514
515function dismissAll() {
516 window.AppServices.notifications.dismissAll();
517 log('Dismissed all notifications');
518 lastNotificationId = null;
519}
520
521function showStats() {
522 const count = window.AppServices.notifications.count();
523 const statsEl = document.getElementById('stats');
524 statsEl.innerHTML = `
525 <div class="dev-stat">
526 <div class="dev-stat-value">${count}</div>
527 <div class="dev-stat-label">Active Notifications</div>
528 </div>
529 <div class="dev-stat">
530 <div class="dev-stat-value">${lastNotificationId || 'N/A'}</div>
531 <div class="dev-stat-label">Last ID</div>
532 </div>
533 `;
534 log(`Stats: ${count} active notifications`);
535}
536
537// Stress Tests
538function spawnThree() {
539 for (let i = 1; i <= 3; i++) {
540 setTimeout(() => {
541 const id = window.AppServices.notifications.show({
542 app: 'dev',
543 icon: '🚀',
544 title: `Notification ${i}/3`,
545 message: `Testing notification stacking (${i} of 3)`,
546 action: '/app/dev',
547 badge: i
548 });
549 log(`Spawned notification ${i}/3 (ID: ${id})`);
550 }, (i - 1) * 300);
551 }
552}
553
554function spawnTen() {
555 for (let i = 1; i <= 10; i++) {
556 setTimeout(() => {
557 const id = window.AppServices.notifications.show({
558 app: 'dev',
559 icon: '💥',
560 title: `Batch ${i}/10`,
561 message: `Testing max visible limit (only last 5 should show)`,
562 action: '/app/dev',
563 badge: i
564 });
565 log(`Spawned notification ${i}/10 (ID: ${id})`);
566 }, (i - 1) * 200);
567 }
568}
569
570function spawnSequential() {
571 if (sequentialTimer) {
572 clearInterval(sequentialTimer);
573 sequentialTimer = null;
574 log('Stopped sequential spawn');
575 return;
576 }
577
578 let count = 1;
579 sequentialTimer = setInterval(() => {
580 const id = window.AppServices.notifications.show({
581 app: 'dev',
582 icon: '⏰',
583 title: `Sequential #${count}`,
584 message: 'New notification every 5 seconds',
585 action: '/app/dev',
586 autoDismiss: 8000
587 });
588 log(`Sequential spawn #${count} (ID: ${id})`);
589 count++;
590
591 if (count > 10) {
592 clearInterval(sequentialTimer);
593 sequentialTimer = null;
594 log('Sequential spawn complete');
595 }
596 }, 5000);
597
598 log('Started sequential spawn (10 notifications, 5s intervals)');
599}
600
601// Populate facet dropdown from window.facetsData
602function populateFacetDropdown() {
603 const facetSelect = document.getElementById('custom-facet');
604 if (!facetSelect || !window.facetsData) return;
605
606 // Clear existing options except "None"
607 facetSelect.innerHTML = '<option value="">None</option>';
608
609 // Add facet options
610 window.facetsData.forEach(facet => {
611 const option = document.createElement('option');
612 option.value = facet.name;
613 option.textContent = `${facet.emoji} ${facet.title}`;
614 facetSelect.appendChild(option);
615 });
616}
617
618// Callosum Event Viewer
619(function() {
620 const eventLog = document.getElementById('eventLog');
621 const eventsCard = document.getElementById('eventsCard');
622 const pauseBtn = document.getElementById('pauseBtn');
623 const missedCountEl = document.getElementById('missedCount');
624 let paused = false;
625 let missed = 0;
626
627 function togglePause() {
628 paused = !paused;
629 if (paused) {
630 pauseBtn.classList.add('paused');
631 pauseBtn.innerHTML = '<span>|></span><span>Resume</span>';
632 eventsCard.classList.add('paused');
633 missed = 0;
634 missedCountEl.style.display = 'none';
635 } else {
636 pauseBtn.classList.remove('paused');
637 pauseBtn.innerHTML = '<span>||</span><span>Pause</span>';
638 eventsCard.classList.remove('paused');
639 missed = 0;
640 missedCountEl.style.display = 'none';
641 }
642 }
643
644 function appendEvent(msg) {
645 if (paused) {
646 missed++;
647 missedCountEl.textContent = missed + ' missed';
648 missedCountEl.style.display = 'inline';
649 return;
650 }
651
652 const line = document.createElement('div');
653 line.className = 'event-line';
654 if (msg.event === 'error' || (msg.event === 'exit' && msg.exit_code !== 0)) {
655 line.className = 'event-line error';
656 }
657 line.textContent = JSON.stringify(msg);
658 eventLog.appendChild(line);
659 eventLog.scrollTop = eventLog.scrollHeight;
660
661 while (eventLog.children.length > 100) {
662 eventLog.removeChild(eventLog.firstChild);
663 }
664 }
665
666 pauseBtn.addEventListener('click', togglePause);
667 if (window.appEvents) {
668 window.appEvents.listen('*', appendEvent);
669 }
670})();
671
672// Initial log and setup
673populateFacetDropdown();
674log('Dev Tools loaded - ready to test notifications');
675</script>