Skip to content
6 changes: 6 additions & 0 deletions apps/mark/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@
scope: 'branch' | 'commit';
comments: number;
annotations: number;
warnings: number;
};
let timelineReviewDetailsById = $state<Record<string, TimelineReviewDetails>>({});
let reviewDetailsLoadVersion = 0;
Expand Down Expand Up @@ -864,11 +865,15 @@

let comments = 0;
let annotations = 0;
let warnings = 0;
for (const comment of fullReview.comments) {
if (comment.commentType === 'information') {
annotations += 1;
} else {
comments += 1;
if (comment.commentType === 'warning') {
warnings += 1;
}
}
}

Expand All @@ -877,6 +882,7 @@
scope: fullReview.scope,
comments,
annotations,
warnings,
};
return { id: review.id, details };
} catch (e) {
Expand Down
38 changes: 30 additions & 8 deletions apps/mark/src/lib/features/diff/DiffCommentsSection.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Bot, Check, Copy, MessageSquare, Trash2 } from 'lucide-svelte';
import { AlertTriangle, Bot, Check, Copy, MessageSquare, Trash2 } from 'lucide-svelte';
import type { Comment } from '../../types';
import { formatLineRange, truncateText } from './diffModalHelpers';

Expand Down Expand Up @@ -68,12 +68,22 @@
style="padding-left: 8px"
onclick={() => onSelectComment(comment)}
>
<span class="comment-icon" class:agent-comment={comment.author === 'agent'}>
<span class="comment-icons">
{#if comment.author === 'agent'}
<Bot size={12} />
{:else}
<MessageSquare size={12} />
<span class="comment-icon agent-icon">
<Bot size={12} />
</span>
{/if}
<span
class="comment-icon"
class:comment-icon-warning={comment.commentType === 'warning'}
>
{#if comment.commentType === 'warning'}
<AlertTriangle size={12} />
{:else}
<MessageSquare size={12} />
{/if}
</span>
</span>
<span class="comment-details">
<span class="comment-location">
Expand Down Expand Up @@ -220,18 +230,30 @@
gap: 2px !important;
padding-top: 6px !important;
padding-bottom: 6px !important;
padding-left: 28px !important;
padding-left: 40px !important;
width: 100%;
}

.comment-icon {
.comment-icons {
position: absolute;
left: 8px;
top: 8px;
display: flex;
align-items: center;
gap: 3px;
}

.comment-icon {
display: flex;
align-items: center;
color: var(--text-faint);
}

.comment-icon.agent-comment {
.comment-icon.agent-icon {
color: var(--status-modified);
}

.comment-icon.comment-icon-warning {
color: var(--status-modified);
}

Expand Down
16 changes: 15 additions & 1 deletion apps/mark/src/lib/features/diff/DiffFileTreeSection.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import {
AlertTriangle,
Check,
RotateCcw,
ChevronRight,
Expand Down Expand Up @@ -228,7 +229,16 @@
{@render fileIcon(node.file, showReviewedSection)}
<span class="file-name">{node.name}</span>
{#if node.file.commentCount > 0}
<span class="comment-indicator"><MessageSquare size={12} /></span>
<span
class="comment-indicator"
class:comment-indicator-warning={node.file.commentTypes.includes('warning')}
>
{#if node.file.commentTypes.includes('warning')}
<AlertTriangle size={12} />
{:else}
<MessageSquare size={12} />
{/if}
</span>
{/if}
{#if searchState?.state.isOpen && searchState.state.fileResults.has(node.file.path)}
{@const resultCount =
Expand Down Expand Up @@ -525,4 +535,8 @@
margin-left: auto;
padding-left: 4px;
}

.comment-indicator.comment-indicator-warning {
color: var(--status-modified);
}
</style>
44 changes: 29 additions & 15 deletions apps/mark/src/lib/features/timeline/BranchTimeline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { FileText, GitCommitVertical, FileSearch } from 'lucide-svelte';
import type { BranchTimeline as BranchTimelineData } from '../../types';
import TimelineRow from './TimelineRow.svelte';
import type { TimelineItemType } from './TimelineRow.svelte';
import type { TimelineItemType, TimelineBadge } from './TimelineRow.svelte';
import {
collectRunningSessionIds,
createLiveSessionHints,
Expand Down Expand Up @@ -48,7 +48,14 @@
onDeleteReview?: (reviewId: string, sessionId?: string) => void;
onDeleteImage?: (imageId: string) => void;
/** Optional per-review breakdown of visible comments vs hold-to-reveal annotations. */
reviewCommentBreakdown?: Record<string, { comments: number; annotations: number }>;
reviewCommentBreakdown?: Record<
string,
{
comments: number;
annotations: number;
warnings?: number;
}
>;
onNewNote?: () => void;
onNewCommit?: () => void;
onNewReview?: (e: MouseEvent) => void;
Expand Down Expand Up @@ -113,6 +120,7 @@
reviewId?: string;
imageId?: string;
imageFilename?: string;
badges?: TimelineBadge[];
/** When set, delete button is shown but disabled with this tooltip. */
deleteDisabledReason?: string;
};
Expand All @@ -122,10 +130,6 @@
return text.replace(/<(action|branch-history)>[\s\S]*?<\/\1>/g, '').trim();
}

function formatCount(count: number, singular: string): string {
return `${count} ${singular}${count === 1 ? '' : 's'}`;
}

let runningSessionIds = $derived.by(() => collectRunningSessionIds(timeline, pendingItems));

$effect(() => {
Expand Down Expand Up @@ -233,30 +237,39 @@
const isFailed = !isRunning && !!review.sessionId && totalCount === 0;
const isDeleting = deletingReviewIds.has(review.id);
const liveHint = review.sessionId ? liveSessionHints[review.sessionId] : undefined;
const countParts: string[] = [];
if (commentCount > 0) countParts.push(formatCount(commentCount, 'comment'));
if (annotationCount > 0) countParts.push(formatCount(annotationCount, 'annotation'));

// Build badges: warnings get their own badge, everything else is a comment
const warningCount = breakdown?.warnings ?? 0;
const nonWarningCount = commentCount - warningCount;

const badges: TimelineBadge[] = [];
if (warningCount > 0) {
badges.push({ icon: 'warning', count: warningCount });
}
if (nonWarningCount > 0) {
badges.push({ icon: 'comment', count: nonWarningCount });
}

let type: TimelineItemType;
let secondaryMeta: string | undefined;
let meta: string | undefined;

if (isFailed) {
type = 'failed-review';
secondaryMeta = 'Session finished — no comments created';
meta = 'Session finished — no comments created';
} else if (isRunning) {
type = 'generating-review';
secondaryMeta = liveHint ?? 'Generating review';
meta = liveHint ?? 'Generating review';
} else {
type = 'review';
secondaryMeta = formatRelativeTimeMs(review.createdAt);
meta = formatRelativeTimeMs(review.createdAt);
}

all.push({
key: `review-${review.id}`,
type,
title: review.title || 'Code Review',
meta: countParts.length > 0 ? countParts.join(' + ') : undefined,
secondaryMeta: isDeleting ? 'Deleting...' : secondaryMeta,
meta: isDeleting ? 'Deleting...' : meta,
badges: badges.length > 0 ? badges : undefined,
deleting: isDeleting,
timestamp: Math.floor(review.createdAt / 1000),
sessionId: review.sessionId ?? undefined,
Expand Down Expand Up @@ -418,6 +431,7 @@
title={item.title}
meta={item.meta}
secondaryMeta={item.secondaryMeta}
badges={item.badges}
deleting={item.deleting}
isLast={index === items.length - 1 &&
!onNewNote &&
Expand Down
37 changes: 35 additions & 2 deletions apps/mark/src/lib/features/timeline/TimelineRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@
| 'failed-review'
| 'image';

export type TimelineBadge = {
icon: 'comment' | 'warning';
count: number;
};

interface Props {
type: TimelineItemType;
title: string;
meta?: string;
secondaryMeta?: string;
badges?: TimelineBadge[];
deleting?: boolean;
isLast?: boolean;
sessionId?: string;
Expand All @@ -48,6 +54,7 @@
title,
meta,
secondaryMeta,
badges,
deleting = false,
isLast = false,
sessionId,
Expand Down Expand Up @@ -134,14 +141,26 @@
<span class="timeline-title" class:skeleton-title={isPending} class:failed-title={isFailed}
>{title}</span
>
{#if meta || secondaryMeta}
{#if meta || secondaryMeta || (badges && badges.length > 0)}
<div class="timeline-meta">
{#if meta}
<span class="meta-item">{meta}</span>
{/if}
{#if secondaryMeta}
<span class="meta-item meta-sha" class:failed-meta={isFailed}>{secondaryMeta}</span>
{/if}
{#if badges}
{#each badges as badge}
<span class="meta-badge">
{#if badge.icon === 'warning'}
<AlertTriangle size={10} />
{:else}
<MessageSquare size={10} />
{/if}
<span>{badge.count}</span>
</span>
{/each}
{/if}
</div>
{/if}
</div>
Expand Down Expand Up @@ -318,7 +337,7 @@

.timeline-meta {
display: flex;
align-items: baseline;
align-items: center;
gap: 8px;
margin-top: 3px;
}
Expand All @@ -332,6 +351,20 @@
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
}

.meta-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
border-radius: 8px;
background: none;
border: 1px solid var(--border-subtle);
color: var(--text-muted);
font-size: calc(var(--size-xs) - 1px);
font-weight: 600;
line-height: 1;
}

/* Actions container — visible on row hover */
.timeline-actions {
display: flex;
Expand Down
14 changes: 13 additions & 1 deletion packages/diff-viewer/src/lib/utils/diffModalHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Comment, FileDiffSummary } from '../types';
import type { Comment, CommentType, FileDiffSummary } from '../types';
import { fileSummaryPath } from '../state/diffViewerState.svelte';

export interface FileEntry {
path: string;
status: 'added' | 'deleted' | 'modified' | 'renamed';
isReviewed: boolean;
commentCount: number;
/** The distinct comment types present on this file (e.g. 'warning', 'suggestion'). */
commentTypes: CommentType[];
}

export interface TreeNode {
Expand All @@ -30,8 +32,17 @@ export function buildFileEntries(
): FileEntry[] {
const reviewedSet = new Set(reviewedPaths);
const commentCounts = new Map<string, number>();
const commentTypesByFile = new Map<string, Set<CommentType>>();
for (const comment of comments) {
commentCounts.set(comment.path, (commentCounts.get(comment.path) || 0) + 1);
if (comment.commentType) {
let types = commentTypesByFile.get(comment.path);
if (!types) {
types = new Set();
commentTypesByFile.set(comment.path, types);
}
types.add(comment.commentType);
}
}

return files.map((summary) => {
Expand All @@ -41,6 +52,7 @@ export function buildFileEntries(
status: fileStatus(summary),
isReviewed: reviewedSet.has(path),
commentCount: commentCounts.get(path) || 0,
commentTypes: [...(commentTypesByFile.get(path) || [])],
};
});
}
Expand Down
Loading