.claude/skills/webui-export-markdown/SKILL.md
This skill should be used when the user asks about "export markdown", "export chat", "download markdown", "export conversation", "导出 Markdown", "导出聊天记录", "ExportRequest", "ExportMessage", "handleVibeExportMarkdown", "buildExportMarkdown", "POST /api/vibe/export", "exportVibeSession", "chat export", "markdown download", "sanitizeFilename", "Content-Disposition attachment", "export button vibe", "history export", "tool_use in export", "message type mapping export", or needs to debug, extend, or understand the chat history Markdown export feature including the full-stack data flow from frontend trigger to file download.
npx skillsauth add liuyu520/cc-connect-fork webui-export-markdownInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Allow users to export Vibe Coding chat conversations as Markdown files. Supports two
entry points: the active tab (with rich tool_use info) and the history panel (text-only).
The backend generates the Markdown via a single POST /api/vibe/export endpoint.
Frontend trigger (Export button)
├─ Active Tab: collect tab.messages (includes tool_use, error, etc.)
└─ History Panel: fetch via getVibeMessages() → map to ExportMessage[]
↓
POST /api/vibe/export (JSON body with metadata + messages)
↓
Go handler: handleVibeExportMarkdown()
→ authenticate → parse body → buildExportMarkdown()
→ Content-Disposition: attachment → browser downloads .md file
| File | Role |
|------|------|
| core/webui.go | ExportRequest/ExportMessage structs, handleVibeExportMarkdown handler, buildExportMarkdown/renderMessageMarkdown formatter, sanitizeFilename |
| core/webui_export_test.go | Unit tests: sanitize, markdown build, empty/405/success handler tests |
| web/src/api/vibe.ts | ExportRequest/ExportMessage types, exportVibeSession() — POST + Blob download |
| web/src/pages/VibeCoding/VibeCoding.tsx | Export button in tab bar, handleExportCurrentTab(), chatToExportMessages() |
| web/src/pages/VibeCoding/VibeHistory.tsx | Per-session export button (hover), handleExportSession() |
| web/src/i18n/locales/*.json | vibe.exportMarkdown, vibe.exportFailed (5 locales) |
Path is
/api/vibe/export(NOT/api/vibe/sessions/export) to avoid routing conflict with the existing/api/vibe/sessions/prefix handler in Go'shttp.ServeMux.
{
"session_name": "astrBot_hw",
"project": "/Users/ywwl/.../astrBot_hw",
"agent_type": "claudecode",
"session_id": "abc123",
"messages": [
{"role": "user", "type": "text", "content": "Hello", "timestamp": 1711612800000},
{"role": "assistant", "type": "tool_use", "content": "Read file: main.go", "tool_name": "Read", "timestamp": 1711612801000},
{"role": "assistant", "type": "text", "content": "This file is...", "timestamp": 1711612802000}
]
}
type ExportRequest struct {
SessionName string `json:"session_name"`
Project string `json:"project"`
AgentType string `json:"agent_type"`
SessionID string `json:"session_id"`
Messages []ExportMessage `json:"messages"`
}
type ExportMessage struct {
Role string `json:"role"` // "user" or "assistant"
Type string `json:"type"` // "text", "tool_use", "result", "tool_result", "error"
Content string `json:"content"`
ToolName string `json:"tool_name,omitempty"` // only for type="tool_use"
Timestamp int64 `json:"timestamp"` // millisecond Unix epoch
}
Content-Type: text/markdown; charset=utf-8Content-Disposition: attachment; filename="name_20260328_143000.md"; filename*=UTF-8''...| Code | Condition | |------|-----------| | 400 | Invalid JSON, empty messages array, body > 20MB | | 401 | Authentication failed | | 405 | Non-POST method |
| Frontend type | Markdown Rendering |
|-----------------|-------------------|
| text | Direct content: ## User (HH:mm:ss) |
| tool_use | Blockquote + tool name: ## Assistant - Tool: Read (HH:mm:ss) |
| tool_result | Fenced code block: ``` |
| result | Treated as normal text (final response) |
| error | Warning blockquote: > Error: ... |
| thinking | Skipped |
| permission_request | Skipped |
| system | Skipped |
# Chat Export: astrBot_hw
| Field | Value |
|-------|-------|
| Project | /Users/ywwl/.../astrBot_hw |
| Agent | claudecode |
| Session ID | abc123 |
| Messages | 15 |
| Exported At | 2026-03-28 14:30:00 (CST) |
---
## User (14:00:00)
Hello
---
## Assistant - Tool: Read (14:00:01)
> Read file: main.go
---
## Assistant (14:00:02)
This file is...
func (s *WebUIServer) handleVibeExportMarkdown(w http.ResponseWriter, r *http.Request) {
// 1. authenticate (same as other vibe endpoints)
// 2. setCORS + OPTIONS handling
// 3. method check (POST only)
// 4. http.MaxBytesReader(w, r.Body, 20<<20) — 20MB limit
// 5. json.Decode → ExportRequest
// 6. validate: len(Messages) > 0
// 7. buildExportMarkdown() → Markdown string
// 8. sanitizeFilename() → safe filename
// 9. set Content-Type + Content-Disposition headers
// 10. w.Write(md)
}
Note: Follow existing handler pattern: authenticate first, then setCORS, matching
handleVibeSessions / handleVibePrompts code style.
tab.messages.length === 0<Loader2> spinner during exporttab.messages → filters out thinking/permission_request/system → POSTconst chatToExportMessages = (messages: ChatMessage[]): ExportMessage[] => {
return messages
.filter((m) => !['thinking', 'permission_request', 'system'].includes(m.type))
.map((m) => ({
role: m.role, type: m.type, content: m.content,
tool_name: m.toolName, timestamp: m.timestamp,
}));
};
session.message_count === 0getVibeMessages() first, then maps:// History records are text-only (no tool_use)
const messages = data.messages.map((msg) => ({
role: msg.role, type: 'text',
content: msg.content,
timestamp: new Date(msg.created_at).getTime(), // ISO → ms epoch
}));
export const exportVibeSession = async (data: ExportRequest): Promise<void> => {
const res = await fetch('/api/vibe/export', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(await res.text());
// Extract filename from Content-Disposition
const match = res.headers.get('Content-Disposition')?.match(/filename="([^"]+)"/);
const filename = match?.[1] || 'chat_export.md';
// Trigger browser download
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
var filenameRe = regexp.MustCompile(`[^a-zA-Z0-9_\-]`)
func sanitizeFilename(name string) string {
s := filenameRe.ReplaceAllString(name, "_")
s = strings.Trim(s, "_")
if s == "" { s = "export" }
return s
}
Pattern: {sanitized_project_name}_{YYYYMMDD}_{HHmmss}.md
| Key | EN | ZH | ZH-TW | JA | ES |
|-----|----|----|-------|----|----|
| vibe.exportMarkdown | Export Markdown | 导出 Markdown | 匯出 Markdown | Markdownエクスポート | Exportar Markdown |
| vibe.exportFailed | Export failed | 导出失败 | 匯出失敗 | エクスポート失敗 | Error al exportar |
renderMessageMarkdown() in core/webui.gochatToExportMessages() filter in VibeCoding.tsx if the type was previously excludedformat field to ExportRequest (default: "markdown")buildExportJSON)The same POST /api/vibe/export endpoint works — just POST the messages in the
same format. The frontend needs a new export button in the IM session UI that
fetches messages and calls exportVibeSession().
Active Tab: tab.messages is empty (no messages yet).
History: session.message_count === 0 (empty session).
Check browser console for CORS errors. Ensure setCORS() is called in the handler.
In dev mode, verify vite proxy forwards /api/vibe/export to port 9830.
Ensure Content-Disposition includes both filename (ASCII fallback) and
filename*=UTF-8'' (RFC 5987). The handler uses url.PathEscape() for encoding.
The handler limits body to 20MB via http.MaxBytesReader. For very long sessions,
consider increasing the limit or paginating messages.
vibe-chat-history — ChatStore persistence, history panel, REST API for sessions/messageswebui-vibe-coding — WebUI architecture, WebSocket protocol, webuiSession lifecycleadd-new-feature — General feature implementation checklisttools
This skill should be used when the user asks about "webui", "web ui", "vibe coding", "WebUIServer", "web interface", "browser Claude Code", "web frontend", "React admin dashboard", "web/src", "VibeCoding page", "WebSocket /api/vibe/ws", "webui config", "port 9830", "static file serving", "webuiSession", "claude code subprocess from web", "project dropdown", "work dir select", "listProjects", "Management API frontend", "multi tab vibe", "TabBar", "VibeSession component", "copy work dir", "clipboard copy", "disconnect confirm", "断开确认", "复制路径", "copyWorkDir", "AgentSystemPrompt", "project awareness", "/project command in agent", "attachment upload", "sendWithAttachments", "file upload vibe", "image upload vibe", "drag drop vibe", "paste image vibe", or needs to debug, extend, or understand the browser-based Vibe Coding interface and its Go backend.
tools
This skill should be used when the user asks about "permission request", "control_request", "control_response", "permission prompt tool", "permission-prompt-tool stdio", "permission popup not showing", "permission dialog missing", "authorize tool use", "allow deny button", "webuiSession permission", "respondPermission", "updatedInput", "control_cancel_request", "permission cancelled", "permission_cancelled", "pendingInputs", "permission flow webui", "vibe permission", "前端没有弹出授权", "权限请求不显示", "权限弹窗", "webuiSession vs claudeSession", "webui parity", or needs to debug, extend, or understand how tool permission requests flow between Claude Code CLI, the Go WebUI backend, and the React frontend.
tools
This skill should be used when the user asks about "attachment upload", "file upload", "image upload", "paste image", "drag drop file", "vibe attachment", "pendingAttachments", "sendWithAttachments", "fileToAttachment", "AttachmentItem", "base64 attachment", "WebSocket attachment protocol", "multimodal content", "image content block", "file content block", "clipboard paste image", "drag and drop upload", "attachment preview", "file size limit", "10MB limit", "ExtFromMime", "附件上传", "图片上传", "拖拽上传", "粘贴图片", "附件预览", "文件大小限制", or needs to debug, extend, or understand the WebUI attachment upload feature including the full-stack data flow from browser to Claude Code CLI.
development
This skill should be used when the user asks about "vibe history", "chat history persistence", "vibe chat database", "ChatStore biz_type", "cc_sessions biz_type", "cc_chat_messages biz_type", "vibe MySQL", "vibe session save", "/api/vibe/sessions", "VibeHistory component", "handleVibeSessions", "handleVibeSessionMessages", "ListSessions", "GetMessages", "chat history sidebar", "load history session", "continue conversation from history", "vibe chatstore integration", "webuiSession chatStore", "chatStore is nil", "MySQL DSN format", "mysql driver log", "slogWriter", "chatstore log", "database operation log", "vite proxy /api/vibe", "404 api vibe sessions", "unified history", "IM history in vibe", "listAllSessionsSQL", "biz_type filter", "history includes IM", "history source tag", or needs to debug, extend, or understand how Vibe Coding chat messages are persisted to MySQL and loaded as browsable history in the frontend.