diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
index f4b4bd1..0bb4fc2 100644
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
+++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
@@ -277,6 +277,30 @@ limitations under the License.
}
}
+
+ @if (milestone.checks.length > 0) {
+
+ @for (check of milestone.checks; track trackMarkdownLine($index, check)) {
+
2 ? (check.indent - 2) * 12 : 0)"
+ >
+
+
+ @for (seg of check.segments; track seg) {
+ {{ seg.text }}
+ }
+
+
+ }
+
+ }
}
@@ -285,36 +309,52 @@ limitations under the License.
@if (parsedNote().otherLines.length > 0) {
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
-
- @switch (line.type) {
- @case ('checked') {
-
check_box
- }
- @case ('progress') {
-
indeterminate_check_box
- }
- @case ('unchecked') {
-
check_box_outline_blank
- }
- @case ('list-item') {
-
•
- }
- }
- @if (line.type !== 'empty') {
-
+ @if (line.type === 'verify' || line.type === 'finding') {
+
+
+
@for (seg of line.segments; track seg) {
{{ seg.text }}
}
-
- }
-
+
+
+ } @else {
+
+ @switch (line.type) {
+ @case ('checked') {
+ check_box
+ }
+ @case ('progress') {
+ indeterminate_check_box
+ }
+ @case ('unchecked') {
+ check_box_outline_blank
+ }
+ @case ('list-item') {
+ •
+ }
+ }
+ @if (line.type !== 'empty') {
+
+ @for (seg of line.segments; track seg) {
+ {{ seg.text }}
+ }
+
+ }
+
+ }
}
}
@@ -1110,6 +1150,12 @@ limitations under the License.
@for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {
+ @if (line.type === 'verify') {
+ {{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }}
+ }
+ @if (line.type === 'finding') {
+ finding
+ }
@for (seg of line.segments; track seg) {
@if (seg.bold) {
{{ seg.text }}
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss
index 169ec72..b12e560 100644
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss
+++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss
@@ -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;
+ }
}
}
diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
index baf3a39..5d9d053 100644
--- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
+++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
@@ -290,6 +290,30 @@ limitations under the License.
}
}
+
+ @if (milestone.checks.length > 0) {
+
+ @for (check of milestone.checks; track trackMarkdownLine($index, check)) {
+
2 ? (check.indent - 2) * 12 : 0)"
+ >
+
+
+ @for (seg of check.segments; track seg) {
+ {{ seg.text }}
+ }
+
+
+ }
+
+ }
}
@@ -298,36 +322,52 @@ limitations under the License.
@if (parsedNote().otherLines.length > 0) {
@for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
-
- @switch (line.type) {
- @case ('checked') {
-
check_box
- }
- @case ('progress') {
-
indeterminate_check_box
- }
- @case ('unchecked') {
-
check_box_outline_blank
- }
- @case ('list-item') {
-
•
- }
- }
- @if (line.type !== 'empty') {
-
+ @if (line.type === 'verify' || line.type === 'finding') {
+
+
+
@for (seg of line.segments; track seg) {
{{ seg.text }}
}
-
- }
-
+
+
+ } @else {
+
+ @switch (line.type) {
+ @case ('checked') {
+ check_box
+ }
+ @case ('progress') {
+ indeterminate_check_box
+ }
+ @case ('unchecked') {
+ check_box_outline_blank
+ }
+ @case ('list-item') {
+ •
+ }
+ }
+ @if (line.type !== 'empty') {
+
+ @for (seg of line.segments; track seg) {
+ {{ seg.text }}
+ }
+
+ }
+
+ }
}
}
diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss
index 577ac88..2de9b95 100644
--- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss
+++ b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss
@@ -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 {
diff --git a/apps/showcase_ui/src/app/core/models/markdown.model.ts b/apps/showcase_ui/src/app/core/models/markdown.model.ts
index b748481..10b9add 100644
--- a/apps/showcase_ui/src/app/core/models/markdown.model.ts
+++ b/apps/showcase_ui/src/app/core/models/markdown.model.ts
@@ -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 {
diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts
new file mode 100644
index 0000000..0fd04de
--- /dev/null
+++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts
@@ -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('verify');
+ expect(html).toContain('status is ok');
+ });
+});
diff --git a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts
index bc8ca44..0b3e377 100644
--- a/apps/showcase_ui/src/app/utils/markdown-parser.util.ts
+++ b/apps/showcase_ui/src/app/utils/markdown-parser.util.ts
@@ -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(`${itemContent}`);
+ 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(`${kind}${atEnd}${itemContent}`);
+ } else if (findingMatch) {
+ const itemContent = formatInlineMarkdown(findingMatch[1].trim());
+ result.push(`finding${itemContent}`);
+ } else {
+ const itemContent = formatInlineMarkdown(itemText);
+ result.push(`${itemContent}`);
+ }
continue;
}