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