Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable user-visible changes to Hunk are documented in this file.

### Added

- Surfaced the agent author name in inline notes and the matching agent popover so multi-agent reviews are readable at a glance, with a fallback title when an annotation has no author.
- Tinted inline agent notes per author with a stable per-theme accent — borders, the title-row right-edge bar, the guide cap below multi-row ranges, and a soft body/title background — so notes from different agents are visually distinct on the same diff.

### Changed

### Fixed
Expand Down
18 changes: 14 additions & 4 deletions examples/3-agent-review-demo/agent-context.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
{
"newRange": [1, 3],
"summary": "Adds one normalization helper for whitespace, case, and dashed shortcut terms.",
"rationale": "This lets the search layer reason about one normalized token shape instead of repeating slightly different cleanup logic in multiple places."
"rationale": "This lets the search layer reason about one normalized token shape instead of repeating slightly different cleanup logic in multiple places.",
"author": "llama"
}
]
},
Expand All @@ -20,7 +21,14 @@
{
"newRange": [15, 35],
"summary": "Prefix and exact keyword matches now outrank weaker substring hits before the result list is sorted.",
"rationale": "The old behavior made every match look equally good, which was fine for filtering but weak for command-palette ranking where the top result should usually be the most obvious intent."
"rationale": "The old behavior made every match look equally good, which was fine for filtering but weak for command-palette ranking where the top result should usually be the most obvious intent.",
"author": "grok"
},
{
"newRange": [20, 27],
"summary": "Worth checking the score floor — could mask edge cases.",
"rationale": "The scoring thresholds (4, 3, 2, 1) look good but validate that zero-score items are properly filtered out.",
"author": "gemini"
}
]
},
Expand All @@ -31,7 +39,8 @@
{
"newRange": [1, 8],
"summary": "The preview now shows only the top three ranked commands.",
"rationale": "Once ranking is reliable, the preview can stay compact and let the best results carry the review without flooding the UI."
"rationale": "Once ranking is reliable, the preview can stay compact and let the best results carry the review without flooding the UI.",
"author": "phi"
}
]
},
Expand All @@ -42,7 +51,8 @@
{
"newRange": [1, 8],
"summary": "The test covers a dashed query form so the new normalization helper has a visible behavioral contract.",
"rationale": "Without a test that exercises `short-cuts` specifically, it would be easy to regress the helper and still pass on simpler substring-only cases."
"rationale": "Without a test that exercises `short-cuts` specifically, it would be easy to regress the helper and still pass on simpler substring-only cases.",
"author": "sonnet"
}
]
}
Expand Down
7 changes: 6 additions & 1 deletion src/ui/components/panes/AgentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { buildAgentPopoverContent } from "../../lib/agentPopover";
import { fitText, padText } from "../../lib/text";
import { resolveAuthorAccent } from "../../lib/agentColor";
import type { AppTheme } from "../../themes";

