p:first-child]:mt-0px [&>p:last-child]:mb-0px', {
- 'bg-aou-2 p-6px md:p-8px': isUserMessage || cronMeta,
- 'bg-3 p-6px md:p-8px': isTeammateMessage,
- 'w-full': !(isUserMessage || cronMeta || isTeammateMessage),
- })}
- style={{
- ...(isUserMessage || cronMeta
- ? { borderRadius: '8px 0 8px 8px', color: 'var(--text-primary)' }
- : isTeammateMessage
- ? {
- borderRadius: '0 8px 8px 8px',
- ...(teammateColor ? { borderLeft: `3px solid ${teammateColor}` } : {}),
- }
- : undefined),
- }}
- >
- {/* JSON 内容使用折叠组件 Use CollapsibleContent for JSON content */}
- {shouldRenderPlainText ? (
-
- {renderedText}
-
- ) : json ? (
-
+ {Boolean(renderedText && renderedText.trim()) && (
+ p:first-child]:mt-0px [&>p:last-child]:mb-0px', {
+ 'bg-aou-2 p-6px md:p-8px': isUserMessage || cronMeta,
+ 'bg-3 p-6px md:p-8px': isTeammateMessage,
+ 'w-full': !(isUserMessage || cronMeta || isTeammateMessage),
+ })}
+ style={{
+ ...(isUserMessage || cronMeta
+ ? { borderRadius: '8px 0 8px 8px', color: 'var(--text-primary)' }
+ : isTeammateMessage
+ ? {
+ borderRadius: '0 8px 8px 8px',
+ ...(teammateColor ? { borderLeft: `3px solid ${teammateColor}` } : {}),
+ }
+ : undefined),
+ }}
+ >
+ {/* JSON 内容使用折叠组件 Use CollapsibleContent for JSON content */}
+ {shouldRenderPlainText ? (
+
+ {renderedText}
+
+ ) : json ? (
+
+
+ {`\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``}
+
+
+ ) : (
- {`\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``}
+
+ {data}
+
-
- ) : (
-
-
- {data}
-
-
- )}
-
+ )}
+
+ )}
+ {walkthrough &&
{t('messages.delivery.pending', { defaultValue: 'Unread' })}
diff --git a/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/WalkthroughCard.module.css b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/WalkthroughCard.module.css
new file mode 100644
index 00000000000..ede42fd141a
--- /dev/null
+++ b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/WalkthroughCard.module.css
@@ -0,0 +1,179 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+.card {
+ width: 100%;
+ margin-top: 10px;
+ margin-bottom: 8px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--color-primary-6) 28%, var(--color-border-2));
+ border-radius: 12px;
+ background: var(--color-bg-2);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
+ transition:
+ border-color 0.2s ease,
+ box-shadow 0.2s ease;
+}
+
+.card:hover {
+ border-color: color-mix(in srgb, var(--color-primary-6) 45%, var(--color-border-2));
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
+}
+
+.header {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px;
+ border-bottom: 1px solid transparent;
+ background: var(--color-fill-1);
+ cursor: pointer;
+ user-select: none;
+ transition: background-color 0.16s ease;
+}
+
+.headerExpanded {
+ border-bottom-color: var(--color-border-2);
+}
+
+.headerLeft {
+ display: flex;
+ min-width: 0;
+ flex: 1;
+ align-items: center;
+ gap: 8px;
+}
+
+.iconBox {
+ display: flex;
+ width: 26px;
+ height: 26px;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ border-radius: 7px;
+ color: var(--color-primary-6);
+ background: var(--color-primary-light-1);
+}
+
+.title {
+ display: block;
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+ color: var(--color-text-1);
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.4;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.badge {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ padding: 1px 7px;
+ border-radius: 999px;
+ color: var(--color-primary-6);
+ background: var(--color-primary-light-1);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.4;
+}
+
+.headerRight {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 6px;
+ margin-inline-start: 12px;
+}
+
+.counterBadge {
+ color: var(--color-text-3);
+ font-size: 12px;
+}
+
+.body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px 14px;
+}
+
+.summaryBox {
+ padding: 8px 12px;
+ border: 1px solid var(--color-border-2);
+ border-radius: 8px;
+ color: var(--color-text-2);
+ background: var(--color-fill-1);
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.section {
+ overflow: hidden;
+ border: 1px solid var(--color-border-2);
+ border-radius: 8px;
+ background: var(--color-bg-1);
+ transition: border-color 0.16s ease;
+}
+
+.section:hover {
+ border-color: color-mix(in srgb, var(--color-primary-6) 30%, var(--color-border-2));
+}
+
+.sectionHeader {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: var(--color-fill-1);
+ cursor: pointer;
+ user-select: none;
+ transition: background-color 0.16s ease;
+}
+
+.sectionHeader:hover {
+ background: var(--color-fill-2);
+}
+
+.sectionHeaderLeft {
+ display: flex;
+ min-width: 0;
+ flex: 1;
+ align-items: center;
+ gap: 8px;
+}
+
+.sectionTitle {
+ color: var(--color-text-1);
+ font-size: 12px;
+ font-weight: 600;
+ overflow-wrap: anywhere;
+}
+
+.sectionContent {
+ padding: 10px 12px;
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.actionButton:global(.arco-btn) {
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border-radius: 6px;
+ color: var(--color-text-2);
+}
+
+.actionButton:global(.arco-btn):hover {
+ color: var(--color-text-1);
+ background: var(--color-fill-2);
+}
diff --git a/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/index.tsx b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/index.tsx
new file mode 100644
index 00000000000..01081f33308
--- /dev/null
+++ b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/index.tsx
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Button, Tooltip, Message } from '@arco-design/web-react';
+import { Attention, Brain, Check, CheckOne, Compass, Copy, Down, FileText, PlayOne, Up } from '@icon-park/react';
+import classNames from 'classnames';
+import MarkdownView from '@renderer/components/Markdown';
+import { copyText } from '@/renderer/utils/ui/clipboard';
+import type { WalkthroughData, WalkthroughSectionType } from './types';
+import styles from './WalkthroughCard.module.css';
+
+const CODE_STYLE = { marginTop: 4, marginBlock: 4 };
+
+interface WalkthroughCardProps {
+ walkthrough: WalkthroughData;
+ onLocalFileLink?: (path: string) => void;
+}
+
+const renderSectionIcon = (type: WalkthroughSectionType) => {
+ switch (type) {
+ case 'delivered':
+ return
;
+ case 'howItWorks':
+ return
;
+ case 'usage':
+ return
;
+ case 'notes':
+ return
;
+ default:
+ return
;
+ }
+};
+
+export const WalkthroughCard: React.FC
= ({ walkthrough, onLocalFileLink }) => {
+ const { t } = useTranslation();
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [collapsedSections, setCollapsedSections] = useState>({});
+ const [copied, setCopied] = useState(false);
+
+ const displayTitle = walkthrough.title || t('messages.walkthrough.title', { defaultValue: 'Execution Walkthrough' });
+
+ const handleCopy = useCallback(
+ async (e?: { stopPropagation?: () => void }) => {
+ e?.stopPropagation?.();
+ try {
+ await copyText(walkthrough.rawContent);
+ setCopied(true);
+ Message.success(t('messages.walkthrough.copySuccess', { defaultValue: 'Walkthrough copied to clipboard' }));
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ Message.error(t('messages.walkthrough.copyFailed', { defaultValue: 'Failed to copy walkthrough' }));
+ console.error('[WalkthroughCard] Failed to copy:', err);
+ }
+ },
+ [t, walkthrough.rawContent]
+ );
+
+ const toggleSection = useCallback((sectionId: string) => {
+ setCollapsedSections((prev) => ({
+ ...prev,
+ [sectionId]: !prev[sectionId],
+ }));
+ }, []);
+
+ return (
+
+ {/* Header */}
+
setIsExpanded(!isExpanded)}
+ role='button'
+ tabIndex={0}
+ aria-expanded={isExpanded}
+ >
+
+
+
+
+
+ {displayTitle}
+
+
{t('messages.walkthrough.badge', { defaultValue: 'Walkthrough' })}
+
+
+
+ {!isExpanded && (
+
+ {t('messages.walkthrough.sectionsCount', {
+ count: walkthrough.sections.length,
+ defaultValue: `${walkthrough.sections.length} sections`,
+ })}
+
+ )}
+
+
+ {
+ void handleCopy(e);
+ }}
+ aria-label={t('messages.walkthrough.copy', { defaultValue: 'Copy Walkthrough' })}
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {
+ e.stopPropagation();
+ setIsExpanded(!isExpanded);
+ }}
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
+ >
+ {isExpanded ? : }
+
+
+
+
+
+ {/* Expanded Body */}
+ {isExpanded && (
+
+ {walkthrough.summary && (
+
+
+ {walkthrough.summary}
+
+
+ )}
+
+ {walkthrough.sections.map((section) => {
+ const isSectionCollapsed = Boolean(collapsedSections[section.id]);
+ return (
+
+
toggleSection(section.id)}
+ role='button'
+ tabIndex={0}
+ aria-expanded={!isSectionCollapsed}
+ >
+
+ {renderSectionIcon(section.type)}
+ {section.title}
+
+
{
+ e.stopPropagation();
+ toggleSection(section.id);
+ }}
+ aria-label={isSectionCollapsed ? 'Expand section' : 'Collapse section'}
+ >
+ {isSectionCollapsed ? : }
+
+
+
+ {!isSectionCollapsed && (
+
+
+ {section.content}
+
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ );
+};
+
+export default WalkthroughCard;
diff --git a/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/types.ts b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/types.ts
new file mode 100644
index 00000000000..56a99ca2bb8
--- /dev/null
+++ b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/types.ts
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export type WalkthroughSectionType = 'delivered' | 'howItWorks' | 'usage' | 'notes' | 'custom';
+
+export interface WalkthroughSection {
+ id: string;
+ type: WalkthroughSectionType;
+ title: string;
+ content: string;
+}
+
+export interface WalkthroughData {
+ title: string;
+ summary?: string;
+ sections: WalkthroughSection[];
+ rawContent: string;
+}
diff --git a/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/walkthroughParser.ts b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/walkthroughParser.ts
new file mode 100644
index 00000000000..2ac7f1fb698
--- /dev/null
+++ b/packages/desktop/src/renderer/pages/conversation/Messages/components/WalkthroughCard/walkthroughParser.ts
@@ -0,0 +1,239 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { WalkthroughData, WalkthroughSection, WalkthroughSectionType } from './types';
+
+const EXPLICIT_TAG_REGEX = /\[WALKTHROUGH\]([\s\S]*?)(?:\[\/WALKTHROUGH\]|$)/i;
+
+const IMPLICIT_HEADER_REGEX = /(?:^|\n)(#{1,2}\s+(?:Walkthrough|Resumo da Execução|Execution Walkthrough)[\s\S]*)$/i;
+
+/**
+ * Classify a section heading into one of the known semantic types
+ * based on multilingual keywords (English, Portuguese, Chinese, Spanish, etc.).
+ */
+export function classifySectionType(heading: string): WalkthroughSectionType {
+ const normalized = heading
+ .toLowerCase()
+ .replace(/^[\d.)\s\-#]+/, '')
+ .trim();
+
+ // 1. Delivered / Changes / What was done
+ if (
+ /\bdelivered\b|\bentregue\b|\bentregas?\b|\balterações\b|\balteracoes\b|\bchanges?\b|\bmodifi|\bwhat was done\b|\bo que foi feito\b|\bwhat changed\b|交付|纳品|сделано/i.test(
+ normalized
+ )
+ ) {
+ return 'delivered';
+ }
+
+ // 2. Notes / Attention / Watch out / Tips / Caveats (checked before howItWorks so "Dicas Técnicas" matches notes)
+ if (
+ /\bnotes?\b|\batenção\b|\batencao\b|\bwatch out\b|\btips?\b|\bdicas?\b|\bcaveats?\b|\bwarnings?\b|\balertas?\b|\bcuidados?\b|\bobservações\b|\bobservacoes\b|\bprestar atenção\b|\bprestar atencao\b|\bimportante\b|\bimportant\b|注意|注意事項|внимание/i.test(
+ normalized
+ )
+ ) {
+ return 'notes';
+ }
+
+ // 3. How it works / Architecture / Mechanics / Under the hood
+ if (
+ /\bhow it works?\b|\bcomo funciona\b|\barchitect|\barquitetura\b|\btechnical\b|\bdetalhes técnicos\b|\bdetalhes tecnicos\b|\bfuncionamento\b|\bmecanismo\b|\bunder the hood\b|\bpor baixo dos panos\b|原理|运作/i.test(
+ normalized
+ )
+ ) {
+ return 'howItWorks';
+ }
+
+ // 4. How to use / Usage / Testing / Verification / Steps
+ if (
+ /\bhow to use\b|\bcomo usar\b|\busage\b|\buso\b|\bhow to test\b|\btesting\b|\bcomo testar\b|\bverification\b|\bverificação\b|\bverificacao\b|\bpasso a passo\b|\bstep[- ]by[- ]step\b|\bquick start\b|\bgetting started\b|使用|検証|инструк/i.test(
+ normalized
+ )
+ ) {
+ return 'usage';
+ }
+
+ return 'custom';
+}
+
+/**
+ * Parse a raw markdown block into structured WalkthroughData.
+ */
+function parseWalkthroughMarkdown(rawContent: string): WalkthroughData | null {
+ const trimmed = rawContent.trim();
+ if (!trimmed) return null;
+
+ const lines = trimmed.split('\n');
+ let title = '';
+ let contentStartIndex = 0;
+
+ // Check if first line is a main header `# Title`
+ const firstLine = lines[0]?.trim() ?? '';
+ const titleMatch = firstLine.match(/^#\s+(.+)$/);
+ if (titleMatch) {
+ title = titleMatch[1].trim();
+ // Clean "Walkthrough:" prefix if present
+ title = title.replace(/^walkthrough\s*[:\-–—]\s*/i, '').trim();
+ contentStartIndex = 1;
+ }
+
+ const remainingText = lines.slice(contentStartIndex).join('\n').trim();
+
+ // Find all section headers (## or ###)
+ const sectionHeadingRegex = /^(#{2,3})\s+(.+)$/gm;
+ const headings: Array<{ index: number; heading: string }> = [];
+ let match: RegExpExecArray | null;
+
+ while ((match = sectionHeadingRegex.exec(remainingText)) !== null) {
+ headings.push({
+ index: match.index,
+ heading: match[2].trim(),
+ });
+ }
+
+ let summary: string | undefined;
+ const sections: WalkthroughSection[] = [];
+
+ if (headings.length === 0) {
+ // No subheadings — treating remaining text as content
+ summary = remainingText || undefined;
+ if (!summary) return null;
+ sections.push({
+ id: 'section-overview',
+ type: 'custom',
+ title: title || 'Overview',
+ content: summary,
+ });
+ } else {
+ // Summary is everything before the first heading
+ const firstHeadingIndex = headings[0].index;
+ if (firstHeadingIndex > 0) {
+ const potentialSummary = remainingText
+ .slice(0, firstHeadingIndex)
+ .replace(/^---\s*$/gm, '')
+ .trim();
+ if (potentialSummary) {
+ summary = potentialSummary;
+ }
+ }
+
+ // Extract each section
+ for (let i = 0; i < headings.length; i++) {
+ const current = headings[i];
+ const nextIndex = i + 1 < headings.length ? headings[i + 1].index : remainingText.length;
+ const sectionFullText = remainingText.slice(current.index, nextIndex);
+
+ // Remove the heading line itself to get the section content
+ const firstNewline = sectionFullText.indexOf('\n');
+ const sectionContent =
+ firstNewline !== -1
+ ? sectionFullText
+ .slice(firstNewline + 1)
+ .replace(/---+\s*$/m, '')
+ .trim()
+ : '';
+
+ const sectionType = classifySectionType(current.heading);
+
+ sections.push({
+ id: `section-${i}-${sectionType}`,
+ type: sectionType,
+ title: current.heading,
+ content: sectionContent,
+ });
+ }
+ }
+
+ return {
+ title,
+ summary,
+ sections,
+ rawContent: trimmed,
+ };
+}
+
+/**
+ * Check if the text contains a walkthrough block (either explicit tags or implicit markdown header).
+ */
+export function hasWalkthrough(text: string): boolean {
+ if (!text || typeof text !== 'string') return false;
+ if (EXPLICIT_TAG_REGEX.test(text)) return true;
+
+ const match = text.match(IMPLICIT_HEADER_REGEX);
+ if (!match) return false;
+
+ // Validate that implicit header actually has at least 2 structured subsections
+ const data = parseWalkthroughMarkdown(match[1]);
+ if (!data || data.sections.length < 2) return false;
+
+ const recognizedTypes = data.sections.filter((s) => s.type !== 'custom');
+ return recognizedTypes.length >= 2;
+}
+
+/**
+ * Parse walkthrough from message content.
+ * Supports explicit [WALKTHROUGH]...[/WALKTHROUGH] and standard markdown walkthrough headings.
+ */
+export function parseWalkthrough(text: string): WalkthroughData | null {
+ if (!text || typeof text !== 'string') return null;
+
+ // 1. Try explicit tag match first
+ const explicitMatch = text.match(EXPLICIT_TAG_REGEX);
+ if (explicitMatch && explicitMatch[1]?.trim()) {
+ return parseWalkthroughMarkdown(explicitMatch[1].trim());
+ }
+
+ // 2. Fall back to implicit markdown section
+ const implicitMatch = text.match(IMPLICIT_HEADER_REGEX);
+ if (implicitMatch && implicitMatch[1]?.trim()) {
+ const data = parseWalkthroughMarkdown(implicitMatch[1].trim());
+ if (data && data.sections.length >= 2) {
+ const recognized = data.sections.filter((s) => s.type !== 'custom');
+ if (recognized.length >= 2) {
+ return data;
+ }
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Strip walkthrough blocks from text so the regular message bubble renders cleanly without it.
+ */
+export function stripWalkthrough(text: string): string {
+ if (!text || typeof text !== 'string') return text;
+
+ // 1. If explicit tags exist, strip them
+ if (EXPLICIT_TAG_REGEX.test(text)) {
+ return text
+ .replace(EXPLICIT_TAG_REGEX, '')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim();
+ }
+
+ // 2. If valid implicit walkthrough exists, strip it
+ const implicitMatch = text.match(IMPLICIT_HEADER_REGEX);
+ if (implicitMatch && hasWalkthrough(text)) {
+ return text
+ .slice(0, implicitMatch.index)
+ .replace(/\n{3,}/g, '\n\n')
+ .trim();
+ }
+
+ return text;
+}
+
+/**
+ * Strip explicit [WALKTHROUGH] tags for clipboard copying so it becomes clean readable markdown.
+ */
+export function cleanWalkthroughForClipboard(text: string): string {
+ if (!text || typeof text !== 'string') return text;
+ return text
+ .replace(/\[\/?WALKTHROUGH\]/gi, '')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim();
+}
diff --git a/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts b/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts
index 677ae71324f..8f8072f94f3 100644
--- a/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts
+++ b/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts
@@ -1262,6 +1262,16 @@ export type I18nKey =
| 'messages.slash.hint'
| 'messages.slash.title'
| 'messages.unknownMessageType'
+ | 'messages.walkthrough.badge'
+ | 'messages.walkthrough.collapse'
+ | 'messages.walkthrough.copy'
+ | 'messages.walkthrough.copyFailed'
+ | 'messages.walkthrough.copySuccess'
+ | 'messages.walkthrough.expand'
+ | 'messages.walkthrough.sectionsCount'
+ | 'messages.walkthrough.sectionsCount_one'
+ | 'messages.walkthrough.sectionsCount_other'
+ | 'messages.walkthrough.title'
| 'pet.confirmBubble'
| 'pet.confirmBubbleDescription'
| 'pet.desktopOnly'
diff --git a/packages/desktop/src/renderer/services/i18n/locales/de-DE/messages.json b/packages/desktop/src/renderer/services/i18n/locales/de-DE/messages.json
index 1cf8b4ca9d6..ccd6d63d248 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/de-DE/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/de-DE/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "Anfragedetails:",
"delivery": {
"pending": "Ungelesen"
+ },
+ "walkthrough": {
+ "title": "Ausführungs-Walkthrough",
+ "badge": "Walkthrough",
+ "copy": "Walkthrough kopieren",
+ "copySuccess": "Walkthrough in Zwischenablage kopiert",
+ "copyFailed": "Fehler beim Kopieren des Walkthroughs",
+ "expand": "Erweitern",
+ "collapse": "Einklappen",
+ "sectionsCount": "{{count}} Abschnitte",
+ "sectionsCount_one": "{{count}} Abschnitt",
+ "sectionsCount_other": "{{count}} Abschnitte"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/en-US/messages.json b/packages/desktop/src/renderer/services/i18n/locales/en-US/messages.json
index 0de6c7dd419..44ee19abcec 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/en-US/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/en-US/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "Request details:",
"delivery": {
"pending": "Unread"
+ },
+ "walkthrough": {
+ "title": "Execution Walkthrough",
+ "badge": "Walkthrough",
+ "copy": "Copy Walkthrough",
+ "copySuccess": "Walkthrough copied to clipboard",
+ "copyFailed": "Failed to copy walkthrough",
+ "expand": "Expand",
+ "collapse": "Collapse",
+ "sectionsCount": "{{count}} sections",
+ "sectionsCount_one": "{{count}} section",
+ "sectionsCount_other": "{{count}} sections"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/es-ES/messages.json b/packages/desktop/src/renderer/services/i18n/locales/es-ES/messages.json
index 178a03a081b..7475da81f3b 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/es-ES/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/es-ES/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "Detalles de la solicitud:",
"delivery": {
"pending": "No leído"
+ },
+ "walkthrough": {
+ "title": "Resumen de Ejecución (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "Copiar resumen",
+ "copySuccess": "Resumen copiado al portapapeles",
+ "copyFailed": "Error al copiar resumen",
+ "expand": "Expandir",
+ "collapse": "Contraer",
+ "sectionsCount": "{{count}} secciones",
+ "sectionsCount_one": "{{count}} sección",
+ "sectionsCount_other": "{{count}} secciones"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/fa-IR/messages.json b/packages/desktop/src/renderer/services/i18n/locales/fa-IR/messages.json
index a430562ab26..1e8590a525b 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/fa-IR/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/fa-IR/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "جزئیات درخواست:",
"delivery": {
"pending": "خواندهنشده"
+ },
+ "walkthrough": {
+ "title": "راهنمای گامبهگام اجرا (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "کپی خلاصه",
+ "copySuccess": "خلاصه در کلیپبورد کپی شد",
+ "copyFailed": "کپی خلاصه انجام نشد",
+ "expand": "گسترش",
+ "collapse": "جمع کردن",
+ "sectionsCount": "{{count}} بخش",
+ "sectionsCount_one": "{{count}} بخش",
+ "sectionsCount_other": "{{count}} بخش"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/fr-FR/messages.json b/packages/desktop/src/renderer/services/i18n/locales/fr-FR/messages.json
index 576c4ecfd5b..cbb47ae2a2c 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/fr-FR/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/fr-FR/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "Détails de la demande :",
"delivery": {
"pending": "Non lu"
+ },
+ "walkthrough": {
+ "title": "Récapitulatif d'exécution (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "Copier le récapitulatif",
+ "copySuccess": "Récapitulatif copié dans le presse-papiers",
+ "copyFailed": "Échec de la copie du récapitulatif",
+ "expand": "Développer",
+ "collapse": "Réduire",
+ "sectionsCount": "{{count}} sections",
+ "sectionsCount_one": "{{count}} section",
+ "sectionsCount_other": "{{count}} sections"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/ja-JP/messages.json b/packages/desktop/src/renderer/services/i18n/locales/ja-JP/messages.json
index f13a1646017..11dfdeaf521 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/ja-JP/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/ja-JP/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "リクエスト内容:",
"delivery": {
"pending": "未読"
+ },
+ "walkthrough": {
+ "title": "実行ウォークスルー",
+ "badge": "Walkthrough",
+ "copy": "ウォークスルーをコピー",
+ "copySuccess": "クリップボードにコピーしました",
+ "copyFailed": "コピーに失敗しました",
+ "expand": "展開",
+ "collapse": "折りたたむ",
+ "sectionsCount": "{{count}} 件のセクション",
+ "sectionsCount_one": "{{count}} 件のセクション",
+ "sectionsCount_other": "{{count}} 件のセクション"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/ko-KR/messages.json b/packages/desktop/src/renderer/services/i18n/locales/ko-KR/messages.json
index 9963844e2c0..c78f0725c46 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/ko-KR/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/ko-KR/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "요청 내용:",
"delivery": {
"pending": "읽지 않음"
+ },
+ "walkthrough": {
+ "title": "실행 워크스루",
+ "badge": "Walkthrough",
+ "copy": "워크스루 복사",
+ "copySuccess": "클립보드에 복사되었습니다",
+ "copyFailed": "복사 실패",
+ "expand": "펼치기",
+ "collapse": "접기",
+ "sectionsCount": "{{count}}개 섹션",
+ "sectionsCount_one": "{{count}}개 섹션",
+ "sectionsCount_other": "{{count}}개 섹션"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/pt-BR/messages.json b/packages/desktop/src/renderer/services/i18n/locales/pt-BR/messages.json
index fdb8f7b254a..c5400ce7022 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/pt-BR/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/pt-BR/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "Detalhes da solicitação:",
"delivery": {
"pending": "Não lido"
+ },
+ "walkthrough": {
+ "title": "Walkthrough da Execução",
+ "badge": "Walkthrough",
+ "copy": "Copiar Walkthrough",
+ "copySuccess": "Walkthrough copiado para a área de transferência",
+ "copyFailed": "Falha ao copiar walkthrough",
+ "expand": "Expandir",
+ "collapse": "Recolher",
+ "sectionsCount": "{{count}} seções",
+ "sectionsCount_one": "{{count}} seção",
+ "sectionsCount_other": "{{count}} seções"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/ru-RU/messages.json b/packages/desktop/src/renderer/services/i18n/locales/ru-RU/messages.json
index 563002681af..9f8a0ef55e3 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/ru-RU/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/ru-RU/messages.json
@@ -101,5 +101,17 @@
"requestDetails": "Детали запроса:",
"delivery": {
"pending": "Непрочитано"
+ },
+ "walkthrough": {
+ "title": "Обзор выполнения (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "Скопировать обзор",
+ "copySuccess": "Обзор скопирован в буфер обмена",
+ "copyFailed": "Не удалось скопировать обзор",
+ "expand": "Развернуть",
+ "collapse": "Свернуть",
+ "sectionsCount": "Разделов: {{count}}",
+ "sectionsCount_one": "{{count}} раздел",
+ "sectionsCount_other": "Разделов: {{count}}"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/tr-TR/messages.json b/packages/desktop/src/renderer/services/i18n/locales/tr-TR/messages.json
index a42b137f139..caca4fb202e 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/tr-TR/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/tr-TR/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "İstek ayrıntıları:",
"delivery": {
"pending": "Okunmadı"
+ },
+ "walkthrough": {
+ "title": "Yürütme Özeti (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "Özeti Kopyala",
+ "copySuccess": "Özet panoya kopyalandı",
+ "copyFailed": "Özet kopyalanamadı",
+ "expand": "Genişlet",
+ "collapse": "Daralt",
+ "sectionsCount": "{{count}} bölüm",
+ "sectionsCount_one": "{{count}} bölüm",
+ "sectionsCount_other": "{{count}} bölüm"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/uk-UA/messages.json b/packages/desktop/src/renderer/services/i18n/locales/uk-UA/messages.json
index e318c881db1..34d3e015ebc 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/uk-UA/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/uk-UA/messages.json
@@ -103,5 +103,17 @@
"requestDetails": "Деталі запиту:",
"delivery": {
"pending": "Непрочитано"
+ },
+ "walkthrough": {
+ "title": "Огляд виконання (Walkthrough)",
+ "badge": "Walkthrough",
+ "copy": "Скопіювати огляд",
+ "copySuccess": "Огляд скопійовано в буфер обміну",
+ "copyFailed": "Не вдалося скопіювати огляд",
+ "expand": "Розгорнути",
+ "collapse": "Згорнути",
+ "sectionsCount": "Розділів: {{count}}",
+ "sectionsCount_one": "{{count}} розділ",
+ "sectionsCount_other": "Розділів: {{count}}"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/zh-CN/messages.json b/packages/desktop/src/renderer/services/i18n/locales/zh-CN/messages.json
index c32cfc7c0b8..a9b76c58eaf 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/zh-CN/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/zh-CN/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "请求内容:",
"delivery": {
"pending": "未读"
+ },
+ "walkthrough": {
+ "title": "任务执行总结",
+ "badge": "Walkthrough",
+ "copy": "复制总结",
+ "copySuccess": "已复制总结到剪贴板",
+ "copyFailed": "复制总结失败",
+ "expand": "展开",
+ "collapse": "折叠",
+ "sectionsCount": "{{count}} 个模块",
+ "sectionsCount_one": "{{count}} 个模块",
+ "sectionsCount_other": "{{count}} 个模块"
}
}
diff --git a/packages/desktop/src/renderer/services/i18n/locales/zh-TW/messages.json b/packages/desktop/src/renderer/services/i18n/locales/zh-TW/messages.json
index f6fdc649884..c3654a56a6b 100644
--- a/packages/desktop/src/renderer/services/i18n/locales/zh-TW/messages.json
+++ b/packages/desktop/src/renderer/services/i18n/locales/zh-TW/messages.json
@@ -97,5 +97,17 @@
"requestDetails": "請求內容:",
"delivery": {
"pending": "未讀"
+ },
+ "walkthrough": {
+ "title": "任務執行總結",
+ "badge": "Walkthrough",
+ "copy": "複製總結",
+ "copySuccess": "已複製總結至剪貼簿",
+ "copyFailed": "複製總結失敗",
+ "expand": "展開",
+ "collapse": "摺疊",
+ "sectionsCount": "{{count}} 個模組",
+ "sectionsCount_one": "{{count}} 個模組",
+ "sectionsCount_other": "{{count}} 個模組"
}
}
diff --git a/packages/desktop/src/renderer/utils/chat/turnCopy.ts b/packages/desktop/src/renderer/utils/chat/turnCopy.ts
index a68f42c7821..f9ea4c7c19a 100644
--- a/packages/desktop/src/renderer/utils/chat/turnCopy.ts
+++ b/packages/desktop/src/renderer/utils/chat/turnCopy.ts
@@ -6,6 +6,10 @@
import { hasThinkTags, stripThinkTags } from './thinkTagFilter';
import { hasSkillSuggest, stripSkillSuggest } from './skillSuggestParser';
+import {
+ hasWalkthrough,
+ cleanWalkthroughForClipboard,
+} from '@renderer/pages/conversation/Messages/components/WalkthroughCard/walkthroughParser';
/**
* Turn-level copy support. An AI reply can be split into several stored text
@@ -98,6 +102,9 @@ export function buildTurnClipboardText(segments: string[]): string {
if (hasSkillSuggest(cleaned)) {
cleaned = stripSkillSuggest(cleaned);
}
+ if (hasWalkthrough(cleaned)) {
+ cleaned = cleanWalkthroughForClipboard(cleaned);
+ }
return cleaned.trim();
})
.filter(Boolean)
diff --git a/tests/unit/chat/turnCopy.test.ts b/tests/unit/chat/turnCopy.test.ts
index aaf1155d597..2354e50b2bc 100644
--- a/tests/unit/chat/turnCopy.test.ts
+++ b/tests/unit/chat/turnCopy.test.ts
@@ -83,6 +83,18 @@ describe('buildTurnClipboardText', () => {
expect(joined).toContain('tail');
});
+ it('cleans walkthrough tags while keeping markdown content in turn copy', () => {
+ const joined = buildTurnClipboardText([
+ 'Done!',
+ '[WALKTHROUGH]\n# Walkthrough: Feature\n## Delivered\n- Item 1\n[/WALKTHROUGH]',
+ ]);
+ expect(joined).not.toContain('[WALKTHROUGH]');
+ expect(joined).not.toContain('[/WALKTHROUGH]');
+ expect(joined).toContain('Done!');
+ expect(joined).toContain('# Walkthrough: Feature');
+ expect(joined).toContain('## Delivered');
+ });
+
it('drops segments that clean down to nothing', () => {
expect(buildTurnClipboardText(['only draft ', 'kept'])).toBe('kept');
});
diff --git a/tests/unit/renderer/conversation/walkthrough/MessageTextWalkthrough.dom.test.tsx b/tests/unit/renderer/conversation/walkthrough/MessageTextWalkthrough.dom.test.tsx
new file mode 100644
index 00000000000..7e30726a2b3
--- /dev/null
+++ b/tests/unit/renderer/conversation/walkthrough/MessageTextWalkthrough.dom.test.tsx
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, render, screen } from '@testing-library/react';
+import React from 'react';
+import { Message } from '@arco-design/web-react';
+import type { IMessageText } from '@/common/chat/chatLib';
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (k: string, opts?: { defaultValue?: string }) => opts?.defaultValue || k,
+ i18n: { language: 'en' },
+ }),
+}));
+
+vi.mock('@renderer/components/Markdown', () => ({
+ default: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('@/renderer/utils/ui/clipboard', () => ({
+ copyText: vi.fn().mockResolvedValue(undefined),
+}));
+
+vi.mock('@/renderer/hooks/context/ConversationContext', () => ({
+ useConversationContextSafe: () => ({
+ conversation_id: 'conv-123',
+ workspace: '/test/workspace',
+ }),
+}));
+
+vi.mock('@/renderer/hooks/context/LayoutContext', () => ({
+ useLayoutContext: () => ({
+ isMobile: false,
+ }),
+}));
+
+vi.mock('@/renderer/pages/conversation/Preview/hooks/useLocalFilePreview', () => ({
+ useLocalFilePreview: () => vi.fn(),
+}));
+
+vi.mock('@/renderer/hooks/chat/useForkConversation', () => ({
+ useForkConversation: () => vi.fn(),
+}));
+
+import MessageText from '@/renderer/pages/conversation/Messages/components/MessageText';
+
+describe('MessageText Walkthrough integration', () => {
+ beforeEach(() => {
+ vi.spyOn(Message, 'info').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'warning').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'error').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'success').mockImplementation(() => '' as never);
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ });
+
+ it('renders both text bubble and WalkthroughCard when message has text and walkthrough', () => {
+ const msg: IMessageText = {
+ id: 'msg-1',
+ msg_id: 'msg-1',
+ type: 'text',
+ position: 'left',
+ conversation_id: 'conv-123',
+ created_at: Date.now(),
+ content: {
+ content: `Tudo pronto! Aqui estão as alterações:
+
+[WALKTHROUGH]
+# Walkthrough: Sistema de Ditado
+## 1. O que foi Entregue
+- Modelo de IA Parakeet
+## 2. Como Funciona
+- Inferência local
+[/WALKTHROUGH]`,
+ },
+ };
+
+ render( );
+
+ expect(screen.getByTestId('message-text-content')).toBeInTheDocument();
+ expect(screen.getByText('Tudo pronto! Aqui estão as alterações:')).toBeInTheDocument();
+ expect(screen.getByTestId('walkthrough-card')).toBeInTheDocument();
+ expect(screen.getByText('Sistema de Ditado')).toBeInTheDocument();
+ });
+
+ it('renders only WalkthroughCard without text bubble when message is purely walkthrough', () => {
+ const msg: IMessageText = {
+ id: 'msg-2',
+ msg_id: 'msg-2',
+ type: 'text',
+ position: 'left',
+ conversation_id: 'conv-123',
+ created_at: Date.now(),
+ content: {
+ content: `[WALKTHROUGH]
+# Walkthrough: Pure Summary
+## Delivered Changes
+- Fixed bug
+## Verification
+- Run tests
+[/WALKTHROUGH]`,
+ },
+ };
+
+ render( );
+
+ expect(screen.queryByTestId('message-text-content')).not.toBeInTheDocument();
+ expect(screen.getByTestId('walkthrough-card')).toBeInTheDocument();
+ expect(screen.getByText('Pure Summary')).toBeInTheDocument();
+ });
+
+ it('renders only text bubble when message does not contain a walkthrough', () => {
+ const msg: IMessageText = {
+ id: 'msg-3',
+ msg_id: 'msg-3',
+ type: 'text',
+ position: 'left',
+ conversation_id: 'conv-123',
+ created_at: Date.now(),
+ content: {
+ content: 'Just a normal assistant response.',
+ },
+ };
+
+ render( );
+
+ expect(screen.getByTestId('message-text-content')).toBeInTheDocument();
+ expect(screen.getByText('Just a normal assistant response.')).toBeInTheDocument();
+ expect(screen.queryByTestId('walkthrough-card')).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/renderer/conversation/walkthrough/WalkthroughCard.dom.test.tsx b/tests/unit/renderer/conversation/walkthrough/WalkthroughCard.dom.test.tsx
new file mode 100644
index 00000000000..a39d99d2c73
--- /dev/null
+++ b/tests/unit/renderer/conversation/walkthrough/WalkthroughCard.dom.test.tsx
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { Message } from '@arco-design/web-react';
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (k: string, opts?: { defaultValue?: string }) => opts?.defaultValue || k,
+ i18n: { language: 'en' },
+ }),
+}));
+
+vi.mock('@renderer/components/Markdown', () => ({
+ default: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('@/renderer/utils/ui/clipboard', () => ({
+ copyText: vi.fn().mockResolvedValue(undefined),
+}));
+
+import WalkthroughCard from '@/renderer/pages/conversation/Messages/components/WalkthroughCard';
+import type { WalkthroughData } from '@/renderer/pages/conversation/Messages/components/WalkthroughCard/types';
+import { copyText } from '@/renderer/utils/ui/clipboard';
+
+describe('WalkthroughCard', () => {
+ beforeEach(() => {
+ vi.spyOn(Message, 'info').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'warning').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'error').mockImplementation(() => '' as never);
+ vi.spyOn(Message, 'success').mockImplementation(() => '' as never);
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ });
+
+ const sampleData: WalkthroughData = {
+ title: 'Parakeet TDT Speech-to-Text',
+ summary: 'Guia da nova funcionalidade de voz local.',
+ rawContent: '# Raw markdown content',
+ sections: [
+ {
+ id: 'sec-1',
+ type: 'delivered',
+ title: '1. O que foi Entregue',
+ content: 'Modelo Parakeet TDT integrado.',
+ },
+ {
+ id: 'sec-2',
+ type: 'howItWorks',
+ title: '2. Como Funciona',
+ content: 'Decodificação PCM via IPC.',
+ },
+ {
+ id: 'sec-3',
+ type: 'usage',
+ title: '3. Como Usar',
+ content: 'Abra as configurações e selecione o modelo.',
+ },
+ {
+ id: 'sec-4',
+ type: 'notes',
+ title: '4. O que Prestar Atenção',
+ content: 'O modelo roda 100% offline.',
+ },
+ ],
+ };
+
+ it('renders the walkthrough card with title, badge and sections', () => {
+ render( );
+
+ expect(screen.getByTestId('walkthrough-card')).toBeInTheDocument();
+ expect(screen.getByText('Parakeet TDT Speech-to-Text')).toBeInTheDocument();
+ expect(screen.getByText('Walkthrough')).toBeInTheDocument();
+ expect(screen.getByText('Guia da nova funcionalidade de voz local.')).toBeInTheDocument();
+
+ expect(screen.getByTestId('walkthrough-section-delivered')).toBeInTheDocument();
+ expect(screen.getByTestId('walkthrough-section-howItWorks')).toBeInTheDocument();
+ expect(screen.getByTestId('walkthrough-section-usage')).toBeInTheDocument();
+ expect(screen.getByTestId('walkthrough-section-notes')).toBeInTheDocument();
+ });
+
+ it('copies raw content when copy button is clicked', async () => {
+ render( );
+
+ const copyBtn = screen.getByLabelText('Copy Walkthrough');
+ await act(async () => {
+ fireEvent.click(copyBtn);
+ });
+
+ expect(copyText).toHaveBeenCalledWith('# Raw markdown content');
+ expect(Message.success).toHaveBeenCalledWith('Walkthrough copied to clipboard');
+ });
+
+ it('collapses and expands the entire card', () => {
+ render( );
+
+ const collapseBtn = screen.getByLabelText('Collapse');
+ fireEvent.click(collapseBtn);
+
+ // Body sections should no longer be visible
+ expect(screen.queryByTestId('walkthrough-section-delivered')).not.toBeInTheDocument();
+
+ // Click to expand again
+ const expandBtn = screen.getByLabelText('Expand');
+ fireEvent.click(expandBtn);
+
+ expect(screen.getByTestId('walkthrough-section-delivered')).toBeInTheDocument();
+ });
+
+ it('toggles collapse on individual sections', () => {
+ render( );
+
+ const sectionTitle = screen.getByText('1. O que foi Entregue');
+ expect(screen.getByText('Modelo Parakeet TDT integrado.')).toBeInTheDocument();
+
+ // Click section header to collapse it
+ fireEvent.click(sectionTitle);
+ expect(screen.queryByText('Modelo Parakeet TDT integrado.')).not.toBeInTheDocument();
+
+ // Click section header to expand it again
+ fireEvent.click(sectionTitle);
+ expect(screen.getByText('Modelo Parakeet TDT integrado.')).toBeInTheDocument();
+ });
+
+ it('renders custom section icon for unrecognized section types', () => {
+ const dataWithCustom: WalkthroughData = {
+ title: 'Custom Section Walkthrough',
+ rawContent: 'test',
+ sections: [
+ {
+ id: 'sec-custom',
+ type: 'custom',
+ title: '5. Pull Request Status',
+ content: 'PR #4247 is open.',
+ },
+ ],
+ };
+
+ render( );
+ expect(screen.getByTestId('walkthrough-section-custom')).toBeInTheDocument();
+ expect(screen.getByText('5. Pull Request Status')).toBeInTheDocument();
+ });
+
+ it('collapses and expands by clicking the header container directly', () => {
+ render( );
+
+ const header = screen.getByRole('button', { name: /Parakeet TDT Speech-to-Text/i });
+ fireEvent.click(header);
+
+ // Body should be hidden
+ expect(screen.queryByTestId('walkthrough-section-delivered')).not.toBeInTheDocument();
+
+ // Click again to expand
+ fireEvent.click(header);
+ expect(screen.getByTestId('walkthrough-section-delivered')).toBeInTheDocument();
+ });
+
+ it('handles copy error gracefully and shows error message', async () => {
+ vi.mocked(copyText).mockRejectedValueOnce(new Error('Clipboard denied'));
+
+ render( );
+
+ const copyBtn = screen.getByLabelText('Copy Walkthrough');
+ await act(async () => {
+ fireEvent.click(copyBtn);
+ });
+
+ expect(Message.error).toHaveBeenCalledWith('Failed to copy walkthrough');
+ });
+});
diff --git a/tests/unit/renderer/conversation/walkthrough/walkthroughParser.test.ts b/tests/unit/renderer/conversation/walkthrough/walkthroughParser.test.ts
new file mode 100644
index 00000000000..6cb1cd5db16
--- /dev/null
+++ b/tests/unit/renderer/conversation/walkthrough/walkthroughParser.test.ts
@@ -0,0 +1,201 @@
+/**
+ * @license
+ * Copyright 2026 AionUi (aionui.com)
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ parseWalkthrough,
+ stripWalkthrough,
+ hasWalkthrough,
+ classifySectionType,
+ cleanWalkthroughForClipboard,
+} from '@/renderer/pages/conversation/Messages/components/WalkthroughCard/walkthroughParser';
+
+describe('walkthroughParser', () => {
+ const sampleExplicitWalkthrough = `Here is the summary of the work done:
+
+[WALKTHROUGH]
+# Walkthrough: Ditado Local com Parakeet TDT v3 no AionUi
+
+Guia completo e organizado sobre a nova funcionalidade de **Ditado Local**.
+
+## 1. O que foi Entregue
+- Modelo de IA Parakeet TDT v3 quantizado INT8
+- Integração no Chat com gravação de áudio
+
+## 2. Como Funciona por Baixo dos Panos
+Decodificação de áudio em PCM 16kHz mono enviada via IPC.
+
+## 3. Como Usar no Dia a Dia
+1. Abra as Configurações
+2. Selecione Local (Offline - Parakeet TDT)
+
+## 4. O que Prestar Atenção & Dicas Técnicas
+> [!TIP]
+> 100% offline e privado.
+[/WALKTHROUGH]`;
+
+ describe('classifySectionType', () => {
+ it('classifies delivered sections across languages', () => {
+ expect(classifySectionType('1. O que foi Entregue')).toBe('delivered');
+ expect(classifySectionType('What was delivered')).toBe('delivered');
+ expect(classifySectionType('Changes Made')).toBe('delivered');
+ expect(classifySectionType('交付内容')).toBe('delivered');
+ });
+
+ it('classifies how it works sections across languages', () => {
+ expect(classifySectionType('2. Como Funciona por Baixo dos Panos')).toBe('howItWorks');
+ expect(classifySectionType('How It Works')).toBe('howItWorks');
+ expect(classifySectionType('Architecture & Mechanics')).toBe('howItWorks');
+ expect(classifySectionType('实现原理')).toBe('howItWorks');
+ });
+
+ it('classifies usage and testing sections across languages', () => {
+ expect(classifySectionType('3. Como Usar no Dia a Dia')).toBe('usage');
+ expect(classifySectionType('How to Test')).toBe('usage');
+ expect(classifySectionType('Verification Steps')).toBe('usage');
+ expect(classifySectionType('使用与验证')).toBe('usage');
+ });
+
+ it('classifies notes, caveats and tips sections across languages', () => {
+ expect(classifySectionType('4. O que Prestar Atenção & Dicas Técnicas')).toBe('notes');
+ expect(classifySectionType('Notes and Caveats')).toBe('notes');
+ expect(classifySectionType('Important Tips & Warnings')).toBe('notes');
+ expect(classifySectionType('注意事项')).toBe('notes');
+ });
+
+ it('falls back to custom for unrecognized titles', () => {
+ expect(classifySectionType('5. Next Steps and PR Status')).toBe('custom');
+ });
+ });
+
+ describe('hasWalkthrough', () => {
+ it('returns true when explicit tag is present', () => {
+ expect(hasWalkthrough(sampleExplicitWalkthrough)).toBe(true);
+ });
+
+ it('returns true for streaming/unclosed explicit tag', () => {
+ expect(hasWalkthrough('Working on it...\n\n[WALKTHROUGH]\n# Partial walkthrough')).toBe(true);
+ });
+
+ it('returns true for implicit markdown section with at least 2 structured subheadings', () => {
+ const markdown = `Tudo pronto!
+# Walkthrough: User Authentication
+## Delivered Changes
+- Auth service
+## How to Test
+- Run tests`;
+ expect(hasWalkthrough(markdown)).toBe(true);
+ });
+
+ it('returns false when no walkthrough exists', () => {
+ expect(hasWalkthrough('Just normal chat message')).toBe(false);
+ expect(hasWalkthrough('')).toBe(false);
+ });
+
+ it('returns false for casual mention of the word walkthrough without structure', () => {
+ expect(hasWalkthrough('Can you give me a walkthrough of how this code works?')).toBe(false);
+ });
+ });
+
+ describe('parseWalkthrough', () => {
+ it('parses explicit walkthrough block completely', () => {
+ const data = parseWalkthrough(sampleExplicitWalkthrough);
+ expect(data).not.toBeNull();
+ expect(data?.title).toBe('Ditado Local com Parakeet TDT v3 no AionUi');
+ expect(data?.summary).toContain('Guia completo e organizado');
+ expect(data?.sections).toHaveLength(4);
+
+ expect(data?.sections[0].type).toBe('delivered');
+ expect(data?.sections[0].title).toBe('1. O que foi Entregue');
+ expect(data?.sections[0].content).toContain('Parakeet TDT v3');
+
+ expect(data?.sections[1].type).toBe('howItWorks');
+ expect(data?.sections[1].content).toContain('PCM 16kHz mono');
+
+ expect(data?.sections[2].type).toBe('usage');
+ expect(data?.sections[2].content).toContain('Abra as Configurações');
+
+ expect(data?.sections[3].type).toBe('notes');
+ expect(data?.sections[3].content).toContain('100% offline e privado');
+ });
+
+ it('parses implicit markdown walkthrough', () => {
+ const implicit = `Task completed!
+
+# Walkthrough: Refactor State
+## Delivered Changes
+- Replaced Redux with Zustand
+## How It Works
+- Lightweight state store
+## Verification
+- Run bun run test`;
+
+ const data = parseWalkthrough(implicit);
+ expect(data).not.toBeNull();
+ expect(data?.title).toBe('Refactor State');
+ expect(data?.sections).toHaveLength(3);
+ expect(data?.sections[0].type).toBe('delivered');
+ expect(data?.sections[1].type).toBe('howItWorks');
+ expect(data?.sections[2].type).toBe('usage');
+ });
+
+ it('parses walkthrough without subheadings as single overview section', () => {
+ const plain = `[WALKTHROUGH]
+# Walkthrough: Simple Task
+Here is just a plain summary without any subsections.
+[/WALKTHROUGH]`;
+ const data = parseWalkthrough(plain);
+ expect(data).not.toBeNull();
+ expect(data?.title).toBe('Simple Task');
+ expect(data?.sections).toHaveLength(1);
+ expect(data?.sections[0].type).toBe('custom');
+ });
+
+ it('returns null for empty or non-walkthrough content', () => {
+ expect(parseWalkthrough('')).toBeNull();
+ expect(parseWalkthrough('Just an answer')).toBeNull();
+ });
+ });
+
+ describe('stripWalkthrough', () => {
+ it('strips explicit walkthrough from message text', () => {
+ const stripped = stripWalkthrough(sampleExplicitWalkthrough);
+ expect(stripped).toBe('Here is the summary of the work done:');
+ expect(stripped).not.toContain('[WALKTHROUGH]');
+ expect(stripped).not.toContain('Parakeet TDT v3');
+ });
+
+ it('strips implicit walkthrough section from text', () => {
+ const implicit = `Hello user!
+Here are your results.
+
+# Walkthrough: Feature X
+## Delivered
+- Done
+## How to Test
+- Test it`;
+
+ const stripped = stripWalkthrough(implicit);
+ expect(stripped).toBe('Hello user!\nHere are your results.');
+ expect(stripped).not.toContain('Walkthrough: Feature X');
+ });
+
+ it('leaves normal text unchanged when no walkthrough is present', () => {
+ const text = 'Normal text without any walkthrough.';
+ expect(stripWalkthrough(text)).toBe(text);
+ });
+ });
+
+ describe('cleanWalkthroughForClipboard', () => {
+ it('removes tags while preserving markdown content', () => {
+ const cleaned = cleanWalkthroughForClipboard(sampleExplicitWalkthrough);
+ expect(cleaned).not.toContain('[WALKTHROUGH]');
+ expect(cleaned).not.toContain('[/WALKTHROUGH]');
+ expect(cleaned).toContain('# Walkthrough: Ditado Local');
+ expect(cleaned).toContain('## 1. O que foi Entregue');
+ });
+ });
+});