feat(showcase_ui): support verification and finding cards in agent notes

This commit is contained in:
somew1nd committed 2026-09-02 00:58:48 -07:00
1 parent 99343dfe55
commit 73d7b0c116
7 files changed
+494 -52

No files matched your search

@@ -277,6 +277,30 @@ limitations under the License.
}
</div>
}
<!-- Verification Box (Pulled out into a minimalist framed card) -->
@if (milestone.checks.length > 0) {
<div class="milestone-checks-container">
@for (check of milestone.checks; track trackMarkdownLine($index, check)) {
<div
class="verification-card"
[ngClass]="check.type"
[style.margin-left.px]="(check.indent > 2 ? (check.indent - 2) * 12 : 0)"
>
<div class="card-header">
<span class="card-tag" [class.assert]="check.checkKind === 'assert'">{{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of check.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
}
</div>
}
</div>
}
</div>
@@ -285,36 +309,52 @@ limitations under the License.
@if (parsedNote().otherLines.length > 0) {
<div class="other-lines-container">
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@if (line.type === 'verify' || line.type === 'finding') {
<div class="verification-card standalone" [ngClass]="line.type" [style.margin-left.px]="line.indent * 12">
<div class="card-header">
<span class="card-tag" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
</div>
</div>
} @else {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
}
</div>
}
@@ -1110,6 +1150,12 @@ limitations under the License.
<div class="parsed-other-lines">
@for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {
<p class="other-line" [class]="'line-' + line.type">
@if (line.type === 'verify') {
<span class="verify-badge" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }}</span>
}
@if (line.type === 'finding') {
<span class="finding-badge">finding</span>
}
@for (seg of line.segments; track seg) {
@if (seg.bold) {
<strong>{{ seg.text }}</strong>
@@ -404,6 +404,76 @@ $stream-row-height: 25px;
}
}
&.verify {
color: #334155;
gap: 8px;
align-items: flex-start;
.verify-badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
font-size: 10.5px;
font-weight: 550;
line-height: 1.2;
letter-spacing: 0.2px;
text-transform: lowercase;
color: #475569;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 4px;
padding: 1.5px 6px;
margin-top: 1.5px;
user-select: none;
&.assert {
color: #0369a1;
background: #f0f9ff;
border-color: #e0f2fe;
}
}
.line-text {
color: #334155;
font-size: 12.5px;
line-height: 1.5;
}
}
&.finding {
color: #92400e;
gap: 8px;
align-items: flex-start;
.finding-badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
font-size: 10.5px;
font-weight: 550;
line-height: 1.2;
letter-spacing: 0.2px;
text-transform: lowercase;
color: #b45309;
background: #fef3c7;
border: 1px solid #fde68a;
border-radius: 4px;
padding: 1.5px 6px;
margin-top: 1.5px;
user-select: none;
}
.line-text {
color: #92400e;
font-size: 12.5px;
line-height: 1.5;
}
}
.check-icon {
font-size: 16px;
user-select: none;
@@ -430,6 +500,84 @@ $stream-row-height: 25px;
}
}
}
.milestone-checks-container {
margin-top: 6px;
padding-left: 26px;
display: flex;
flex-direction: column;
gap: 6px;
}
}
.verification-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 7px 10px;
display: flex;
flex-direction: column;
gap: 3px;
&.standalone {
margin-top: 6px;
margin-bottom: 6px;
}
.card-header {
display: flex;
align-items: center;
.card-tag {
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
font-size: 9.5px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.6px;
text-transform: uppercase;
color: #64748b;
&.assert {
color: #0284c7;
}
}
}
.card-body {
font-family: inherit;
font-size: 12px;
line-height: 1.5;
color: #334155;
word-break: break-word;
.bold-segment {
font-weight: 600;
color: #1e293b;
}
.code-segment {
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
font-size: 11px;
background: #f1f5f9;
color: #0f172a;
padding: 1px 5px;
border-radius: 4px;
border: 1px solid #e2e8f0;
}
}
&.finding {
background: #fffbeb;
border-color: #fef3c7;
.card-header .card-tag {
color: #b45309;
}
.card-body {
color: #92400e;
}
}
}
.other-lines-container {
@@ -447,6 +595,11 @@ $stream-row-height: 25px;
font-size: 12.5px;
color: #334155;
line-height: 1.5;
.line-text {
word-break: break-word;
color: inherit;
}
}
}
}
@@ -1763,6 +1916,14 @@ $stream-row-height: 25px;
color: #1f1f1f;
margin: 4px 0;
padding-left: 2px;
&.md-verify-item {
color: #334155;
}
&.md-finding-item {
color: #92400e;
}
}
}
@@ -290,6 +290,30 @@ limitations under the License.
}
</div>
}
<!-- Verification Box (Pulled out into a minimalist framed card) -->
@if (milestone.checks.length > 0) {
<div class="milestone-checks-container">
@for (check of milestone.checks; track trackMarkdownLine($index, check)) {
<div
class="verification-card"
[ngClass]="check.type"
[style.margin-left.px]="(check.indent > 2 ? (check.indent - 2) * 12 : 0)"
>
<div class="card-header">
<span class="card-tag" [class.assert]="check.checkKind === 'assert'">{{ check.checkKind || 'verify' }}{{ check.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of check.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</div>
</div>
}
</div>
}
</div>
}
</div>
@@ -298,36 +322,52 @@ limitations under the License.
@if (parsedNote().otherLines.length > 0) {
<div class="other-lines-container">
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@if (line.type === 'verify' || line.type === 'finding') {
<div class="verification-card standalone" [ngClass]="line.type" [style.margin-left.px]="line.indent * 12">
<div class="card-header">
<span class="card-tag" [class.assert]="line.checkKind === 'assert'">{{ line.checkKind || 'verify' }}{{ line.atEnd ? ' @end' : '' }}</span>
</div>
<div class="card-body">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
</div>
</div>
} @else {
<div
class="md-line"
[ngClass]="line.type"
[style.margin-left.px]="line.indent * 12"
>
@switch (line.type) {
@case ('checked') {
<span class="material-symbols-outlined check-icon checked">check_box</span>
}
@case ('progress') {
<span class="material-symbols-outlined check-icon progress">indeterminate_check_box</span>
}
@case ('unchecked') {
<span class="material-symbols-outlined check-icon unchecked">check_box_outline_blank</span>
}
@case ('list-item') {
<span class="bullet">•</span>
}
}
@if (line.type !== 'empty') {
<span class="line-text">
@for (seg of line.segments; track seg) {
<span
[class.bold-segment]="seg.bold"
[class.code-segment]="seg.code"
>{{ seg.text }}</span>
}
</span>
}
</div>
}
}
</div>
}
@@ -878,6 +878,84 @@ $text-muted: #a1a1aa;
}
}
}
.milestone-checks-container {
margin-top: 8px;
padding-left: 28px;
display: flex;
flex-direction: column;
gap: 8px;
}
}
.verification-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 4px;
&.standalone {
margin-top: 8px;
margin-bottom: 8px;
}
.card-header {
display: flex;
align-items: center;
.card-tag {
font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
font-size: 10.5px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.6px;
text-transform: uppercase;
color: #64748b;
&.assert {
color: #0284c7;
}
}
}
.card-body {
font-family: inherit;
font-size: 13.5px;
line-height: 1.6;
color: #334155;
word-break: break-word;
.bold-segment {
font-weight: 600;
color: #1e293b;
}
.code-segment {
font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
font-size: 12px;
background: #f1f5f9;
color: #0f172a;
padding: 1.5px 6px;
border-radius: 4px;
border: 1px solid #e2e8f0;
}
}
&.finding {
background: #fffbeb;
border-color: #fef3c7;
.card-header .card-tag {
color: #b45309;
}
.card-body {
color: #92400e;
}
}
}
.other-lines-container {
@@ -21,9 +21,11 @@ export interface MarkdownSegment {
}
export interface MarkdownLine {
type: 'h1' | 'h2' | 'h3' | 'checked' | 'progress' | 'unchecked' | 'list-item' | 'text' | 'empty';
type: 'h1' | 'h2' | 'h3' | 'checked' | 'progress' | 'unchecked' | 'list-item' | 'verify' | 'assert' | 'finding' | 'text' | 'empty';
segments: MarkdownSegment[];
indent: number;
checkKind?: 'verify' | 'assert' | 'finding';
atEnd?: boolean;
}
export interface NoteMilestone {
@@ -31,6 +33,7 @@ export interface NoteMilestone {
type: 'checked' | 'progress' | 'unchecked';
segments: MarkdownSegment[];
subSteps: MarkdownLine[];
checks: MarkdownLine[];
}
export interface ParsedNote {
@@ -0,0 +1,70 @@
import { parseNoteLines, parseNote, renderMarkdownToHtml } from './markdown-parser.util';
describe('markdown-parser.util verification & check lines', () => {
it('should parse - verify: line as type verify with checkKind verify', () => {
const markdown = `- [x] Parent task
- [x] Subtask 1
- verify: Commute duration is recorded in note \`eta_details\``;
const lines = parseNoteLines(markdown);
expect(lines.length).toBe(3);
expect(lines[0].type).toBe('checked');
expect(lines[1].type).toBe('checked');
const verifyLine = lines[2];
expect(verifyLine.type).toBe('verify');
expect(verifyLine.checkKind).toBe('verify');
expect(verifyLine.atEnd).toBe(false);
expect(verifyLine.segments.map(s => s.text).join('')).toBe('Commute duration is recorded in note eta_details');
expect(verifyLine.segments.find(s => s.code)?.text).toBe('eta_details');
});
it('should parse - assert: and - assert@end: lines', () => {
const markdown = `- [ ] Open app
- assert: the welcome screen shows up
- assert@end: final status is completed`;
const lines = parseNoteLines(markdown);
expect(lines.length).toBe(3);
expect(lines[1].type).toBe('verify');
expect(lines[1].checkKind).toBe('assert');
expect(lines[1].atEnd).toBe(false);
expect(lines[2].type).toBe('verify');
expect(lines[2].checkKind).toBe('assert');
expect(lines[2].atEnd).toBe(true);
});
it('should parse - finding: lines', () => {
const markdown = `- [x] Step 1
- finding: Unresolved verify failure`;
const lines = parseNoteLines(markdown);
expect(lines[1].type).toBe('finding');
expect(lines[1].checkKind).toBe('finding');
expect(lines[1].segments[0].text).toBe('Unresolved verify failure');
});
it('should group verify lines under parent milestone checks in parseNote', () => {
const note = `- [x] Open Maps
- [x] Search destination
- verify: Arrival time is visible in note \`eta\``;
const parsed = parseNote(note);
expect(parsed.milestones.length).toBe(1);
expect(parsed.milestones[0].subSteps.length).toBe(1);
expect(parsed.milestones[0].subSteps[0].type).toBe('checked');
expect(parsed.milestones[0].checks.length).toBe(1);
expect(parsed.milestones[0].checks[0].type).toBe('verify');
expect(parsed.milestones[0].checks[0].checkKind).toBe('verify');
});
it('should render verify badges in renderMarkdownToHtml', () => {
const text = `- verify: status is ok`;
const html = renderMarkdownToHtml(text);
expect(html).toContain('class="md-verify-item"');
expect(html).toContain('<span class="verify-badge">verify</span>');
expect(html).toContain('status is ok');
});
});
@@ -72,6 +72,31 @@ export function parseNoteLines(content: string): MarkdownLine[] {
const rawText = trimmed.substring(6);
return { type: 'unchecked', segments: parseMarkdownSegments(rawText), indent };
}
// Check lines (- verify: / - assert: / - verify@end: / - assert@end:)
const checkMatch = trimmed.match(/^[-*]\s*(verify|assert)(@end)?\s*:\s*(.*)$/i);
if (checkMatch) {
const kind = checkMatch[1].toLowerCase() as 'verify' | 'assert';
const atEnd = Boolean(checkMatch[2]);
const rawText = checkMatch[3].trim();
return {
type: 'verify',
checkKind: kind,
atEnd,
segments: parseMarkdownSegments(rawText),
indent
};
}
// System findings (- finding:)
const findingMatch = trimmed.match(/^[-*]\s*finding\s*:\s*(.*)$/i);
if (findingMatch) {
const rawText = findingMatch[1].trim();
return {
type: 'finding',
checkKind: 'finding',
segments: parseMarkdownSegments(rawText),
indent
};
}
// Regular list item
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
const rawText = trimmed.substring(2);
@@ -125,12 +150,17 @@ export function parseNote(content: string): ParsedNote {
type: line.type as 'checked' | 'progress' | 'unchecked',
segments: line.segments,
subSteps: [],
checks: [],
_indent: line.indent
};
milestones.push(currentMilestone);
} else if (currentMilestone) {
if (line.indent > (currentMilestone._indent ?? 0)) {
currentMilestone.subSteps.push(line);
if (line.type === 'verify' || line.type === 'assert' || line.type === 'finding') {
currentMilestone.checks.push(line);
} else {
currentMilestone.subSteps.push(line);
}
} else {
otherLines.push(line);
}
@@ -297,8 +327,22 @@ export function renderMarkdownToHtml(text: string): string {
const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
const indent = subLevel * 12;
const style = indent ? ` style="margin-left: ${indent}px"` : '';
const itemContent = formatInlineMarkdown(ulMatch[3]);
result.push(`<li${style}>${itemContent}</li>`);
const itemText = ulMatch[3];
const checkMatch = itemText.match(/^(verify|assert)(@end)?\s*:\s*(.*)$/i);
const findingMatch = itemText.match(/^finding\s*:\s*(.*)$/i);
if (checkMatch) {
const kind = checkMatch[1].toLowerCase();
const atEnd = checkMatch[2] ? '@end' : '';
const assertClass = kind === 'assert' ? ' assert' : '';
const itemContent = formatInlineMarkdown(checkMatch[3].trim());
result.push(`<li${style} class="md-verify-item"><span class="verify-badge${assertClass}">${kind}${atEnd}</span>${itemContent}</li>`);
} else if (findingMatch) {
const itemContent = formatInlineMarkdown(findingMatch[1].trim());
result.push(`<li${style} class="md-finding-item"><span class="finding-badge">finding</span>${itemContent}</li>`);
} else {
const itemContent = formatInlineMarkdown(itemText);
result.push(`<li${style}>${itemContent}</li>`);
}
continue;
}