Skip to content

Commit cf3eb38

Browse files
ozgesolidkeyclaude
andcommitted
Replace annotation balloons with sidebar bar + activity panel
The old approach rendered complex balloon DOM per annotated line on every scroll repaint, overlapping log content and adding render cost regardless of whether the user cared about annotations at that moment. New design — zero render-loop cost: - Annotation bar: a fixed 190px panel to the right of the minimap. Cards are positioned proportionally to their line numbers (same math as minimap markers). A ◄ arrow on each card points to the target position. Cards show agent name + first line of text, click navigates. Minimum spacing prevents overlap. Hidden when no annotations. - Annotations activity panel: new entry in the activity bar (speech bubble icon, count badge). Lists all annotations sorted by line number with severity-coloured left border and a clear-all button. Click any item to navigate. - Render loop: replaced the per-line balloon structure with a single 7px gutter dot per annotated line — one div, no nesting, no pointer events. All the detail lives in the bar and panel, not in the scroll viewport. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e704d6c commit cf3eb38

3 files changed

Lines changed: 249 additions & 89 deletions

File tree

src/renderer/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="4.5" cy="5" r="1.5"/><path d="M6 6q2 3 2 8"/><path d="M8 14q0-5 5-5t6 5"/><line x1="8" y1="14" x2="19" y2="14"/><path d="M19 14q2-3 3-6"/><line x1="10" y1="14" x2="10" y2="20"/><line x1="17" y1="14" x2="17" y2="20"/></svg>
7979
<span class="activity-badge" id="badge-history"></span>
8080
</button>
81+
<button class="activity-bar-btn" data-panel="annotations" title="AI Annotations" data-help="AI agent comments and findings, linked to specific log lines.&#10;Click an annotation to jump to the line.">
82+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="10" x2="15" y2="10"/><line x1="9" y1="14" x2="13" y2="14"/></svg>
83+
<span class="activity-badge" id="badge-annotations"></span>
84+
</button>
8185
<div class="activity-bar-separator"></div>
8286
<button class="activity-bar-btn" data-bottom-tab="analysis" title="Analysis (Ctrl+6)">
8387
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/></svg>
@@ -195,6 +199,15 @@
195199
<p class="placeholder">No activity recorded</p>
196200
</div>
197201
</div>
202+
<div class="panel-view" id="panel-annotations" data-panel="annotations">
203+
<div class="section-header-inline">
204+
<span class="section-title">AI Annotations</span>
205+
<button id="btn-clear-annotations" class="section-btn" title="Clear all annotations">&#128465;</button>
206+
</div>
207+
<div id="annotations-list" class="annotations-list-content">
208+
<p class="placeholder">No annotations yet</p>
209+
</div>
210+
</div>
198211
</div>
199212
</div>
200213

src/renderer/renderer.ts

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,7 @@ let logViewerWrapper: HTMLDivElement | null = null;
10201020
let minimapElement: HTMLDivElement | null = null;
10211021
let minimapContentElement: HTMLDivElement | null = null;
10221022
let minimapViewportElement: HTMLDivElement | null = null;
1023+
let annotationBarElement: HTMLDivElement | null = null;
10231024
let minimapData: Array<{ level: string | undefined }> = [];
10241025
const 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+
60906178
function 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'];
1191912008
const 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

1192712017
const 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

Comments
 (0)