/** Render one framed floating agent note popover. */
Expand All @@ -12,6 +13,7 @@ export function AgentCard({
summary,
theme,
width,
author,
}: {
locationLabel: string;
noteCount?: number;
Expand All @@ -21,6 +23,7 @@ export function AgentCard({
summary: string;
theme: AppTheme;
width: number;
author?: string;
}) {
const popover = buildAgentPopoverContent({
summary,
Expand All @@ -29,7 +32,9 @@ export function AgentCard({
noteIndex,
noteCount,
width,
author,
});
const accent = resolveAuthorAccent(author, theme.noteAccentPalette) ?? theme.accent;
const titleWidth = Math.max(1, popover.innerWidth - (onClose ? 4 : 0));

return (
Expand All @@ -38,7 +43,7 @@ export function AgentCard({
width,
height: popover.height,
border: true,
borderColor: theme.accent,
borderColor: accent,
backgroundColor: theme.panel,
paddingLeft: 1,
paddingRight: 1,
Expand Down
46 changes: 29 additions & 17 deletions src/ui/components/panes/AgentInlineNote.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { AgentAnnotation, LayoutMode } from "../../../core/types";
import { wrapText } from "../../lib/agentPopover";
import { formatAgentNoteTitle, wrapText } from "../../lib/agentPopover";
import { annotationRangeLabel } from "../../lib/agentAnnotations";
import { fitText, padText } from "../../lib/text";
import {
deriveAuthorBackground,
deriveAuthorTitleBackground,
resolveAuthorAccent,
} from "../../lib/agentColor";
import type { AppTheme } from "../../themes";

function inlineNoteTitle(noteIndex: number, noteCount: number) {
return noteCount > 1 ? `AI note ${noteIndex + 1}/${noteCount}` : "AI note";
}

interface AgentInlineNoteLine {
kind: "summary" | "rationale";
text: string;
Expand Down Expand Up @@ -83,7 +84,15 @@ export function AgentInlineNote({
width: number;
}) {
const closeText = onClose ? "[x]" : "";
const titleText = `${inlineNoteTitle(noteIndex, noteCount)} · ${annotationRangeLabel(annotation)}`;
const titleText = `${formatAgentNoteTitle(noteIndex, noteCount, annotation.author)} · ${annotationRangeLabel(annotation)}`;
const resolvedAccent = resolveAuthorAccent(annotation.author, theme.noteAccentPalette);
const accent = resolvedAccent ?? theme.noteBorder;
const noteBackground = resolvedAccent
? (deriveAuthorBackground(resolvedAccent, theme.appearance) ?? theme.noteBackground)
: theme.noteBackground;
const noteTitleBackground = resolvedAccent
? (deriveAuthorTitleBackground(resolvedAccent, theme.appearance) ?? theme.noteTitleBackground)
: theme.noteTitleBackground;
const splitWidths = splitColumnWidths(width);
const canDockRight = layout === "split" && anchorSide === "new" && width >= 84;
const canDockLeft = layout === "split" && anchorSide === "old" && width >= 84;
Expand Down Expand Up @@ -125,7 +134,7 @@ export function AgentInlineNote({
<text>{" ".repeat(boxLeft)}</text>
</box>
<box style={{ width: boxWidth, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
{topBorder}
</text>
</box>
Expand All @@ -136,12 +145,12 @@ export function AgentInlineNote({
<text>{" ".repeat(boxLeft)}</text>
</box>
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
│
</text>
</box>
<box style={{ width: titleWidth, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteTitleText} bg={theme.noteTitleBackground}>
<text fg={theme.noteTitleText} bg={noteTitleBackground}>
{padText(fitText(titleText, titleWidth), titleWidth)}
</text>
</box>
Expand All @@ -150,11 +159,11 @@ export function AgentInlineNote({
onMouseUp={onClose}
style={{ width: closeText.length + 1, height: 1, backgroundColor: theme.panel }}
>
<text fg={theme.noteTitleText} bg={theme.noteTitleBackground}>{` ${closeText}`}</text>
<text fg={theme.noteTitleText} bg={noteTitleBackground}>{` ${closeText}`}</text>
</box>
) : null}
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
│
</text>
</box>
Expand All @@ -169,17 +178,17 @@ export function AgentInlineNote({
<text>{" ".repeat(boxLeft)}</text>
</box>
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
│
</text>
</box>
<box style={{ width: bodyWidth, height: 1, backgroundColor: theme.panel }}>
<text fg={line.kind === "summary" ? theme.text : theme.muted} bg={theme.noteBackground}>
<text fg={line.kind === "summary" ? theme.text : theme.muted} bg={noteBackground}>
{padText(line.text, bodyWidth)}
</text>
</box>
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
│
</text>
</box>
Expand All @@ -191,7 +200,7 @@ export function AgentInlineNote({
<text>{" ".repeat(boxLeft)}</text>
</box>
<box style={{ width: boxWidth, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder} bg={theme.noteBackground}>
<text fg={accent} bg={noteBackground}>
{bottomBorder}
</text>
</box>
Expand All @@ -205,17 +214,20 @@ export function AgentInlineNoteGuideCap({
side,
theme,
width,
accent,
}: {
side: "old" | "new";
theme: AppTheme;
width: number;
accent?: string;
}) {
const borderColor = accent ?? theme.noteBorder;
return (
<box style={{ width: "100%", height: 1, flexDirection: "row", backgroundColor: theme.panel }}>
{side === "old" ? (
<>
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder}>╵</text>
<text fg={borderColor}>╵</text>
</box>
<box style={{ width: Math.max(0, width - 1), height: 1, backgroundColor: theme.panel }}>
<text>{" ".repeat(Math.max(0, width - 1))}</text>
Expand All @@ -227,7 +239,7 @@ export function AgentInlineNoteGuideCap({
<text>{" ".repeat(Math.max(0, width - 1))}</text>
</box>
<box style={{ width: 1, height: 1, backgroundColor: theme.panel }}>
<text fg={theme.noteBorder}>╵</text>
<text fg={borderColor}>╵</text>
</box>
</>
)}
Expand Down
Loading