@@ -1020,6 +1020,7 @@ let logViewerWrapper: HTMLDivElement | null = null;
10201020let minimapElement: HTMLDivElement | null = null;
10211021let minimapContentElement: HTMLDivElement | null = null;
10221022let minimapViewportElement: HTMLDivElement | null = null;
1023+ let annotationBarElement: HTMLDivElement | null = null;
10231024let minimapData: Array<{ level: string | undefined }> = [];
10241025const MINIMAP_SAMPLE_RATE = 1000; // Sample every N lines for minimap
10251026
@@ -1970,9 +1971,18 @@ function createLogViewer(): void {
19701971 minimapTooltip.id = 'minimap-tooltip';
19711972 minimapElement.appendChild(minimapTooltip);
19721973
1974+ // Annotation bar — cards positioned proportionally, shown only when annotations exist
1975+ if (annotationBarElement) annotationBarElement.remove();
1976+ annotationBarElement = document.createElement('div');
1977+ annotationBarElement.className = 'annotation-bar';
1978+ if (!state.showAnnotations || state.annotations.length === 0) {
1979+ annotationBarElement.classList.add('hidden');
1980+ }
1981+
19731982 // Add to wrapper
19741983 logViewerWrapper.appendChild(logViewerElement);
19751984 logViewerWrapper.appendChild(minimapElement);
1985+ logViewerWrapper.appendChild(annotationBarElement);
19761986 elements.editorContainer.appendChild(logViewerWrapper);
19771987
19781988 // Event listeners with passive flag for better scroll performance
@@ -2478,36 +2488,15 @@ function renderVisibleLines(): void {
24782488
24792489 fragment.appendChild(lineElement);
24802490
2481- // Agent annotation balloon (only when visible and annotations exist for this line)
2491+ // Lightweight annotation gutter dot — annotation detail lives in the bar and panel
24822492 if (state.showAnnotations && state.annotationsByLine.size > 0) {
24832493 const lineAnns = state.annotationsByLine.get(line.lineNumber);
24842494 if (lineAnns) {
2485- // Show a small indicator dot on the line + balloon on hover/always
2486- const indicator = document.createElement('div');
2487- indicator.className = 'annotation-indicator';
2488- indicator.style.cssText = `position:absolute;right:44px;transform:translateY(${top}px);z-index:3;`;
2489- indicator.dataset.lineNumber = String(line.lineNumber);
2490-
2491- // Build balloon content (stacked if multiple)
2492- let balloonHtml = '';
2493- for (const ann of lineAnns) {
2494- const sevClass = `severity-${ann.severity || 'info'}`;
2495- balloonHtml += `<div class="annotation-balloon ${sevClass}">` +
2496- `<span class="annotation-agent">${escapeHtml(ann.agentName)}</span>` +
2497- `<span class="annotation-text">${escapeHtml(ann.text)}</span>` +
2498- `</div>`;
2499- }
2500-
2501- const balloon = document.createElement('div');
2502- balloon.className = 'annotation-balloon-container';
2503- balloon.style.cssText = `position:absolute;right:48px;transform:translateY(${top - 4}px);z-index:10;`;
2504- balloon.innerHTML = balloonHtml;
2505-
2506- indicator.innerHTML = `<span class="annotation-dot severity-${lineAnns[0].severity || 'info'}">${lineAnns.length > 1 ? lineAnns.length : ''}</span>`;
2507- indicator.title = lineAnns.map(a => `${a.agentName}: ${a.text}`).join('\n');
2508-
2509- fragment.appendChild(indicator);
2510- fragment.appendChild(balloon);
2495+ const dot = document.createElement('div');
2496+ dot.className = `annotation-gutter-dot severity-${lineAnns[0].severity || 'info'}`;
2497+ dot.style.cssText = `position:absolute;right:6px;transform:translateY(${top + (getLineHeight() - 8) / 2}px);z-index:3;`;
2498+ dot.title = lineAnns.map(a => `${a.agentName}: ${a.text}`).join('\n');
2499+ fragment.appendChild(dot);
25112500 }
25122501 }
25132502
@@ -6087,9 +6076,109 @@ function rebuildAnnotationIndex(): void {
60876076 }
60886077}
60896078
6079+ function renderAnnotationBar(): void {
6080+ if (!annotationBarElement) return;
6081+
6082+ const totalLines = getTotalLines();
6083+ const hasAnns = state.showAnnotations && state.annotations.length > 0 && totalLines > 0;
6084+
6085+ if (!hasAnns) {
6086+ annotationBarElement.classList.add('hidden');
6087+ annotationBarElement.innerHTML = '';
6088+ return;
6089+ }
6090+
6091+ annotationBarElement.classList.remove('hidden');
6092+ const barHeight = annotationBarElement.clientHeight || minimapElement?.clientHeight || 400;
6093+
6094+ // Sort by line number, then position with minimum spacing to avoid overlap
6095+ const sorted = [...state.annotations].sort((a, b) => a.lineNumber - b.lineNumber);
6096+ const MIN_GAP = 30;
6097+ const positions: number[] = [];
6098+ let prevBottom = -MIN_GAP;
6099+ for (const ann of sorted) {
6100+ const ideal = (ann.lineNumber / totalLines) * barHeight;
6101+ const top = Math.max(ideal, prevBottom + MIN_GAP);
6102+ positions.push(top);
6103+ prevBottom = top;
6104+ }
6105+
6106+ const frag = document.createDocumentFragment();
6107+ sorted.forEach((ann, i) => {
6108+ const card = document.createElement('div');
6109+ const sev = ann.severity || 'info';
6110+ card.className = `ann-bar-card severity-${sev}`;
6111+ card.style.top = `${positions[i]}px`;
6112+ card.title = `Line ${ann.lineNumber + 1} — ${ann.agentName}: ${ann.text}`;
6113+ card.dataset.lineNumber = String(ann.lineNumber);
6114+
6115+ const firstLine = ann.text.split('\n')[0];
6116+ const truncated = firstLine.length > 60 ? firstLine.slice(0, 58) + '…' : firstLine;
6117+
6118+ card.innerHTML =
6119+ `<div class="ann-bar-agent">${escapeHtml(ann.agentName)}</div>` +
6120+ `<div class="ann-bar-text">${escapeHtml(truncated)}</div>`;
6121+
6122+ card.addEventListener('click', () => {
6123+ const di = getFilteredDisplayIndex(ann.lineNumber);
6124+ goToLine(di >= 0 ? di : ann.lineNumber, ann.lineNumber);
6125+ renderVisibleLines();
6126+ });
6127+
6128+ frag.appendChild(card);
6129+ });
6130+
6131+ annotationBarElement.innerHTML = '';
6132+ annotationBarElement.appendChild(frag);
6133+ }
6134+
6135+ function renderAnnotationsPanel(): void {
6136+ const list = document.getElementById('annotations-list');
6137+ if (!list) return;
6138+
6139+ const badge = document.getElementById('badge-annotations');
6140+
6141+ if (state.annotations.length === 0) {
6142+ list.innerHTML = '<p class="placeholder">No annotations yet</p>';
6143+ if (badge) badge.textContent = '';
6144+ return;
6145+ }
6146+
6147+ if (badge) badge.textContent = String(state.annotations.length);
6148+
6149+ const sorted = [...state.annotations].sort((a, b) => a.lineNumber - b.lineNumber);
6150+ const frag = document.createDocumentFragment();
6151+
6152+ for (const ann of sorted) {
6153+ const sev = ann.severity || 'info';
6154+ const item = document.createElement('div');
6155+ item.className = `ann-panel-item severity-${sev}`;
6156+ item.dataset.lineNumber = String(ann.lineNumber);
6157+
6158+ item.innerHTML =
6159+ `<span class="ann-panel-line">L${ann.lineNumber + 1}</span>` +
6160+ `<div class="ann-panel-body">` +
6161+ `<span class="ann-panel-agent">${escapeHtml(ann.agentName)}</span>` +
6162+ `<span class="ann-panel-text">${escapeHtml(ann.text)}</span>` +
6163+ `</div>`;
6164+
6165+ item.addEventListener('click', () => {
6166+ const di = getFilteredDisplayIndex(ann.lineNumber);
6167+ goToLine(di >= 0 ? di : ann.lineNumber, ann.lineNumber);
6168+ renderVisibleLines();
6169+ });
6170+
6171+ frag.appendChild(item);
6172+ }
6173+
6174+ list.innerHTML = '';
6175+ list.appendChild(frag);
6176+ }
6177+
60906178function toggleAnnotations(): void {
60916179 state.showAnnotations = !state.showAnnotations;
60926180 elements.btnAnnotationsToggle.classList.toggle('active', state.showAnnotations);
6181+ renderAnnotationBar();
60936182 renderVisibleLines();
60946183 renderMinimapMarkers();
60956184}
@@ -11915,13 +12004,14 @@ function formatBytes(bytes: number): string {
1191512004
1191612005// Sidebar toggle
1191712006// Panel system
11918- const PANEL_IDS = ['folders', 'bookmarks', 'highlights', 'stats', 'history'];
12007+ const PANEL_IDS = ['folders', 'bookmarks', 'highlights', 'stats', 'history', 'annotations' ];
1191912008const PANEL_NAMES: Record<string, string> = {
1192012009 'folders': 'Folders',
1192112010 'bookmarks': 'Bookmarks',
1192212011 'highlights': 'Highlights',
1192312012 'stats': 'Stats',
1192412013 'history': 'History',
12014+ 'annotations': 'AI Annotations',
1192512015};
1192612016
1192712017const BOTTOM_TAB_IDS = ['analysis', 'time-gaps', 'search-results', 'search-configs', 'video', 'live', 'notes'];
@@ -11970,6 +12060,11 @@ function openPanel(panelId: string): void {
1197012060 loadAndRenderHistory();
1197112061 }
1197212062
12063+ // Render annotations panel on open
12064+ if (panelId === 'annotations') {
12065+ renderAnnotationsPanel();
12066+ }
12067+
1197312068 savePanelState();
1197412069}
1197512070
@@ -13075,9 +13170,16 @@ function init(): void {
1307513170 window.api.onAnnotationsChanged((anns: any[]) => {
1307613171 state.annotations = anns;
1307713172 rebuildAnnotationIndex();
13173+ renderAnnotationBar();
13174+ renderAnnotationsPanel();
1307813175 renderVisibleLines();
1307913176 renderMinimapMarkers();
1308013177 });
13178+
13179+ // Clear all annotations button
13180+ document.getElementById('btn-clear-annotations')?.addEventListener('click', async () => {
13181+ await window.api.clearAnnotations();
13182+ });
1308113183 // Launch/Stop agent button
1308213184 elements.chatLaunchAgent.addEventListener('click', toggleAgent);
1308313185 // Agent setup wizard
0 commit comments