
    function closeDialog(dialogId) {
        var dialog = document.getElementById(dialogId);
        if (dialog) dialog.classList.add('hidden');
    }
/**
 * ZAgent Chat Manager - Ported from PhoneIDE ChatManager
 * Features: SSE streaming, tool call display, markdown rendering, async input, pause task, dual theme
 * + Model test button, modality checkboxes (text/image/video/audio), reasoning checkbox
 * + File/image upload, myagent-style workflow/alarm UI
 */
const ChatManager = (() => {
    'use strict';

    // ── State ────────────────────────────────────────────────
    let isProcessing = false;
    let messages = [];
    let currentConvId = null;
    let currentAbortController = null;
    let currentStreamEl = null;
    let streamBuffer = '';
    let autoScrollEnabled = true;
    let llmConfig = null;
    let conversations = [];
    let workingModels = []; // working copy for settings dialog
    let pendingAttachments = [];
    let ttsEnabled = false; // global TTS toggle
    let ttsAutoRead = false; // auto-read assistant messages
    let ttsEngine = 'edgetts'; // 'edgetts' or 'indextts'

    // ── Pagination State ──
    let PAGE_SIZE = 50;          // messages per page
    let hasMoreMessages = false;  // whether older messages exist
    let isLoadingMore = false;    // prevent duplicate loads
    let oldestMsgTimestamp = null; // timestamp of the oldest loaded message
    let ttsServerUrl = '';
    let ttsSpeed = 1.0;
    let ttsVoiceIndex = 0;
    let currentSpeakingEl = null; // currently speaking message element
    let currentTTSAudio = null; // currently playing Edge/Index TTS Audio element
    let thinkingEl = null; // thinking/retry status element

    // ── i18n (Internationalization) ────────────────────────────
    let _lang = localStorage.getItem('zagent_lang') || 'en';

    const I18N = {
        // ── Sidebar ──
        'sidebar.newChat':       { en: 'New Chat', zh: '新对话' },
        'sidebar.closeSidebar':  { en: 'Close sidebar', zh: '关闭侧边栏' },
        'sidebar.noConversations': { en: 'No conversations yet', zh: '暂无对话' },
        'sidebar.llm':           { en: 'LLM', zh: '大模型' },
        'sidebar.systemPrompt':  { en: 'System Prompt', zh: '提示词' },
        'sidebar.aicq':          { en: 'AICQ', zh: 'AICQ通信' },
        'sidebar.skills':        { en: 'Skills', zh: '技能' },
        'sidebar.sites':         { en: 'Sites', zh: '网站管理' },
        'sidebar.workflows':     { en: 'Workflows', zh: '工作流' },
        'sidebar.alarms':        { en: 'Alarms', zh: '闹钟' },
        'sidebar.workdir':       { en: 'Work Dir', zh: '工作目录' },
        'sidebar.tts':           { en: 'TTS', zh: '朗读' },
        'sidebar.customerService': { en: 'Customer Svc', zh: '设为客服' },

        // ── Chat header ──
        'header.toggleSidebar':  { en: 'Toggle sidebar', zh: '切换侧边栏' },
        'header.toggleTheme':    { en: 'Toggle theme', zh: '切换主题' },
        'header.deleteConv':     { en: 'Delete conversation', zh: '删除对话' },
        'header.settings':       { en: 'Settings', zh: '设置' },

        // ── Chat area ──
        'chat.title':            { en: 'ZAgent AI Assistant', zh: 'ZAgent AI 助手' },
        'chat.startConversation': { en: 'Start a conversation to begin', zh: '开始对话以使用' },
        'chat.inputPlaceholder':  { en: 'Type a message... (Enter to send, Shift+Enter for new line)', zh: '输入消息... (Enter发送, Shift+Enter换行)' },
        'chat.stop':             { en: 'Stop', zh: '停止' },
        'chat.send':             { en: 'Send', zh: '发送' },
        'chat.attachFile':       { en: 'Attach file', zh: '发送文件' },
        'chat.takePhoto':        { en: 'Take photo', zh: '拍照发送' },
        'chat.selectModel':      { en: 'Select model', zh: '选择模型' },
        'chat.confirmDeleteConv': { en: 'Delete this conversation?', zh: '确定删除此对话？' },
        'chat.convRenamed': { en: 'Conversation renamed', zh: '对话已重命名' },
        'chat.renameFailed': { en: 'Rename failed', zh: '重命名失败' },
        'chat.failedLoadConv':   { en: 'Failed to load conversation', zh: '加载对话失败' },
        'chat.loadMoreMessages':  { en: 'Scroll up to load more', zh: '向上滚动加载更多消息' },
        'chat.taskRunning':      { en: 'A task is currently running. Please stop it first.', zh: '当前有任务运行中，请先停止' },
        'chat.taskCancelled':    { en: 'Task cancelled', zh: '任务已取消' },
        'chat.taskAutoStopped': { en: 'Auto-stopped residual task, retrying...', zh: '已自动停止残留任务，正在重试...' },
        'chat.untitled':         { en: 'Untitled', zh: '未命名' },

        // ── Settings dialog ──
        'settings.title':        { en: 'LLM Settings', zh: '大模型设置' },
        'settings.addModel':     { en: '+ Add Model', zh: '+ 添加模型' },
        'settings.save': { en: 'Save', zh: '保存' },
        'settings.saved':        { en: 'Settings saved', zh: '设置已保存' },
        'settings.saveFailed':   { en: 'Failed to save settings', zh: '保存设置失败' },
        'settings.testModel':    { en: 'Test model', zh: '测试模型' },
        'settings.name':         { en: 'Name', zh: '名称' },
        'settings.modelId':      { en: 'Model ID', zh: '模型ID' },
        'settings.apiBaseUrl':   { en: 'API Base URL', zh: 'API 基础URL' },
        'settings.apiKey':       { en: 'API Key', zh: 'API 密钥' },
        'settings.temperature':  { en: 'Temperature', zh: '温度' },
        'settings.maxTokens':    { en: 'Max Tokens', zh: '最大令牌数' },
        'settings.enabled': { en: 'Enabled', zh: '已启用' },
        'settings.systemPrompt': { en: 'System Prompt (instructions for the AI assistant)', zh: '系统提示词（AI助手指令）' },
        'settings.modalities':   { en: 'Modalities:', zh: '模型能力:' },
        'settings.reasoning':    { en: 'Reasoning', zh: '推理' },
        'settings.text':         { en: 'Text', zh: '文本' },
        'settings.image':        { en: 'Image', zh: '图像' },
        'settings.video':        { en: 'Video', zh: '视频' },
        'settings.audio':        { en: 'Audio', zh: '音频' },
        'settings.disabled':     { en: ' (disabled)', zh: ' (已禁用)' },

        // ── System Prompt dialog ──
        'sysprompt.title':       { en: 'System Prompt', zh: '系统提示词' },
        'sysprompt.placeholder': { en: 'Enter system prompt...', zh: '输入系统提示词...' },
        'sysprompt.saved':       { en: 'System prompt saved', zh: '系统提示词已保存' },
        'sysprompt.saveFailed':  { en: 'Failed to save system prompt', zh: '保存系统提示词失败' },

        // ── AICQ / Status dialog ──
        'status.title':          { en: 'AICQ', zh: 'AICQ通信' },
        'status.failedGetStatus': { en: 'Failed to get status', zh: '获取状态失败' },

        // ── Workflows ──
        'wf.title':              { en: 'Workflows', zh: '工作流' },
        'wf.refresh':            { en: 'Refresh', zh: '刷新' },
        'wf.new':                { en: '+ New Workflow', zh: '+ 新建工作流' },
        'wf.total': { en: 'Total', zh: '总数' },
        'wf.enabled': { en: 'Enabled', zh: '已启用' },
        'wf.empty': { en: 'No workflows yet. Click above to create one.', zh: '暂无工作流，点击上方按钮创建' },
        'wf.enable':             { en: 'Enable', zh: '启用' },
        'wf.disable':            { en: 'Disable', zh: '禁用' },
        'wf.started': { en: 'Workflow started', zh: '工作流已启动' },
        'wf.enabled2': { en: 'Enabled', zh: '已启用' },
        'wf.disabled2':          { en: 'Disabled', zh: 'disabled' },
        'wf.copied': { en: 'Workflow copied', zh: '工作流已复制' },
        'wf.confirmDelete':      { en: 'Delete workflow "{0}"? This cannot be undone.', zh: '删除工作流 "{0}"？此操作不可恢复。' },
        'wf.deleted': { en: 'Deleted', zh: '已删除' },
        'wf.deleteFailed':       { en: 'Delete failed', zh: '删除失败' },
        'wf.loadFailed':         { en: 'Load failed', zh: '加载失败' },
        'wf.editTitle': { en: 'Edit Workflow', zh: '编辑工作流' },
        'wf.newTitle': { en: 'New Workflow', zh: '新建工作流' },
        'wf.saveChanges': { en: 'Save Changes', zh: '保存更改' },
        'wf.create': { en: 'Create Workflow', zh: '创建工作流' },
        'wf.name': { en: 'Name *', zh: '名称 *' },
        'wf.namePlaceholder': { en: 'Enter workflow name', zh: '请输入工作流名称' },
        'wf.description': { en: 'Description', zh: '描述' },
        'wf.descPlaceholder': { en: 'Brief description (optional)', zh: '简短描述（可选）' },
        'wf.variables': { en: 'Variables (JSON)', zh: '变量 (JSON)' },
        'wf.addStep': { en: '+ Add Step', zh: '+ 添加步骤' },
        'wf.cancel': { en: 'Cancel', zh: '取消' },
        'wf.noSteps': { en: 'No steps yet. Click below to add.', zh: '暂无步骤，点击下方按钮添加' },
        'wf.expand':             { en: 'Expand', zh: '展开' },
        'wf.edit':               { en: 'Edit', zh: '编辑' },
        'wf.moveUp':             { en: 'Move up', zh: '上移' },
        'wf.moveDown':           { en: 'Move down', zh: '下移' },
        'wf.remove':             { en: 'Remove', zh: '删除' },
        'wf.addStepTitle': { en: '+ Add Step', zh: '+ 添加步骤' },
        'wf.addBtn': { en: 'Add', zh: '添加' },
        'wf.requiredEmpty':      { en: 'Required parameters cannot be empty: ', zh: '必填参数不能为空: ' },
        'wf.addCustomParam': { en: '+ Add custom parameter', zh: '+ 添加自定义参数' },
        'wf.save2': { en: 'Save', zh: '保存' },
        'wf.enterName': { en: 'Please enter a workflow name', zh: '请输入工作流名称' },
        'wf.addOneStep': { en: 'Please add at least one step', zh: '请至少添加一个步骤' },
        'wf.created': { en: 'Workflow created', zh: '工作流已创建' },
        'wf.createFailed':       { en: 'Create failed', zh: '创建失败' },
        'wf.invalidJson': { en: 'Invalid JSON for variables', zh: '变量的 JSON 格式无效' },
        'wf.saved2': { en: 'Saved', zh: '已保存' },
        'wf.saveFailed2':        { en: 'Save failed', zh: '保存失败' },
        'wf.runs':               { en: 'Runs', zh: '运行记录' },
        'wf.loading': { en: 'Loading...', zh: '加载中...' },
        'wf.close': { en: 'Close', zh: '关闭' },
        'wf.runFailed':          { en: 'Run failed', zh: '运行失败' },
        'wf.confirmCopyOrDelete': { en: 'Copy or Delete? OK=Copy, Cancel=Delete', zh: '复制还是删除？确定=复制，取消=删除' },
        'wf.unknownError':       { en: 'Unknown error', zh: '未知错误' },
        'wf.operationFailed':    { en: 'Operation failed', zh: '操作失败' },
        'wf.copyFailed':         { en: 'Copy failed', zh: '复制失败' },

        // ── Alarms ──
        'alarm.title':           { en: 'Alarms', zh: '闹钟' },
        'alarm.paused': { en: 'Alarm paused', zh: '闹钟已暂停' },
        'alarm.pauseFailed':     { en: 'Pause failed', zh: '暂停失败' },
        'alarm.resumed': { en: 'Alarm resumed', zh: '闹钟已恢复' },
        'alarm.resumeFailed':    { en: 'Resume failed', zh: '恢复失败' },
        'alarm.confirmDelete':   { en: 'Delete alarm "{0}"? This cannot be undone.', zh: '删除闹钟 "{0}"？此操作不可恢复。' },
        'alarm.deleted': { en: 'Alarm deleted', zh: '闹钟已删除' },
        'alarm.deleteFailed':    { en: 'Delete failed', zh: '删除失败' },
        'alarm.enterName': { en: 'Please enter an alarm name', zh: '请输入闹钟名称' },
        'alarm.created': { en: 'Alarm created', zh: '闹钟已创建' },
        'alarm.createFailed':    { en: 'Create failed', zh: '创建失败' },
        'alarm.modeOnce':        { en: 'One-time', zh: '一次性' },
        'alarm.modeDaily':       { en: 'Daily', zh: '每日' },
        'alarm.modeWeekdays':    { en: 'Weekdays', zh: '工作日' },
        'alarm.modeWeekly':      { en: 'Weekly', zh: '每周' },
        'alarm.modeMonthly':     { en: 'Monthly', zh: '每月' },
        'alarm.modeInterval':    { en: 'Custom interval', zh: '自定义间隔' },
        'alarm.scheduleSet': { en: 'Schedule set', zh: '已设置计划' },
        'alarm.scheduleSetFailed': { en: 'Failed to set schedule', zh: '设置调度失败' },
        'alarm.scheduleCancelled': { en: 'Schedule cancelled', zh: '计划已取消' },
        'alarm.cancelFailed':    { en: 'Failed to cancel schedule', zh: '取消调度失败' },

        // ── TTS ──
        'tts.title':             { en: 'TTS Settings', zh: '朗读设置' },
        'tts.engine':            { en: 'TTS Engine', zh: '朗读引擎' },
        'tts.edgeTts':           { en: 'Edge TTS (Recommended)', zh: 'Edge TTS (推荐)' },
        'tts.indexTts':          { en: 'IndexTTS Server', zh: 'IndexTTS 服务器' },
        'tts.voice':             { en: 'Voice', zh: '语音' },
        'tts.speed':             { en: 'Speed', zh: '语速' },
        'tts.autoRead':          { en: 'Auto-read assistant messages', zh: '自动朗读助手消息' },
        'tts.testTTS':           { en: 'Test TTS', zh: '测试朗读' },
        'tts.enabled':           { en: 'TTS enabled', zh: '朗读已开启' },
        'tts.disabled':          { en: 'TTS disabled', zh: '朗读已关闭' },
        'tts.enableFirst': { en: 'Please enable TTS toggle first', zh: '请先启用朗读开关' },
        'tts.toggleOn': { en: 'TTS is off (click to enable)', zh: '朗读已关闭（点击启用）' },
        'tts.toggleOff': { en: 'TTS is on (click to disable)', zh: '朗读已开启（点击关闭）' },
        'tts.saved': { en: 'TTS settings saved', zh: '朗读设置已保存' },
        'tts.saveFailed':        { en: 'Failed to save TTS settings', zh: '保存TTS设置失败' },
        'tts.playFailed': { en: 'Playback failed', zh: '播放失败' },
        'tts.read':              { en: 'Read', zh: '朗读' },
        'tts.stopRead': { en: 'Stop reading', zh: '停止朗读' },
        'tts.preparing': { en: 'Preparing TTS...', zh: '正在准备朗读...' },
        'tts.noVoice': { en: 'No voice available', zh: '没有可用的语音' },
        'tts.androidVoice':      { en: 'Android System Voice', zh: 'Android 系统语音' },
        'tts.androidVoiceReady': { en: 'Android System Voice (Ready)', zh: 'Android 系统语音 (已就绪)' },
        'tts.androidVoiceInit':  { en: 'Android System Voice (Initializing...)', zh: 'Android 系统语音 (初始化中...)' },
        'tts.browserFallback': { en: 'Edge TTS unavailable, switched to browser voice', zh: 'Edge 朗读不可用，已切换到浏览器语音' },
        'tts.unavailable': { en: 'TTS unavailable: Edge TTS not installed, Android voice not ready, browser unsupported', zh: '朗读不可用：未安装 Edge 朗读、安卓语音未就绪、浏览器不支持' },

        // ── Skills ──
        'skills.title':          { en: 'Skills', zh: '技能' },
        'skills.local':          { en: 'Local Skills', zh: '本机技能' },
        'skills.market':         { en: 'ClawHub Market', zh: 'ClawHub 市场' },
        'skills.installed':      { en: 'Installed', zh: '已安装' },
        'skills.total':          { en: 'Total', zh: '共' },
        'skills.builtin': { en: 'Built-in', zh: '内置' },
        'skills.cli': { en: 'CLI', zh: '命令行' },
        'skills.python': { en: 'Python', zh: 'Python' },
        'skills.guide':          { en: 'Guides', zh: '指南' },
        'skills.disabled': { en: 'disabled', zh: '已禁用' },
        'skills.search': { en: 'Search tools/skills...', zh: '搜索工具/技能...' },
        'skills.builtinTools':   { en: 'Built-in Platform Tools', zh: '内置平台工具' },
        'skills.builtinHint': { en: 'LLM direct call · Cannot disable', zh: 'LLM 直调 · 不可禁用' },
        'skills.cliCommands':    { en: 'CLI Commands', zh: 'CLI 命令' },
        'skills.cliHint': { en: 'Via command tool · Cannot disable', zh: '通过命令工具 · 不可禁用' },
        'skills.pythonSkills':   { en: 'Python Skills', zh: 'Python 技能' },
        'skills.skillGuides':    { en: 'Skill Guides', zh: '技能指南' },
        'skills.noDesc': { en: 'No description', zh: '暂无描述' },
        'skills.detail': { en: 'Detail', zh: '详情' },
        'skills.enable':         { en: 'Enable', zh: '启用' },
        'skills.disableSkill':   { en: 'Disable', zh: '禁用' },
        'skills.noSkills': { en: 'No tools/skills yet', zh: '暂无工具/技能' },
        'skills.loadFailed':     { en: 'Load failed', zh: '加载失败' },
        'skills.enabled': { en: 'Enabled', zh: '已启用' },
        'skills.enableFailed':   { en: 'Operation failed', zh: '操作失败' },
        'skills.gettingDetail': { en: 'Loading detail...', zh: '正在加载详情...' },
        'skills.getDetailFailed': { en: 'Failed to load detail', zh: '获取详情失败' },
        'skills.description': { en: 'Description', zh: '描述' },
        'skills.type':           { en: 'Type', zh: '类型' },
        'skills.category':       { en: 'Category', zh: '类别' },
        'skills.note':           { en: 'Note', zh: '备注' },
        'skills.dangerous':      { en: 'Dangerous', zh: '危险操作' },
        'skills.yes':            { en: 'Yes', zh: '是' },
        'skills.no':             { en: 'No', zh: '否' },
        'skills.params':         { en: 'Parameters', zh: '参数' },
        'skills.paramName':      { en: 'Name', zh: '名称' },
        'skills.paramType':      { en: 'Type', zh: '类型' },
        'skills.paramRequired':  { en: 'Required', zh: '必需' },
        'skills.paramDesc': { en: 'Description', zh: '描述' },
        'skills.builtinPlatform': { en: 'Built-in', zh: '内置平台' },
        'skills.cliCommand':     { en: 'CLI Command', zh: 'CLI 命令' },
        'skills.skillGuide':     { en: 'Skill Guide', zh: '技能指南' },
        'skills.pythonSkill':    { en: 'Python Skill', zh: 'Python 技能' },
        'skills.close': { en: 'Close', zh: '关闭' },

        // ── ClawHub Market ──
        'market.searchPlaceholder': { en: 'Search ClawHub marketplace... (e.g. pdf, chart, seo)', zh: '搜索 ClawHub 市场... (如 pdf, chart, seo)' },
        'market.search':         { en: 'Search', zh: '搜索' },
        'market.newest':         { en: 'Newest', zh: '最新' },
        'market.trending':       { en: 'Trending', zh: '热门' },
        'market.downloads':      { en: 'Downloads', zh: '下载' },
        'market.hint': { en: 'ClawHub is the skill registry of OpenClaw ecosystem with 13,000+ community skills. Installed skills auto-register locally.', zh: 'ClawHub 是 OpenClaw 生态的技能注册中心，包含 13,000+ 社区技能。安装后会自动注册到本地。' },
        'market.searching': { en: 'Searching ClawHub...', zh: '正在搜索 ClawHub...' },
        'market.searchFailed':   { en: 'Search failed', zh: '搜索失败' },
        'market.searchError':    { en: 'Search error', zh: '搜索出错' },
        'market.noResults': { en: 'No matching skills found', zh: '未找到匹配的技能' },
        'market.found':          { en: 'Found', zh: '找到' },
        'market.install':        { en: 'Install', zh: '安装' },
        'market.installing':     { en: 'Installing...', zh: '安装中...' },
        'market.installed':      { en: 'Installed', zh: '已安装' },
        'market.installSuccess': { en: 'Skill {0} installed successfully!', zh: '技能 {0} 安装成功！' },
        'market.installFailed':  { en: 'Install failed', zh: '安装失败' },
        'market.installError':   { en: 'Install error', zh: '安装出错' },
        'market.getDetailFailed': { en: 'Failed to load detail', zh: '获取详情失败' },
        'market.version':        { en: 'Version', zh: '版本' },
        'market.author':         { en: 'Author', zh: '作者' },
        'market.license':        { en: 'License', zh: '许可证' },
        'market.securityScan':   { en: 'Security Scan', zh: '安全扫描' },
        'market.safe':           { en: 'Safe', zh: '安全' },
        'market.suspicious':     { en: 'Suspicious', zh: '可疑' },
        'market.malicious':      { en: 'Malicious', zh: '恶意' },
        'market.unknown':        { en: 'Unknown', zh: '未知' },
        'market.installSkill':   { en: 'Install this skill', zh: '安装此技能' },
        'market.noInstalled': { en: 'No ClawHub skills installed yet', zh: '尚未安装 ClawHub 技能' },
        'market.goMarket':       { en: 'Go to Market', zh: '去市场看看' },
        'market.installedCount': { en: 'ClawHub skills installed', zh: '已安装 ClawHub 技能' },
        'market.updateAll':      { en: 'Update All', zh: '全部更新' },
        'market.update':         { en: 'Update', zh: '更新' },
        'market.uninstall':      { en: 'Uninstall', zh: '卸载' },
        'market.fileMissing':    { en: 'File missing', zh: '文件缺失' },
        'market.installedOn':    { en: 'Installed on', zh: '安装于' },
        'market.confirmUninstall': { en: 'Uninstall ClawHub skill "{0}"?', zh: '确定要卸载 ClawHub 技能「{0}」吗？' },
        'market.uninstalled':    { en: 'Skill {0} uninstalled', zh: '技能 {0} 已卸载' },
        'market.uninstallFailed': { en: 'Uninstall failed', zh: '卸载失败' },
        'market.uninstallError': { en: 'Uninstall error', zh: '卸载出错' },
        'market.updating':       { en: 'Updating {0}...', zh: '正在更新 {0}...' },
        'market.updateDone':     { en: 'Skill {0} updated', zh: '技能 {0} 更新完成' },
        'market.updateFailed':   { en: 'Update failed', zh: '更新失败' },
        'market.updateError':    { en: 'Update error', zh: '更新出错' },
        'market.updatingAll': { en: 'Updating all ClawHub skills...', zh: '正在更新所有 ClawHub 技能...' },
        'market.allUpdated': { en: 'All skills updated', zh: '所有技能已更新' },
        'market.allUpdateFailed': { en: 'Update all failed', zh: '更新全部失败' },
        'market.browsing':       { en: 'Browsing', zh: '正在浏览' },
        'market.browseFailed':   { en: 'Browse failed', zh: '浏览失败' },
        'market.browseError':    { en: 'Browse error', zh: '浏览出错' },
        'market.enterKeyword': { en: 'Please enter a search keyword', zh: '请输入搜索关键字' },

        // ── Sites ──
        'sites.title':           { en: 'Sites', zh: '网站管理' },
        'sites.add':             { en: '+ Add', zh: '+ 添加' },
        'sites.initAll':         { en: 'Init All', zh: '初始化全部' },
        'sites.totalSites':      { en: 'sites', zh: '个网站' },
        'sites.builtin': { en: 'Built-in', zh: '内置' },
        'sites.custom':          { en: 'Custom', zh: '自定义' },
        'sites.profiles':        { en: 'profiles', zh: '个' },
        'sites.loadFailed':      { en: 'Load failed', zh: '加载失败' },
        'sites.email':           { en: 'Email', zh: '邮箱服务' },
        'sites.social':          { en: 'Social', zh: '社交媒体' },
        'sites.cloud':           { en: 'Cloud', zh: '云服务' },
        'sites.aiChat':          { en: 'AI Chat', zh: 'AI 对话' },
        'sites.dev':             { en: 'Dev Tools', zh: '开发工具' },
        'sites.content':         { en: 'Content', zh: '内容平台' },
        'sites.custom2':         { en: 'Custom', zh: '自定义' },

        // ── Files / Workdir ──
        'files.title':           { en: 'Work Dir', zh: '工作目录' },
        'files.changeDir':       { en: 'Change Dir', zh: '更改目录' },
        'files.newWorkdirPath':  { en: 'New work directory path:', zh: '新工作目录路径:' },
        'files.workdirUpdated': { en: 'Work directory updated', zh: '工作目录已更新' },
        'files.confirmDeleteItem': { en: 'Delete {0} "{1}"? This cannot be undone.', zh: '确认删除{0} "{1}"？此操作不可恢复。' },
        'files.folder': { en: 'folder', zh: '文件夹' },
        'files.file': { en: 'file', zh: '文件' },
        'files.deleteFailed':    { en: 'Delete failed', zh: '删除失败' },
        'files.deleted': { en: 'Deleted', zh: '已删除' },
        'files.download':        { en: 'Download', zh: '下载' },
        'files.delete':          { en: 'Delete', zh: '删除' },

        // ── Customer Service ──
        'cs.title':              { en: 'Customer Service', zh: '客服模式' },
        'cs.enabled': { en: 'Customer service mode is enabled', zh: '客服模式已启用' },
        'cs.domainRestricted': { en: 'Domain restricted', zh: '限定网址' },
        'cs.openAccess': { en: 'Open access', zh: '不限来源' },
        'cs.hint':               { en: 'Set this agent as customer service, allowing website visitors to chat via embedded JS.', zh: '将该智能体设置为客服，允许网站访客通过嵌入JS的方式与其对话。' },
        'cs.hint2':              { en: 'In CS mode, you can limit access sources and available tools.', zh: '客服模式下可限定访问来源和可用工具，确保智能体按规范回复。' },
        'cs.mode':               { en: 'CS Mode', zh: '客服模式' },
        'cs.domainOnly': { en: 'Domain restricted', zh: '限定网址' },
        'cs.openMode': { en: 'Open access', zh: '不限来源' },
        'cs.domainLabel':        { en: 'Restricted domain/URL', zh: '限定网址/域名' },
        'cs.domainPlaceholder':  { en: 'e.g. example.com or https://shop.example.com', zh: '例如: example.com 或 https://shop.example.com' },
        'cs.domainHint':         { en: 'Only visitors from this domain/URL can chat with the agent. Separate multiple domains with commas.', zh: '仅允许来自该域名/网址的访客与智能体对话，多个域名用逗号分隔' },
        'cs.systemPrompt':       { en: 'CS System Prompt', zh: '客服系统提示词' },
        'cs.systemPromptPlaceholder': { en: 'You are a professional customer service representative...', zh: '你是一名专业的客服代表，请礼貌、专业地回答用户的问题...' },
        'cs.systemPromptHint':  { en: 'In CS mode, the agent will strictly follow this prompt.', zh: '客服模式下智能体将严格按照此提示词回复' },
        'cs.toolsLabel':         { en: 'CS Available Tools', zh: '客服可调用工具' },
        'cs.toolsHint':          { en: 'Only checked tools are available in CS mode.', zh: '仅勾选的工具在客服模式下可用，确保智能体权限受限' },
        'cs.transferEnabled':    { en: 'Support transfer to human', zh: '支持转人工' },
        'cs.transferHint':       { en: 'When enabled, visitors can request "transfer to human" and the agent will hand off.', zh: '启用后，当访客要求"转人工"时，智能体会将对话转接给人工客服' },
        'cs.transferTarget':     { en: 'Transfer target (human agent account)', zh: '转接目标（人工客服账号）' },
        'cs.transferPlaceholder': { en: 'Enter human agent AICQ Account ID', zh: '输入人工客服的 AICQ Account ID' },
        'cs.embedCode':          { en: 'Embed Code', zh: '嵌入代码' },
        'cs.embedHint':          { en: 'Embed this code in your website HTML to enable CS', zh: '将此代码嵌入网站HTML即可使用客服功能' },
        'cs.cancelCS':           { en: 'Disable CS', zh: '取消客服' },
        'cs.enableCS': { en: 'Enable CS', zh: '启用客服' },
        'cs.updateCS': { en: 'Update CS Settings', zh: '更新客服设置' },
        'cs.saveSuccess': { en: 'CS settings saved', zh: '客服设置已保存' },
        'cs.saveFailed':         { en: 'Save failed', zh: '设置失败' },
        'cs.enterDomain': { en: 'Please enter the restricted domain/URL', zh: '请输入限定的域名/URL' },
        'cs.enterTransferTarget': { en: 'Transfer target account is required when enabling human transfer', zh: '启用转人工时必须填写转接目标账号' },
        'cs.confirmCancel': { en: 'Disable customer service mode? Visitors will no longer be able to chat via embedded JS.', zh: '确定要禁用客服模式？访客将无法通过嵌入 JS 与之对话。' },
        'cs.cancelSuccess': { en: 'CS mode disabled', zh: '客服模式已禁用' },
        'cs.cancelFailed':       { en: 'Cancel failed', zh: '取消失败' },
        'cs.toolCommand': { en: 'Command exec', zh: '执行命令' },
        'cs.toolWebSearch': { en: 'Web search', zh: '网页搜索' },
        'cs.toolWebReader': { en: 'Web reader', zh: '网页阅读' },
        'cs.toolFileSend': { en: 'File send', zh: '发送文件' },
        'cs.toolSaveMemory': { en: 'Save memory', zh: '保存记忆' },
        'cs.toolRecallMemory': { en: 'Recall memory', zh: '回忆记忆' },

        // ── Owner ──
        'owner.enterId':         { en: 'Enter owner AICQ Account ID', zh: '输入主人的 AICQ Account ID' },
        'owner.hint':            { en: 'After setting an owner, the AI Agent will notify them of friend requests etc. Owner binding also prevents zombie cleanup from deleting this Agent.', zh: '设置主人后，AI Agent 会将好友请求等事件通知主人。主人绑定也可以防止僵尸清理删除此 Agent。' },
        'owner.setFailed':       { en: 'Set failed', zh: '设置失败' },
        'owner.setSuccess': { en: 'Owner set successfully!', zh: '主人设置成功！' },
        'owner.friendSent':      { en: 'Friend request sent automatically!', zh: '已自动发送好友请求!' },
        'owner.friendFailed':    { en: 'Friend request failed', zh: '好友请求发送失败，请手动添加好友' },
        'owner.currentOwner':    { en: 'Current owner', zh: '当前主人' },
        'owner.noOwner': { en: 'No owner set', zh: '未设置主人' },
        'owner.queryFailed': { en: 'Query failed', zh: '查询失败' },
        'owner.confirmClear': { en: 'Clear owner binding?', zh: '确定要清除主人绑定吗？' },
        'owner.cleared': { en: 'Owner cleared', zh: '主人已清除' },
        'owner.clearFailed':     { en: 'Clear failed', zh: '清除失败' },
        'owner.enterOrBind': { en: 'Please enter or bind an owner AICQ Account ID first', zh: '请先输入或绑定主人的 AICQ 账号 ID' },
        'owner.sending': { en: 'Sending...', zh: '正在发送...' },
        'owner.friendReqSent': { en: 'Friend request sent to owner!', zh: '已向主人发送好友请求！' },
        'owner.alreadyFriends': { en: 'Already friends or request already sent', zh: '已是好友或请求已发送' },
        'owner.friendReqFailed': { en: 'Friend request failed', zh: '好友请求失败' },
        'owner.sendFailed':      { en: 'Send failed', zh: '发送失败' },
        'owner.sendFriendReq':   { en: 'Send friend request to owner', zh: '向主人发起好友请求' },

        // ── Upload / File ──
        'upload.failed':         { en: 'Upload failed', zh: '上传失败' },
        'upload.error':          { en: 'Upload error', zh: '上传错误' },
        'upload.photoFailed': { en: 'Photo failed or cancelled', zh: '拍照失败或已取消' },
        'upload.taskRunning': { en: 'A task is running, please try later', zh: '有任务正在运行，请稍后再试' },

        // ── Context Menu ──
        'ctx.rename':            { en: 'Rename', zh: '重命名' },
        'ctx.deleteConv':        { en: 'Delete conversation', zh: '删除会话' },

        // ── Secrets ──
        'secret.enterName': { en: 'Please enter a secret name', zh: '请输入密钥名称' },
        'secret.enterValue': { en: 'Please enter a secret value', zh: '请输入密钥值' },
        'secret.saved': { en: 'Secret saved', zh: '密钥已保存' },
        'secret.saveFailed':     { en: 'Save failed', zh: '保存失败' },
        'secret.confirmDelete':  { en: 'Delete secret "{0}"? This cannot be undone.', zh: '删除密钥 "{0}"？此操作不可恢复。' },
        'secret.deleted': { en: 'Secret deleted', zh: '密钥已删除' },
        'secret.deleteFailed':   { en: 'Delete failed', zh: '删除失败' },

        // ── Language ──
        'lang.en': { en: 'English', zh: 'English' },
        'lang.zh':               { en: 'Chinese', zh: '中文' },
        'lang.label':            { en: 'Language', zh: '语言' },

        // ── General ──
        'general.backToChat':    { en: 'Back to chat', zh: '返回聊天' },
        'general.loading': { en: 'Loading...', zh: '加载中...' },
        'panel.aicqTitle':            { en: 'AICQ Communication', zh: 'AICQ通信' },
        'panel.workflowsTitle':            { en: '⚡ Workflows', zh: '⚡ 工作流' },
        'panel.alarmsTitle':            { en: '⏰ Alarms', zh: '⏰ 闹钟' },
        'panel.skillsTitle':            { en: '🛠 Skills', zh: '🛠 技能' },
        'panel.sitesTitle':            { en: 'Site Management', zh: '网站管理' },
        'panel.workdirTitle':            { en: 'Work Directory', zh: '工作目录' },
        'panel.siteDetailTitle':            { en: 'Site Details', zh: '网站详情' },
        'panel.csTitle':            { en: '🎧 Customer Service Mode', zh: '🎧 客服模式' },
        'sites.initAll':              { en: 'Initialize All', zh: '初始化全部' },
        'cs.intro1':              { en: 'Set this agent as a customer service agent, allowing website visitors to chat with it via embedded JS.', zh: '将该智能体设置为客服，允许网站访客通过嵌入JS的方式与其对话。' },
        'cs.intro2':              { en: 'In CS mode, you can restrict access sources and available tools to ensure the agent replies according to spec.', zh: '客服模式下可限定访问来源和可用工具，确保智能体按规范回复。' },
        'cs.domainPh':              { en: 'e.g. example.com or https://shop.example.com', zh: '例如: example.com 或 https://shop.example.com' },
        'cs.domainHint':              { en: 'Only visitors from this domain/URL can chat with the agent. Separate multiple domains with commas.', zh: '仅允许来自该域名/网址的访客与智能体对话，多个域名用逗号分隔' },
        'cs.systemPromptPh':              { en: 'You are a professional customer service representative. Please answer user questions politely and professionally...', zh: '你是一名专业的客服代表，请礼貌、专业地回答用户的问题...' },
        'cs.systemPromptHint':              { en: 'In CS mode, the agent will strictly follow this prompt to reply.', zh: '客服模式下智能体将严格按照此提示词回复' },
        'cs.toolsHint':              { en: 'Only checked tools are available in CS mode to ensure the agent has limited permissions.', zh: '仅勾选的工具在客服模式下可用，确保智能体权限受限' },
        'cs.transferHint':              { en: 'When enabled, if a visitor asks for "human transfer", the agent will transfer the conversation to a human agent.', zh: '启用后，当访客要求"转人工"时，智能体会将对话转接给人工客服' },
        'cs.transferTargetPh':              { en: 'Enter human agent AICQ Account ID', zh: '输入人工客服的 AICQ Account ID' },
        'cs.embedCodeHint':              { en: 'Embed this code into your website HTML to enable CS functionality.', zh: '将此代码嵌入网站HTML即可使用客服功能' },
        'status.commStatus':              { en: 'Communication Status', zh: '通信状态' },
        'owner.accountIdLabel':              { en: 'Owner AICQ Account ID', zh: '主人 AICQ Account ID' },
        'chat.rename':              { en: 'Rename', zh: '重命名' },
        'chat.deleteConv':              { en: 'Delete conversation', zh: '删除会话' },
        'wf.disabled':              { en: 'Disabled', zh: '已禁用' },
        'wf.notRun':              { en: 'Not run', zh: '未运行' },
        'wf.success':              { en: 'Success', zh: '成功' },
        'wf.failed':              { en: 'Failed', zh: '失败' },
        'wf.running':              { en: 'Running', zh: '运行中' },
        'wf.totalRuns':              { en: 'Total runs', zh: '总运行次数' },
        'wf.refresh':              { en: 'Refresh', zh: '刷新' },
        'wf.new':              { en: '+ New Workflow', zh: '+ 新建工作流' },
        'llm.testModel':              { en: 'Test this model', zh: '测试此模型' },
        'tts.requestFailed':              { en: 'TTS request failed', zh: 'TTS 请求失败' },
        'llm.reasoningMode':              { en: 'Reasoning Mode', zh: '推理模式' },
        'llm.modalities':              { en: 'Modalities:', zh: '模型能力:' },
        'wf.tabWorkflows':              { en: 'Workflows', zh: '工作流' },
        'wf.tabSecrets':              { en: 'Secrets', zh: '密钥' },
        'wf.statEnabled':              { en: 'Enabled', zh: '已启用' },
        'wf.secretManage':              { en: '🔑 Secret Management', zh: '🔑 密钥管理' },
        'wf.steps':              { en: 'steps', zh: '步骤' },
        'wf.runCount':              { en: 'Runs', zh: '运行次数' },
        'wf.lastRunPrefix':              { en: 'Last:', zh: '上次:' },
        'wf.stepList':              { en: 'Step List', zh: '步骤列表' },
        'wf.noParams':              { en: 'No params', zh: '无参数' },
        'wf.edit':              { en: 'Edit', zh: '编辑' },
        'wf.moveUp':              { en: 'Move up', zh: '上移' },
        'wf.moveDown':              { en: 'Move down', zh: '下移' },
        'wf.delete':              { en: 'Delete', zh: '删除' },
        'wf.run':              { en: 'Run', zh: '运行' },
        'wf.schedule':              { en: 'Schedule', zh: '定时' },
        'wf.tabWorkflows':              { en: 'Workflows', zh: '工作流' },
        'wf.tabSecrets':              { en: 'Secrets', zh: '密钥' },
        'wf.runCountInline':              { en: 'Runs', zh: '次' },
        'wf.lastRunInline':              { en: 'Last:', zh: '上次:' },
        'wf.moveUp':              { en: 'Move up', zh: '上移' },
        'wf.moveDown':              { en: 'Move down', zh: '下移' },
        'wf.deleteStep':              { en: 'Delete step', zh: '删除' },
        'wf.addStepTitle':              { en: '+ Add Step', zh: '+ 添加步骤' },
        'wf.stepType':              { en: 'Step Type', zh: '步骤类型' },
        'wf.add':              { en: 'Add', zh: '添加' },
        'wf.codePlaceholder':              { en: 'Enter code to execute...', zh: '输入要执行的代码...' },
        'wf.promptPh':              { en: 'Prompt content. Use {{secrets.NAME}} for secrets, {{steps.1.result}} for step results', zh: '提示内容，支持 {{secrets.NAME}} 引用密钥、{{steps.1.result}} 引用步骤结果' },
        'wf.editStepTitle':              { en: 'Edit Step', zh: '编辑步骤' },
        'wf.addCustomParam':              { en: '+ Add custom parameter', zh: '+ 添加自定义参数' },
        'wf.save2':              { en: 'Save', zh: '保存' },
        'wf.runDetails':              { en: '▶ Run Details', zh: '▶ 运行详情' },
        'wf.closeBtn':              { en: 'Close', zh: '关闭' },
        'wf.refreshBtn':              { en: '🔄 Refresh', zh: '🔄 刷新' },
        'wf.loadFailed':              { en: 'Load failed', zh: '加载失败' },
        'wf.noRuns':              { en: 'No run records', zh: '暂无运行记录' },
        'wf.latestRun':              { en: 'Latest run:', zh: '最近运行:' },
        'wf.executing':              { en: 'Executing...', zh: '正在执行...' },
        'wf.paramValue':              { en: 'Value', zh: '值' },
        'wf.paramName':              { en: 'Parameter name', zh: '参数名' },
        'wf.confirmDeleteStep':              { en: 'Delete this step?', zh: '删除此步骤？' },
        'wf.editWorkflow':              { en: 'Edit', zh: '编辑' },
        'wf.scheduleWorkflow':              { en: 'Schedule', zh: '定时' },
        'tts.edgeRecommended':            { en: 'Edge TTS (Recommended)', zh: 'Edge TTS (推荐)' },
        'cs.mode':            { en: 'CS Mode', zh: '客服模式' },
        'cs.domainLabel':            { en: 'Restricted domain/URL', zh: '限定网址/域名' },
        'cs.systemPromptLabel':            { en: 'CS System Prompt', zh: '客服系统提示词' },
        'cs.toolsLabel':            { en: 'CS Available Tools', zh: '客服可调用工具' },
        'cs.transferTargetLabel':            { en: 'Transfer target (human agent account)', zh: '转接目标（人工客服账号）' },
        'cs.embedCodeLabel':            { en: 'Embed Code', zh: '嵌入代码' },
        'cs.disable':            { en: 'Disable CS', zh: '取消客服' },
        'cs.transferEnabled':            { en: '📞 Support human transfer', zh: '📞 支持转人工' },
        'alarm.refresh':            { en: 'Refresh', zh: '刷新' },
        'skills.refresh':            { en: 'Refresh', zh: '刷新' },
        'sites.add':            { en: '+ Add', zh: '+ 添加' },
        'files.change':            { en: 'Change', zh: '更改' },

        // ── Workflow Step Types ──
        'step.skill':            { en: 'Call Skill', zh: '调用技能' },
        'step.code':             { en: 'Run Code', zh: '运行代码' },
        'step.llm':              { en: 'LLM Call', zh: '大模型调用' },
        'step.shell':            { en: 'Run Command (legacy)', zh: '执行命令(旧)' },
        'step.browserOpen':      { en: 'Open Web Page', zh: '打开网页' },
        'step.browserExtract':   { en: 'Extract Data', zh: '提取数据' },
        'step.browserScreenshot': { en: 'Screenshot', zh: '截图' },
        'step.delay':            { en: 'Delay', zh: '等待' },
        'step.apiCall':          { en: 'API Call', zh: 'API调用' },
        'step.condition':        { en: 'Condition', zh: '条件判断' },
        'step.sendEmail':        { en: 'Send Email', zh: '发送邮件' },
        'step.timer':            { en: 'Timer/Alarm', zh: '定时器/闹钟' },

        // ── Workflow Step Help ──
        'step.skill.help':       { en: 'Reuse Agent registered Python skill. name = skill name (e.g. web-search), other params as required.', zh: '直接复用 Agent 已注册的 Python 技能，无需安装 CLI。name 填技能名（如 web-search），其余参数按技能要求填写。推荐优先使用此步骤类型。' },
        'step.code.help':        { en: 'Run Python/Shell/Bash/PowerShell/Cmd code via Agent exec engine. Use {{secrets.xxx}} in code.', zh: '运行 Python/Shell/Bash/PowerShell/Cmd 代码，复用 Agent 执行引擎。代码中可用 {{secrets.xxx}} 引用密钥。推荐优先使用此步骤类型。' },
        'step.llm.help':         { en: 'Call LLM for summary/analysis/translation/reasoning. Use {{steps.N.result}} in prompt.', zh: '调用大语言模型，用于总结/分析/翻译/推理等。复用 Agent 的 LLM 客户端。prompt 中可用 {{steps.N.result}} 引用前序步骤结果。推荐优先使用此步骤类型。' },
        'step.shell.help':       { en: 'Legacy Shell command step. Recommend using code step instead.', zh: '旧版 Shell 命令步骤，推荐改用 code 步骤。' },
        'step.browserOpen.help': { en: 'Open web page using stealth browser. Use browser_extract or browser-screenshot after.', zh: '使用反检测浏览器打开网页。后续步骤可用 browser_extract 提取内容或 browser-screenshot 截图。' },
        'step.browserExtract.help': { en: 'Extract content from browser page (requires browser-open first). selector = CSS selector.', zh: '从浏览器页面提取内容（需先执行 browser-open 步骤）。selector 为 CSS 选择器。' },
        'step.browserScreenshot.help': { en: 'Take screenshot of browser page (requires browser-open first).', zh: '截取浏览器页面截图（需先执行 browser-open 步骤）。save_path 为截图保存路径。' },
        'step.apiCall.help':     { en: 'Use {{secrets.xxx}} in headers or body, e.g. {"Authorization": "Bearer {{secrets.API_KEY}}"}', zh: '在 headers 或 body 中使用 {{secrets.xxx}} 引用密钥，如 {"Authorization": "Bearer {{secrets.API_KEY}}"}' },
        'step.sendEmail.help':   { en: 'SMTP config required in secrets: SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD.', zh: 'SMTP配置需提前在密钥管理器中设置 SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD。content_type: plain或html。QQ邮箱需授权码，Gmail需应用专用密码。' },
        'step.timer.help':       { en: 'Set timer. Alarm triggers auto-execute this workflow! mode: once/daily/weekdays/weekly/monthly/interval.', zh: '设置定时器，闹钟触发时自动执行本工作流！mode: once=一次性, daily=每日, weekdays=工作日, weekly=每周指定日, monthly=每月指定日, interval=自定义间隔。time: HH:MM格式触发时间。' },

        // ── Weekday labels ──
        'weekday.1':             { en: 'Mon', zh: '一' },
        'weekday.2':             { en: 'Tue', zh: '二' },
        'weekday.3':             { en: 'Wed', zh: '三' },
        'weekday.4':             { en: 'Thu', zh: '四' },
        'weekday.5':             { en: 'Fri', zh: '五' },
        'weekday.6':             { en: 'Sat', zh: '六' },
        'weekday.7':             { en: 'Sun', zh: '日' },

        // ── CLI Category Labels ──
        'clicat.perception':     { en: 'Perception', zh: '感知' },
        'clicat.search':         { en: 'Search', zh: '搜索' },
        'clicat.file':           { en: 'File Ops', zh: '文件操作' },
        'clicat.document':       { en: 'Document Gen', zh: '文档生成' },
        'clicat.system':         { en: 'System', zh: '系统' },
        'clicat.browser':        { en: 'Browser', zh: '浏览器' },
        'clicat.gui':            { en: 'GUI Desktop', zh: 'GUI 桌面' },
        'clicat.memory':         { en: 'Memory', zh: '记忆' },
            'chat.newConversation':    { en: 'New Conversation', zh: '新对话' },
        'chat.photo':              { en: 'Photo', zh: '照片' },
        'chat.justNow':            { en: 'just now', zh: '刚刚' },
        'chat.minutesAgo':         { en: ' min ago', zh: '分钟前' },
        'chat.hoursAgo':           { en: ' hr ago', zh: '小时前' },
        'chat.daysAgo':            { en: ' days ago', zh: '天前' },
        'chat.expandLines':        { en: ' lines)', zh: ' 行)' },
        'chat.expandPrefix':       { en: 'Expand (', zh: '展开 (' },
        'chat.collapse':           { en: 'Collapse', zh: '折叠' },
        'chat.copy':               { en: 'Copy', zh: '复制' },
        'chat.renameConv':         { en: 'Rename conversation:', zh: '重命名会话:' },
        'chat.serverNonJson':      { en: 'Server returned non-JSON response (', zh: '服务器返回了非JSON响应 (' },
        'theme.switchDark':        { en: 'Switch to dark theme', zh: '切换到夜间主题' },
        'theme.switchLight':       { en: 'Switch to light theme', zh: '切换到日间主题' },
        'settings.reasoningMode':  { en: 'Reasoning mode', zh: '推理模式' },
        'settings.testThisModel':  { en: 'Test this model', zh: '测试此模型' },
        'owner.title2':            { en: 'Owner AICQ Account ID', zh: '主人 AICQ Account ID' },
        'owner.clearOwner2':       { en: 'Clear Owner', zh: '清除主人' },
        'owner.sendFriendReq2':    { en: 'Send friend request', zh: '向主人发起好友请求' },
        'owner.bindOwner':         { en: 'Bind Owner', zh: '绑定主人' },
        'owner.friendMsg2':        { en: 'Hello! I am your AI Agent, I have set you as my owner. Please accept my friend request so I can serve you better.', zh: '你好！我是你的 AI Agent，已将你设为主人，请通过我的好友请求以便更好地为你服务。' },
        'owner.hint2':             { en: 'After setting an owner, the AI Agent will notify them of friend requests etc. Owner binding also prevents zombie cleanup from deleting this Agent.', zh: '设置主人后，AI Agent 会将好友请求等事件通知主人。主人绑定也可以防止僵尸清理删除此 Agent。' },
        'owner.setOwner2':         { en: 'Set Owner', zh: '设置主人' },
        'owner.owner2':            { en: 'Owner', zh: '主人' },
        'wf.secretsTab':           { en: 'Secrets', zh: '密钥管理' },
        'wf.showSecrets':          { en: 'Secrets', zh: '密钥管理' },
        'wf.notRun':               { en: 'Not run', zh: '未运行' },
        'wf.successBadge':         { en: 'Success', zh: '成功' },
        'wf.failedBadge':          { en: 'Failed', zh: '失败' },
        'wf.runningBadge':         { en: 'Running', zh: '运行中' },
        'wf.untitled2':            { en: 'Untitled', zh: '未命名' },
        'wf.stepCount':            { en: 'steps', zh: '步骤' },
        'wf.runCount':             { en: 'runs', zh: '次' },
        'wf.lastRun2':             { en: 'Last: ', zh: '上次: ' },
        'wf.run2':                 { en: 'Run', zh: '运行' },
        'wf.schedule2':            { en: 'Schedule', zh: '定时' },
        'wf.edit2':                { en: 'Edit', zh: '编辑' },
        'wf.stepList':             { en: 'Step List', zh: '步骤列表' },
        'wf.noParams':             { en: 'No params', zh: '无参数' },
        'wf.stepType':             { en: 'Step type', zh: '步骤类型' },
        'wf.selectContinue':       { en: 'Continue next step', zh: '继续下一步' },
        'wf.selectStop':           { en: 'Stop', zh: '停止' },
        'wf.systemPromptOptional': { en: 'System prompt (optional)', zh: '系统提示（可选）' },
        'wf.promptHint':           { en: 'Prompt content, supports {{secrets.NAME}} for secrets, {{steps.N.result}} for step results', zh: '提示内容，支持 {{secrets.NAME}} 引用密钥、{{steps.N.result}} 引用步骤结果' },
        'wf.requestBody':          { en: 'Request body', zh: '请求体' },
        'wf.editStep':             { en: 'Edit Step', zh: '编辑步骤' },
        'wf.enterParamName':       { en: 'Enter param name:', zh: '输入参数名:' },
        'wf.enterParamValue':      { en: 'Enter param value:', zh: '输入参数值:' },
        'wf.runDetail':            { en: 'Run Detail', zh: '运行详情' },
        'wf.noRunHistory':         { en: 'No run history', zh: '暂无运行记录' },
        'wf.recentRun':            { en: 'Recent run: #', zh: '最近运行: #' },
        'wf.executing':            { en: 'Executing...', zh: '正在执行...' },
        'wf.loadFailed2':          { en: 'Load failed', zh: '加载失败' },
        'wf.scheduleFor':          { en: 'Schedule for: ', zh: '定时调度: ' },
        'wf.scheduleMode':         { en: 'Schedule mode *', zh: '调度模式 *' },
        'wf.once2':                { en: 'once — one-time', zh: 'once — 一次性定时' },
        'wf.daily2':               { en: 'daily — every day', zh: 'daily — 每日' },
        'wf.weekdays2':            { en: 'weekdays — weekdays', zh: 'weekdays — 工作日' },
        'wf.weekly2':              { en: 'weekly — weekly', zh: 'weekly — 每周指定日' },
        'wf.monthly2':             { en: 'monthly — monthly', zh: 'monthly — 每月指定日' },
        'wf.interval2':            { en: 'interval — custom', zh: 'interval — 自定义间隔' },
        'wf.triggerTime':          { en: 'Trigger time (HH:MM)', zh: '触发时间 (HH:MM)' },
        'wf.maxTriggers':          { en: 'Max triggers (0=unlimited)', zh: '最大触发次数 (0=无限)' },
        'wf.saveSchedule':         { en: 'Save Schedule', zh: '保存调度' },
        'wf.cancelSchedule':       { en: 'Cancel Schedule', zh: '取消调度' },
        'wf.intervalSec':          { en: 'Interval seconds (min 60)', zh: '间隔秒数 (最小60)' },
        'wf.weekdays3':            { en: 'Weekdays (0=Mon...6=Sun, e.g. 0,2,4)', zh: '星期几 (0=周一...6=周日, 如 0,2,4)' },
        'wf.monthDays':            { en: 'Month days (e.g. 1,15)', zh: '每月几号 (如 1,15)' },
        'wf.name3':                { en: 'Name *', zh: '名称 *' },
        'wf.descPlaceholder2':     { en: 'Brief description (optional)', zh: '简要描述 (可选)' },
        'wf.variables2':           { en: 'Variables (JSON)', zh: '变量 (JSON)' },
        'wf.enterName2':           { en: 'Enter workflow name', zh: '输入工作流名称' },
        'wf.enterCode':            { en: 'Enter code to execute...', zh: '输入要执行的代码...' },
        'wf.addStep2':             { en: 'Add Step', zh: '添加步骤' },
        'secret.title2':           { en: 'Secrets', zh: '密钥管理' },
        'secret.storeHint':        { en: 'Store sensitive info (API keys, tokens, passwords) for workflows. Use {{secrets.NAME}} in steps.', zh: '为工作流存储敏感信息（API密钥、令牌、密码等），步骤中可用 {{secrets.NAME}} 引用' },
        'secret.name2':            { en: 'Secret name', zh: '密钥名称' },
        'secret.namePlaceholder':  { en: 'e.g. API_KEY', zh: '如 API_KEY' },
        'secret.value2':           { en: 'Secret value', zh: '密钥值' },
        'secret.save2':            { en: 'Save Secret', zh: '保存密钥' },
        'secret.empty':            { en: 'No secrets yet, add above', zh: '暂无密钥，在上方添加' },
        'secret.loadFailed2':      { en: 'Failed to load secrets', zh: '加载密钥失败' },
        'alarm.everyDay':          { en: 'Every day ', zh: '每天 ' },
        'alarm.weekdays2':         { en: 'Weekdays ', zh: '工作日 ' },
        'alarm.everyWeek':         { en: 'Every week ', zh: '每周' },
        'alarm.everyMonth':        { en: 'Every month ', zh: '每月' },
        'alarm.everyHour':         { en: ' hours', zh: ' 小时' },
        'alarm.everyMinute':       { en: ' minutes', zh: ' 分钟' },
        'alarm.everySecond':       { en: ' seconds', zh: ' 秒' },
        'alarm.every2':            { en: 'Every ', zh: '每 ' },
        'alarm.pause2':            { en: 'Pause', zh: '暂停' },
        'alarm.resume2':           { en: 'Resume', zh: '恢复' },
        'alarm.delete2':           { en: 'Delete', zh: '删除' },
        'alarm.mode2':             { en: 'Mode', zh: '模式' },
        'alarm.message2':          { en: 'Message', zh: '消息' },
        'alarm.nextTrigger':       { en: 'Next trigger', zh: '下次触发' },
        'alarm.triggered':         { en: 'Triggered', zh: '已触发' },
        'alarm.workflow2':         { en: 'Workflow', zh: '工作流' },
        'alarm.total':             { en: 'Total alarms', zh: '总闹钟数' },
        'alarm.active':            { en: 'Active', zh: '启用中' },
        'alarm.paused2':           { en: 'Paused', zh: '已暂停' },
        'alarm.empty':             { en: 'No alarms yet. Click above to create one.', zh: '暂无闹钟，点击上方按钮创建' },
        'alarm.timeoutRetry':      { en: 'Request timeout, please retry', zh: '请求超时，请重试' },
        'alarm.createAlarm':       { en: '+ New Alarm', zh: '+ 新建闹钟' },
        'alarm.newAlarm':          { en: 'New Alarm', zh: '新建闹钟' },
        'alarm.name2':             { en: 'Name *', zh: '名称 *' },
        'alarm.namePlaceholder':   { en: 'Alarm name', zh: '闹钟名称' },
        'alarm.mode3':             { en: 'Mode *', zh: '模式 *' },
        'alarm.time2':             { en: 'Time (HH:MM)', zh: '时间 (HH:MM)' },
        'alarm.reminderMsg':       { en: 'Reminder message', zh: '提醒消息' },
        'alarm.maxTriggers2':      { en: 'Max triggers (0=unlimited)', zh: '最大触发次数 (0=无限)' },
        'alarm.bindWorkflow':      { en: 'Bind workflow ID (optional)', zh: '绑定工作流ID (可选)' },
        'alarm.bindWorkflowPh':    { en: 'Workflow ID', zh: '工作流ID' },
        'alarm.create2':           { en: 'Create', zh: '创建' },
        'alarm.intervalSec2':      { en: 'Interval seconds', zh: '间隔秒数' },
        'alarm.weekdays4':         { en: 'Weekdays (0=Mon, 1=Tue, ..., 6=Sun)', zh: '星期几 (0=周一, 1=周二, ..., 6=周日)' },
        'alarm.monthDays2':        { en: 'Month days', zh: '每月几号' },
        'skills.desc2':            { en: 'Description', zh: '描述' },
        'skills.no2':              { en: 'None', zh: '无' },
        'skills.version2':         { en: 'Version', zh: '版本' },
        'skills.author2':          { en: 'Author', zh: '作者' },
        'skills.license2':        { en: 'License', zh: '许可证' },
        'skills.securityScan2':    { en: 'Security scan', zh: '安全扫描' },
        'skills.safe2':            { en: 'Safe', zh: '安全' },
        'skills.suspicious2':      { en: 'Suspicious', zh: '可疑' },
        'skills.malicious2':      { en: 'Malicious', zh: '恶意' },
        'skills.unknown2':         { en: 'Unknown', zh: '未知' },
        'skills.downloads2':       { en: 'Downloads', zh: '下载量' },
        'skills.installThis':      { en: 'Install this skill', zh: '安装此技能' },
        'skills.close2':           { en: 'Close', zh: '关闭' },
        'skills.type2':            { en: 'Type', zh: '类型' },
        'skills.category2':        { en: 'Category', zh: '类别' },
        'skills.note2':            { en: 'Note', zh: '备注' },
        'skills.dangerous2':      { en: 'Dangerous', zh: '危险操作' },
        'skills.yes2':             { en: 'Yes', zh: '是' },
        'skills.no3':              { en: 'No', zh: '否' },
        'skills.params2':          { en: 'Parameters', zh: '参数' },
        'skills.paramName2':       { en: 'Name', zh: '名称' },
        'skills.paramType2':       { en: 'Type', zh: '类型' },
        'skills.paramRequired2':   { en: 'Required', zh: '必需' },
        'skills.paramDesc2':       { en: 'Description', zh: '描述' },
        'tts.voiceXiaoxiao':       { en: 'Xiaoxiao (Female)', zh: '晓晓 (女声)' },
        'tts.voiceYunxi':          { en: 'Yunxi (Male)', zh: '云希 (男声)' },
        'tts.voiceYunjian':        { en: 'Yunjian (Male-Mature)', zh: '云健 (男声-成熟)' },
        'tts.testText':            { en: 'Hello, TTS test successful', zh: '你好，TTS测试成功' },
        'tts.edgeTesting':         { en: 'Edge TTS testing...', zh: 'Edge TTS 测试中...' },
        'tts.indexTesting':        { en: 'Index TTS testing...', zh: 'Index TTS 测试中...' },
        'tts.indexTestFailed':     { en: 'Index TTS test failed', zh: 'IndexTTS测试失败' },
        'tts.requestFailed2':      { en: 'TTS request failed', zh: 'TTS请求失败' },
        'tts.dataError':           { en: 'TTS returned abnormal data', zh: 'TTS返回数据异常' },
        'tts.androidFallback2':    { en: 'Edge TTS unavailable, using Android voice test...', zh: 'Edge TTS不可用，使用Android系统语音测试中...' },
        'tts.browserFallback2':    { en: 'Edge TTS unavailable, using browser voice test', zh: 'Edge TTS不可用，使用浏览器语音测试' },
        'tts.androidInitTimeout':  { en: 'Android voice init timeout, trying browser voice', zh: 'Android语音初始化超时，尝试浏览器语音' },
        'tts.androidTTSFailed':    { en: 'Android TTS failed: ', zh: 'Android TTS失败: ' },
        'tts.browserSynthFailed':  { en: 'Browser speech synthesis failed', zh: '浏览器语音合成失败' },
        'tts.switchedBrowser':     { en: 'Switched to browser voice', zh: '已切换到浏览器语音' },
        'tts.ttsUnavailable2':     { en: 'TTS unavailable: Android voice and browser voice not ready', zh: 'TTS不可用：Android语音和浏览器语音均未就绪' },
        'tts.enterCode':           { en: 'Enter code to execute...', zh: '输入要执行的代码...' },
        'cs.enabledWithMode':      { en: 'Customer service mode enabled', zh: '客服模式已启用' },
        'status.commStatus':       { en: 'Communication Status', zh: '通信状态' },
};

    function t(key, args) {
        var entry = I18N[key];
        if (!entry) return key;
        var text = entry[_lang] || entry['en'] || key;
        if (args) {
            if (typeof args === 'string' || typeof args === 'number') {
                text = text.replace('{0}', args);
            } else if (Array.isArray(args)) {
                for (var i = 0; i < args.length; i++) {
                    text = text.replace('{' + i + '}', args[i]);
                }
            }
        }
        return text;
    }

    function setLang(lang) {
        _lang = lang;
        localStorage.setItem('zagent_lang', lang);
        document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
        applyI18nToPage();
    }

    function applyI18nToPage() {
        // Update all elements with data-i18n attribute
        document.querySelectorAll('[data-i18n]').forEach(function(el) {
            var key = el.getAttribute('data-i18n');
            el.textContent = t(key);
        });
        document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
            var key = el.getAttribute('data-i18n-placeholder');
            el.placeholder = t(key);
        });
        document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
            var key = el.getAttribute('data-i18n-title');
            el.title = t(key);
        });
        // Update specific dynamic elements
        var chatTitle = document.getElementById('chat-title');
        if (chatTitle && (!currentConvId || chatTitle.textContent === t('sidebar.newChat') || chatTitle.textContent === t('chat.newConversation'))) {
            chatTitle.textContent = t('sidebar.newChat');
        }
        var chatInput = document.getElementById('chat-input');
        if (chatInput) chatInput.placeholder = t('chat.inputPlaceholder');
        // Update empty state
        var emptyState = document.getElementById('empty-state');
        if (emptyState) {
            emptyState.querySelector('h2').textContent = t('chat.title');
            emptyState.querySelector('p').textContent = t('chat.startConversation');
        }
        // Update conv list empty text
        var convEmpty = document.querySelector('.conv-empty');
        if (convEmpty) convEmpty.textContent = t('sidebar.noConversations');
        // Update sidebar buttons
        updateSidebarLabels();
        // Update header buttons
        var btnTheme = document.getElementById('btn-theme');
        if (btnTheme) btnTheme.title = t('header.toggleTheme');
        var btnTtsToggle = document.getElementById('btn-tts-toggle');
        if (btnTtsToggle) btnTtsToggle.title = ttsEnabled ? t('tts.toggleOff') : t('tts.toggleOn');
        // Update settings dropdown language label
        var langLabel = document.querySelector('.settings-dropdown-lang-label');
        if (langLabel) langLabel.textContent = t('lang.label');
        var langEn = document.querySelector('.settings-dropdown-lang-en');
        if (langEn) langEn.textContent = t('lang.en');
        var langZh = document.querySelector('.settings-dropdown-lang-zh');
        if (langZh) langZh.textContent = t('lang.zh');
        // Update lang check marks
        var langCheckEn = document.querySelector('.settings-dropdown-lang-check-en');
        var langCheckZh = document.querySelector('.settings-dropdown-lang-check-zh');
        if (langCheckEn) langCheckEn.style.display = _lang === 'en' ? '' : 'none';
        if (langCheckZh) langCheckZh.style.display = _lang === 'zh' ? '' : 'none';
    }

    function updateSidebarLabels() {
        var sfTexts = document.querySelectorAll('.sf-text');
        var labels = [t('sidebar.llm'), t('sidebar.systemPrompt'), t('sidebar.aicq'), t('sidebar.skills'), t('sidebar.sites'), t('sidebar.workflows'), t('sidebar.alarms'), t('sidebar.workdir'), t('sidebar.tts'), t('sidebar.customerService')];
        sfTexts.forEach(function(el, i) {
            if (labels[i]) el.textContent = labels[i];
        });
        // Update title attributes
        var sfBtns = [
            { id: 'btn-settings', key: 'sidebar.llm' },
            { id: 'btn-system-prompt', key: 'sidebar.systemPrompt' },
            { id: 'btn-agent-status', key: 'sidebar.aicq' },
            { id: 'btn-skills', key: 'sidebar.skills' },
            { id: 'btn-sites', key: 'sidebar.sites' },
            { id: 'btn-workflows', key: 'sidebar.workflows' },
            { id: 'btn-alarms', key: 'sidebar.alarms' },
            { id: 'btn-files', key: 'sidebar.workdir' },
            { id: 'btn-tts-settings', key: 'sidebar.tts' },
            { id: 'btn-customer-service', key: 'sidebar.customerService' },
        ];
        sfBtns.forEach(function(item) {
            var btn = document.getElementById(item.id);
            if (btn) btn.title = t(item.key);
        });
    }



    // ── Workflow State ────────────────────────────────────────
    let _workflowsCache = [];
    let _workflowStatsCache = {};
    let _editStepsData = [];
    let _wfTabPage = 'workflows';
    let _runPollTimer = null;
    let _savedWfModalHtml = '';
    let _curStepType = '';

    const STEP_TYPES = {
        skill:             {label: t('step.skill'),   icon: '🛠', params: ['name', 'query', 'num', 'url', 'symbol', 'keyword'], required: ['name'],
                             help: t('step.skill.help')},
        code:              {label: t('step.code'),   icon: '📝', params: ['language', 'code', 'timeout'], required: ['code'],
                             help: t('step.code.help')},
        llm:               {label: t('step.llm'), icon: '🤖', params: ['prompt', 'system', 'model', 'temperature', 'max_tokens'], required: ['prompt'],
                             help: t('step.llm.help')},
        shell:             {label: t('step.shell'), icon: '💻', params: ['command', 'timeout'], required: ['command'],
                             help: t('step.shell.help')},
        'browser-open':    {label: t('step.browserOpen'),   icon: '🌐', params: ['url', 'profile'], required: ['url'],
                             help: t('step.browserOpen.help')},
        browser_extract:   {label: t('step.browserExtract'),   icon: '🔍', params: ['selector', 'attribute'], required: ['selector'],
                             help: t('step.browserExtract.help')},
        'browser-screenshot': {label: t('step.browserScreenshot'),   icon: '📸', params: ['save_path'], required: ['save_path'],
                             help: t('step.browserScreenshot.help')},
        delay:             {label: t('step.delay'),       icon: '⏱', params: ['seconds'], required: ['seconds']},
        api_call:          {label: t('step.apiCall'),    icon: '🔗', params: ['url', 'method', 'headers', 'body'], required: ['url'],
                             help: t('step.apiCall.help')},
        condition:         {label: t('step.condition'),   icon: '🔀', params: ['expression', 'on_true', 'on_false'], required: ['expression']},
        send_email:        {label: t('step.sendEmail'),   icon: '📧', params: ['to', 'subject', 'body', 'cc', 'content_type'], required: ['to'],
                             help: t('step.sendEmail.help')},
        timer:             {label: t('step.timer'), icon: '⏰', params: ['mode', 'time', 'weekdays', 'month_days', 'interval_seconds', 'max_triggers', 'label', 'message'], required: ['mode'],
                             help: t('step.timer.help')}
    };

    // ── Alarm State ──────────────────────────────────────────
    const ALARM_MODES = {
        once:      {label:t('alarm.modeOnce'),  icon:'🕐', color:'#c96442'},
        daily:     {label:t('alarm.modeDaily'),    icon:'📅', color:'#6ee7a0'},
        weekdays:  {label:t('alarm.modeWeekdays'),  icon:'💼', color:'#7db8f0'},
        weekly:    {label:t('alarm.modeWeekly'),    icon:'📆', color:'#fcd34d'},
        monthly:   {label:t('alarm.modeMonthly'),    icon:'🗓', color:'#a78bfa'},
        interval:  {label:t('alarm.modeInterval'), icon:'⏱', color:'#f87171'}
    };
    // IMPORTANT: 0=Monday, 1=Tuesday, ..., 6=Sunday (aligned with backend alarm.go)
    const WEEKDAY_LABELS = [t('weekday.1'),t('weekday.2'),t('weekday.3'),t('weekday.4'),t('weekday.5'),t('weekday.6'),t('weekday.7')];
    let _alarmFilterAgent = '';
    let _alarmsData = [];
    let _alarmsStats = {};

    // ── Secrets State ────────────────────────────────────────
    let _secretsAgentFilter = '_global';
    let _secretsCache = [];

    // ── Theme Management ─────────────────────────────────────
    let currentTheme = 'claude';
    const sunIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
    const moonIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';

    function setTheme(themeId) {
        currentTheme = themeId;
        document.documentElement.setAttribute('data-theme', themeId);
        const btn = document.getElementById('btn-theme');
        if (btn) {
            btn.innerHTML = themeId === 'dark' ? moonIcon : sunIcon;
            btn.title = themeId === 'dark' ? t('theme.switchLight') : t('theme.switchDark');
        }
        const hljsLight = document.getElementById('hljs-light');
        const hljsDark = document.getElementById('hljs-dark');
        if (hljsLight && hljsDark) {
            if (themeId === 'dark') {
                hljsLight.disabled = true;
                hljsDark.disabled = false;
            } else {
                hljsLight.disabled = false;
                hljsDark.disabled = true;
            }
        }
        try { localStorage.setItem('aicqbot_theme', themeId); } catch (e) {}
    }

    function loadTheme() {
        let saved = null;
        try { saved = localStorage.getItem('aicqbot_theme'); } catch (e) {}
        if (saved && (saved === 'dark' || saved === 'claude')) {
            setTheme(saved);
        } else {
            setTheme('claude');
        }
    }

    function toggleTheme() {
        setTheme(currentTheme === 'dark' ? 'claude' : 'dark');
    }

    // ── Tool Icons ───────────────────────────────────────────
    const TOOL_ICONS = {
        read_file: '📖', write_file: '✏️', execute_code: '▶️',
        search_files: '🔍', list_directory: '📁', web_search: '🌐',
        web_reader: '📄', generate_image: '🎨', analyze_image: '📷',
        calculate: '📊', command: '💻', remember: '🧠',
        recall: '🔄', search_memory: '🔎', forget: '🗑️',
        get_current_time: '🕐', add_friend: '🤝', list_friends: '👥',
        send_private_message: '📨', send_message: '💬',
        create_group: '👥', send_group_message: '💬',
        broadcast: '📢', send_file: '📎',
        playaudio: '🎵', playvideo: '🎬', read_file_lines: '📄',
    };
    const COLLAPSE_THRESHOLD = Infinity; // Removed collapse button per user request

    // ── Utility ──────────────────────────────────────────────
    function escapeHTML(s) {
        if (!s) return '';
        return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
    }

    function generateId() { return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2,6); }
    // ── API Helper (for sites/workdir management) ───────────────
    async function api(url, options = {}) {
      try {
        const resp = await fetch(url, {
          headers: { 'Content-Type': 'application/json', ...options.headers },
          ...options,
        });
        const rawText = await resp.text();
        let data;
        try {
          data = JSON.parse(rawText);
        } catch (jsonErr) {
          console.error('API response not JSON:', url, resp.status, rawText.substring(0, 200));
          throw new Error(t('chat.serverNonJson') + resp.status + ')');
        }
        if (!resp.ok) throw new Error(data.error || 'HTTP ' + resp.status);
        return data;
      } catch (e) {
        if (e.name === 'AbortError') throw e;
        console.error('API error:', e);
        throw e;
      }
    }

    // escHtml alias for escapeHTML (used in sites/workdir code)
    function escHtml(s) { return escapeHTML(s); }

    // ── Panel Management (desktop: replace chat-main; mobile: close sidebar) ──
    let _activePanelId = null;

    function isMobile() { return window.innerWidth <= 768; }

    function openPanel(panelId, title, actionsHtml, bodyHtml, footerHtml) {
        if (isMobile()) {
            // Mobile: just close sidebar, the dialog overlay still shows
            closeSidebar();
            return false; // Signal to use dialog mode
        }
        // Desktop: show panel in chat-main
        closeSidebar();
        _activePanelId = panelId;

        var chatMain = document.getElementById('chat-main');
        var panelView = document.getElementById('panel-view');
        var panelTitle = document.getElementById('panel-title');
        var panelActions = document.getElementById('panel-actions');
        var panelBody = document.getElementById('panel-body');
        var panelFooter = document.getElementById('panel-footer');

        if (!panelView) return false;

        panelTitle.textContent = title || '';
        panelActions.innerHTML = actionsHtml || '';
        panelBody.innerHTML = bodyHtml || '';
        panelFooter.innerHTML = footerHtml || '';

        chatMain.classList.add('panel-open');
        panelView.classList.remove('hidden');
        return true; // Signal: panel mode active
    }

    function closePanel() {
        var chatMain = document.getElementById('chat-main');
        var panelView = document.getElementById('panel-view');
        var panelBody = document.getElementById('panel-body');
        var panelFooter = document.getElementById('panel-footer');

        // Move dialog-body/dialog-footer back to their original dialog
        if (_activePanelId) {
            var info = getDialogInfo(_activePanelId);
            if (info) {
                var dialogEl = document.getElementById(info.dialogId);
                var dialog = dialogEl ? dialogEl.querySelector('.dialog') : null;
                if (dialog) {
                    var dialogBody = panelBody ? panelBody.querySelector('.dialog-body') : null;
                    var dialogFooter = panelFooter ? panelFooter.querySelector('.dialog-footer') : null;
                    if (dialogBody) dialog.appendChild(dialogBody);
                    if (dialogFooter) dialog.appendChild(dialogFooter);
                }
            }
        }

        if (chatMain) chatMain.classList.remove('panel-open');
        if (panelView) panelView.classList.add('hidden');
        _activePanelId = null;
    }

    // ── Dialog/Panel Intercept ──────────────────────────────────
    // Show a dialog as a panel on desktop, or as overlay on mobile
    function showAsPanel(panelId, title, actionsHtml) {
        if (isMobile()) {
            closeSidebar();
            return false; // use dialog
        }
        // Desktop: open panel, the caller will populate panel-body
        closeSidebar();
        _activePanelId = panelId;

        var chatMain = document.getElementById('chat-main');
        var panelView = document.getElementById('panel-view');
        var panelTitle = document.getElementById('panel-title');
        var panelActions = document.getElementById('panel-actions');

        if (!panelView) return false;

        panelTitle.textContent = title || '';
        panelActions.innerHTML = actionsHtml || '';

        // Clear panel body/footer (caller will populate)
        document.getElementById('panel-body').innerHTML = '';
        document.getElementById('panel-footer').innerHTML = '';

        chatMain.classList.add('panel-open');
        panelView.classList.remove('hidden');
        return true; // panel mode
    }



    // Helper: get dialog body element by panelId mapping
    function getDialogInfo(panelId) {
        var map = {
            'settings': { dialogId: 'settings-dialog', bodyId: 'settings-body', footerId: null },
            'status': { dialogId: 'status-dialog', bodyId: 'status-body', footerId: null },
            'system-prompt': { dialogId: 'system-prompt-dialog', bodyId: 'system-prompt-body', footerId: null },
            'workflows': { dialogId: 'workflows-dialog', bodyId: 'workflows-body', footerId: null },
            'alarms': { dialogId: 'alarms-dialog', bodyId: 'alarms-body', footerId: null },
            'skills': { dialogId: 'skills-dialog', bodyId: 'skills-body', footerId: null },
            'tts': { dialogId: 'tts-settings-dialog', bodyId: 'tts-settings-body', footerId: null },
            'sites': { dialogId: 'sites-dialog', bodyId: 'sites-body', footerId: null },
            'files': { dialogId: 'files-dialog', bodyId: 'files-body', footerId: null },
            'cs': { dialogId: 'cs-dialog', bodyId: 'cs-body', footerId: null },
        };
        return map[panelId] || null;
    }



    function showToast(msg, type) {
        const t = document.getElementById('toast');
        t.textContent = msg;
        t.className = 'toast ' + (type || '');
        setTimeout(() => { t.className = 'toast hidden'; }, 3000);
    }

    function forceScrollToBottom() {
        if (!autoScrollEnabled) return;
        const c = document.getElementById('chat-messages');
        requestAnimationFrame(() => { c.scrollTop = c.scrollHeight; });
    }

    function fmtDate(d) {
        if (!d) return '-';
        try {
            const dt = new Date(d);
            return dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString();
        } catch(e) { return String(d); }
    }

    function fmtTimeAgo(d) {
        if (!d) return '';
        try {
            const diff = Date.now() - new Date(d).getTime();
            if (diff < 60000) return t('chat.justNow');
            if (diff < 3600000) return Math.floor(diff/60000) + t('chat.minutesAgo');
            if (diff < 86400000) return Math.floor(diff/3600000) + t('chat.hoursAgo');
            return Math.floor(diff/86400000) + t('chat.daysAgo');
        } catch(e) { return ''; }
    }

    // ── Markdown Rendering (simplified from IDE) ─────────────
    const _chatCodeBlockStore = [];

    function renderMarkdownLite(text) {
        if (!text) return '';
        const codeBlocks = [];
        _chatCodeBlockStore.length = 0;
        var codeIdx = 0;
        function storeCodeBlock(match, lang) {
            var id = '\x00CODEBLOCK_' + (codeIdx++) + '\x00';
            var code = match.replace(/^```\w*\n?/, '').replace(/\n?```$/, '');
            codeBlocks.push({ lang: lang || '', code: code });
            _chatCodeBlockStore.push({ lang: lang || '', code: code });
            return id;
        }
        text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(m, lang) { return storeCodeBlock(m, lang); });
        text = text.replace(/```([\s\S]*?)```/g, function(m) { return storeCodeBlock(m, ''); });

        const inlineCodes = [];
        var inlineIdx = 0;
        text = text.replace(/`([^`\n]+)`/g, function(m, code) {
            var id = '\x00INLINE_' + (inlineIdx++) + '\x00';
            inlineCodes.push(code);
            return id;
        });

        let html = escapeHTML(text);
        html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
        html = html.replace(/^\s{0,3}###\s*(.+)$/gm, '<h3>$1</h3>');
        html = html.replace(/^\s{0,3}##\s+(.+)$/gm, '<h2>$1</h2>');
        html = html.replace(/^\s{0,3}#\s+(.+)$/gm, '<h1>$1</h1>');
        html = html.replace(/^[-*]{3,}$/gm, '<hr>');
        html = html.replace(/^&gt;\s?(.+)$/gm, '<blockquote>$1</blockquote>');
        html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
        html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
        html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
        html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');

        html = html.replace(/\x00INLINE_(\d+)\x00/g, function(_, idx) {
            return '<code>' + escapeHTML(inlineCodes[parseInt(idx)]) + '</code>';
        });

        html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, function(_, idx) {
            const block = codeBlocks[parseInt(idx)];
            const escapedCode = escapeHTML(block.code);
            const copyBtn = '<button class="code-copy-btn" data-raw-idx="' + idx + '" title="Copy">📋</button>';
            const langLabel = block.lang ? '<span class="code-lang">' + escapeHTML(block.lang) + '</span>' : '';
            return '<div class="code-block-wrapper">' + langLabel + copyBtn + '<pre><code>' + escapedCode + '</code></pre></div>';
        });

        // GFM Tables
        html = html.replace(/((?:^\|[^\n]+\|$\n?)+)/gm, function(tableBlock) {
            var rows = tableBlock.trim().split('\n');
            if (rows.length < 2) return tableBlock;
            var sepLine = rows[1].trim();
            if (!/^[\|:\-\s]+$/.test(sepLine) || sepLine.indexOf('-') === -1) return tableBlock;
            var result = '<table><thead><tr>';
            var headerCells = rows[0].split('|').filter(function(c) { return c.trim() !== ''; });
            headerCells.forEach(function(cell) { result += '<th>' + cell.trim() + '</th>'; });
            result += '</tr></thead>';
            if (rows.length > 2) {
                result += '<tbody>';
                for (var ri = 2; ri < rows.length; ri++) {
                    var row = rows[ri].trim();
                    if (!row) continue;
                    result += '<tr>';
                    var cells = row.split('|').filter(function(c) { return c.trim() !== ''; });
                    cells.forEach(function(cell) { result += '<td>' + cell.trim() + '</td>'; });
                    result += '</tr>';
                }
                result += '</tbody>';
            }
            result += '</table>';
            return result;
        });

        html = html.replace(/\n{2,}/g, '</p><p>');
        html = html.replace(/\n/g, '<br>');
        if (!html.startsWith('<')) html = '<p>' + html + '</p>';
        return html;
    }

    function renderChatMarkdown(element, text) {
        if (!element) return;
        var isStreaming = element.classList && element.classList.contains('chat-streaming');
        if (isStreaming) {
            var clone = document.createElement('div');
            clone.innerHTML = renderMarkdownLite(text);
            if (typeof renderMathInElement !== 'undefined') {
                try {
                    renderMathInElement(clone, {
                        delimiters: [
                            {left: '$$', right: '$$', display: true},
                            {left: '$', right: '$', display: false},
                            {left: '\\(', right: '\\)', display: false},
                            {left: '\\[', right: '\\]', display: true}
                        ],
                        throwOnError: false
                    });
                } catch(_e) {}
            }
            element.innerHTML = clone.innerHTML;
        } else {
            element.innerHTML = renderMarkdownLite(text);
            if (typeof renderMathInElement !== 'undefined') {
                try { renderMathInElement(element, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\(',right:'\\)',display:false},{left:'\\[',right:'\\]',display:true}], throwOnError: false }); } catch(_e) {}
            }
        }
    }

    // ── Message Rendering ────────────────────────────────────
    function addUserMessage(content) {
        const container = document.getElementById('chat-messages');
        const el = document.createElement('div');
        el.className = 'chat-msg user';
        el.id = generateId();
        el.innerHTML = '<div class="msg-role">You</div><div class="msg-content">' + renderMarkdownLite(content) + '</div>';
        container.appendChild(el);
        addMessageActions(el);
        forceScrollToBottom();
    }

    function addAssistantMessage() {
        const container = document.getElementById('chat-messages');
        const el = document.createElement('div');
        el.className = 'chat-msg assistant chat-streaming';
        el.id = generateId();
        el.innerHTML = '<div class="msg-role">ZAgent</div><div class="msg-content"></div>';
        container.appendChild(el);
        currentStreamEl = el.querySelector('.msg-content');
        streamBuffer = '';
        addMessageActions(el);
        forceScrollToBottom();
        return el;
    }

    function showToolProgress(toolName, args) {
        const container = document.getElementById('chat-messages');
        const el = document.createElement('div');
        el.className = 'chat-msg tool tool-progress';
        el.id = generateId();
        const icon = TOOL_ICONS[toolName] || '🔧';
        const argsStr = args ? formatToolArgs(args) : '';
        el.innerHTML = '<div class="tool-header"><span class="tool-name">' + icon + ' ' + escapeHTML(toolName) + '</span><span class="tool-spinner">⏳</span></div>' +
            (argsStr ? '<div class="tool-args">' + escapeHTML(argsStr) + '</div>' : '') +
            '<div class="tool-result tool-waiting">Executing...</div>';
        el._toolStartTime = Date.now();
        el._toolName = toolName;
        container.appendChild(el);
        forceScrollToBottom();
        return el;
    }

    function formatToolArgs(args) {
        if (!args) return '';
        if (typeof args === 'string') return args;
        try { return JSON.stringify(args, null, 2); } catch(e) { return String(args); }
    }

    function finalizeToolResult(toolEl, result, ok) {
        if (!toolEl) return;
        const duration = Date.now() - (toolEl._toolStartTime || Date.now());
        const spinner = toolEl.querySelector('.tool-spinner');
        if (spinner) {
            spinner.className = ok ? 'tool-status tool-ok' : 'tool-status tool-fail';
            spinner.textContent = ok ? '✓' : '✗';
        }
        const resultEl = toolEl.querySelector('.tool-result');
        if (resultEl) {
            resultEl.className = 'tool-result';
            resultEl.classList.remove('tool-waiting');
            const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
            // Add duration badge next to status
            const headerEl = toolEl.querySelector('.tool-header');
            if (headerEl) {
                const durEl = document.createElement('span');
                durEl.style.cssText = 'margin-left:auto;color:var(--text-muted);font-size:11px;';
                durEl.textContent = (duration/1000).toFixed(1) + 's';
                headerEl.appendChild(durEl);
            }
            // Show result with collapse/expand for long results
            renderToolResult(resultEl, resultStr);
        }
    }

    function renderToolResult(resultEl, resultStr) {
        resultEl.innerHTML = '';
        // Try to pretty-format JSON for better readability
        let displayStr = resultStr;
        try {
            const parsed = JSON.parse(resultStr);
            displayStr = JSON.stringify(parsed, null, 2);
        } catch(e) {
            // Not JSON, use as-is
        }
        const lines = displayStr.split('\n');
        const COLLAPSE_THRESHOLD = 5;
        if (lines.length > COLLAPSE_THRESHOLD) {
            const previewLines = lines.slice(0, COLLAPSE_THRESHOLD).join('\n');
            const preEl = document.createElement('pre');
            preEl.className = 'tool-result-preview';
            preEl.textContent = previewLines + '\n...';
            resultEl.appendChild(preEl);

            const fullPre = document.createElement('pre');
            fullPre.className = 'tool-result-full';
            fullPre.style.display = 'none';
            fullPre.textContent = displayStr;
            resultEl.appendChild(fullPre);

            const toggleBtn = document.createElement('button');
            toggleBtn.className = 'tool-toggle-btn';
            toggleBtn.textContent = t('chat.expandPrefix') + lines.length + t('chat.expandLines');
            toggleBtn.addEventListener('click', function() {
                if (fullPre.style.display === 'none') {
                    fullPre.style.display = 'block';
                    preEl.style.display = 'none';
                    toggleBtn.textContent = t('chat.collapse');
                } else {
                    fullPre.style.display = 'none';
                    preEl.style.display = 'block';
                    toggleBtn.textContent = t('chat.expandPrefix') + lines.length + t('chat.expandLines');
                }
            });
            resultEl.appendChild(toggleBtn);
        } else {
            const preEl = document.createElement('pre');
            preEl.textContent = displayStr;
            resultEl.appendChild(preEl);
        }
    }

    function appendToolOutput(toolEl, chunk) {
        if (!toolEl) return;
        const resultEl = toolEl.querySelector('.tool-result');
        if (!resultEl) return;
        if (resultEl.classList.contains('tool-waiting')) {
            resultEl.classList.remove('tool-waiting');
            resultEl.textContent = chunk;
        } else {
            resultEl.textContent += chunk;
        }
        forceScrollToBottom();
    }

    function showThinkingStatus(message) {
        const container = document.getElementById('chat-messages');
        // Remove existing thinking element if any
        if (thinkingEl) {
            thinkingEl.remove();
        }
        thinkingEl = document.createElement('div');
        thinkingEl.className = 'chat-msg thinking-status';
        thinkingEl.innerHTML = '<div class="msg-content thinking-content">' +
            '<span class="thinking-spinner">⏳</span> ' +
            '<span class="thinking-text">' + escapeHTML(message) + '</span></div>';
        container.appendChild(thinkingEl);
        forceScrollToBottom();
    }

    function hideThinkingStatus() {
        if (thinkingEl) {
            thinkingEl.remove();
            thinkingEl = null;
        }
    }

    function addErrorMessage(errorText) {
        const container = document.getElementById('chat-messages');
        const el = document.createElement('div');
        el.className = 'chat-msg error';
        el.innerHTML = '<div class="msg-role">Error</div><div class="msg-content">' + escapeHTML(errorText) + '</div>';
        container.appendChild(el);
        forceScrollToBottom();
    }

    // ── Thinking Status Styles (injected once)
    if (!document.getElementById('thinking-status-styles')) {
        var styleEl = document.createElement('style');
        styleEl.id = 'thinking-status-styles';
        styleEl.textContent = `
            .chat-msg.thinking-status {
                display: flex;
                justify-content: center;
                padding: 8px 16px;
                margin: 4px 0;
            }
            .thinking-content {
                display: inline-flex;
                align-items: center;
                gap: 8px;
                padding: 6px 14px;
                border-radius: 16px;
                background: var(--bg-hover, #f0f0f0);
                color: var(--text-muted, #888);
                font-size: 13px;
                animation: thinking-pulse 1.5s ease-in-out infinite;
            }
            .thinking-spinner {
                animation: thinking-spin 1s linear infinite;
            }
            @keyframes thinking-spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }
            @keyframes thinking-pulse {
                0%, 100% { opacity: 0.7; }
                50% { opacity: 1; }
            }
        `;
        document.head.appendChild(styleEl);
    }

    // ── Stop Button ──────────────────────────────────────────
    function showStopButton() {
        document.getElementById('chat-stop-row').classList.remove('hidden');
    }
    function hideStopButton() {
        document.getElementById('chat-stop-row').classList.add('hidden');
    }

    // ── SSE Streaming ────────────────────────────────────────
    async function sendMessage() {
        const input = document.getElementById('chat-input');
        const message = input.value.trim();
        if (!message && pendingAttachments.length === 0) return;
        if (isProcessing) {
            showToast(t('chat.taskRunning'), 'warning');
            return;
        }

        input.value = '';
        input.style.height = 'auto';

        // Build full content with attachments
        let fullContent = message;
        if (pendingAttachments.length > 0) {
            const images = pendingAttachments.filter(a => a.is_image);
            const files = pendingAttachments.filter(a => !a.is_image);
            if (images.length) {
                fullContent += '\n\n' + images.map(a => '![image](' + a.url + ')').join('\n');
            }
            if (files.length) {
                fullContent += '\n\n' + files.map(a => '[' + a.filename + '](' + a.url + ')').join('\n');
            }
        }

        // Build message for API (with attachment metadata)
        let apiMessage = message;
        if (pendingAttachments.length > 0) {
            const images = pendingAttachments.filter(a => a.is_image);
            const files = pendingAttachments.filter(a => !a.is_image);
            if (images.length) {
                apiMessage += '\n\n[Images: ' + images.map(a => a.url).join(', ') + ']';
            }
            if (files.length) {
                apiMessage += '\n\n[Files: ' + files.map(a => a.filename + ' (' + a.url + ')').join(', ') + ']';
            }
        }

        // Clear attachments
        pendingAttachments = [];
        renderAttachments();

        if (!currentConvId) {
            currentConvId = 'conv_' + Date.now();
            var autoTitle = message.length > 15 ? message.substring(0, 15) + '...' : message;
            const resp = await fetch('/api/conversations', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: autoTitle })
            });
            const data = await resp.json();
            currentConvId = data.id;
            try { localStorage.setItem('aicqbot_last_conv_id', currentConvId); } catch(_e) {}
            document.getElementById('chat-title').textContent = autoTitle;
            loadConversations();
        }

        const emptyState = document.getElementById('empty-state');
        if (emptyState) emptyState.style.display = 'none';

        addUserMessage(fullContent);

        isProcessing = true;
        showStopButton();
        document.getElementById('btn-send').disabled = true;

        const assistantEl = addAssistantMessage();

        // Note: title is already set when creating the conversation, no need to PATCH again

        try {
            currentAbortController = new AbortController();
            const modelSelect = document.getElementById('chat-model-select');
            const modelIndex = modelSelect.value ? parseInt(modelSelect.value) : 0;

            const resp = await fetch('/api/chat/send/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    message: apiMessage,
                    conv_id: currentConvId,
                    model_index: modelIndex
                }),
                signal: currentAbortController.signal
            });

            if (!resp.ok) {
                const errData = await resp.json().catch(() => ({ error: 'Request failed' }));
                if (resp.status === 409) {
                    // "A task is already running" — auto force-stop and retry
                    isProcessing = false;
                    hideStopButton();
                    document.getElementById('btn-send').disabled = false;
                    // Remove the empty assistant message we just added
                    if (assistantEl && assistantEl.parentNode) assistantEl.parentNode.removeChild(assistantEl);
                    // Force stop the stuck task (double-stop clears Running flag)
                    await fetch('/api/chat/task/stop', { method: 'POST' }).catch(() => {});
                    await fetch('/api/chat/task/stop', { method: 'POST' }).catch(() => {});
                    await new Promise(r => setTimeout(r, 300));
                    showToast(t('chat.taskAutoStopped'), 'info');
                    sendMessage();
                    return;
                }
                throw new Error(errData.error || 'HTTP ' + resp.status);
            }

            const reader = resp.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let currentToolEl = null;

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                const lines = buffer.split('\n');
                buffer = lines.pop();

                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const jsonStr = line.substring(6);
                    try {
                        const parsed = JSON.parse(jsonStr);
                        if (parsed.type === 'text_end') {
                            if (assistantEl) {
                                assistantEl.classList.remove('chat-streaming');
                            }
                            currentStreamEl = null;
                            streamBuffer = '';
                        }
                        else if (parsed.type === 'text_start') {
                            // Reuse existing empty assistant element if available,
                            // otherwise create a new one. This prevents duplicate
                            // empty bubbles when sendMessage already created one.
                            if (currentStreamEl && currentStreamEl.textContent.trim() === '') {
                                // Reuse: the initial assistantEl from sendMessage is empty
                                streamBuffer = '';
                            } else {
                                // Create new: previous section was closed (text_end)
                                assistantEl = addAssistantMessage();
                            }
                        }
                        else if (parsed.type === 'thinking') {
                            if (parsed.content) {
                                var retryInfo = '';
                                if (parsed.retry && parsed.max_retry) {
                                    retryInfo = ' (retry ' + parsed.retry + '/' + parsed.max_retry + ')';
                                }
                                showThinkingStatus(parsed.content + retryInfo);
                            }
                        }
                        else if (parsed.type === 'api_status') {
                            if (parsed.retries || parsed.last_error) {
                                var parts = [];
                                if (parsed.retries) parts.push(parsed.retries + ' retries');
                                if (parsed.last_error) parts.push(parsed.last_error);
                                showThinkingStatus('API: ' + parts.join(', '));
                            }
                        }
                        else if (parsed.type === 'tool_start') {
                            hideThinkingStatus();
                            currentToolEl = showToolProgress(parsed.tool, parsed.args);
                        }
                        else if (parsed.type === 'tool_result') {
                            if (currentToolEl) {
                                finalizeToolResult(currentToolEl, parsed.result, parsed.ok);
                                currentToolEl = null;
                            }
                        }
                        else {
                            handleSSEEvent(parsed, assistantEl, currentToolEl);
                        }
                    } catch(e) {}
                }
            }
        } catch(err) {
            if (err.name !== 'AbortError') {
                addErrorMessage(err.message);
            }
        }

        isProcessing = false;
        hideThinkingStatus();
        hideStopButton();
        document.getElementById('btn-send').disabled = false;
        currentAbortController = null;
        if (assistantEl) {
            assistantEl.classList.remove('chat-streaming');
            // Remove empty assistant messages (LLM returned no text content)
            var contentEl = assistantEl.querySelector('.msg-content');
            if (contentEl && contentEl.textContent.trim() === '' && !assistantEl.querySelector('.tool-header')) {
                assistantEl.remove();
            }
            // Auto-read assistant message if TTS is enabled
            if (ttsEnabled && ttsAutoRead) {
                speakMessage(assistantEl);
            }
        }
        loadConversations();
    }

    function handleSSEEvent(event, assistantEl, currentToolEl) {
        switch (event.type) {
            case 'text':
                hideThinkingStatus();
                streamBuffer += event.content;
                if (currentStreamEl) {
                    renderChatMarkdown(currentStreamEl, streamBuffer);
                }
                forceScrollToBottom();
                break;
            case 'tool_start':
                hideThinkingStatus();
                break;
            case 'tool_output':
                if (currentToolEl) appendToolOutput(currentToolEl, event.content);
                break;
            case 'tool_result':
                break;
            case 'system':
                if (event.content) {
                    showThinkingStatus(event.content);
                }
                break;
            case 'thinking':
                if (event.content) {
                    var retryInfo = '';
                    if (event.retry && event.max_retry) {
                        retryInfo = ' (retry ' + event.retry + '/' + event.max_retry + ')';
                    }
                    showThinkingStatus(event.content + retryInfo);
                }
                break;
            case 'api_status':
                if (event.retries || event.last_error) {
                    var parts = [];
                    if (event.retries) parts.push(event.retries + ' retries');
                    if (event.last_error) parts.push(event.last_error);
                    showThinkingStatus('API: ' + parts.join(', '));
                }
                break;
            case 'error':
                addErrorMessage(event.content);
                break;
            case 'done':
                hideThinkingStatus();
                if (currentStreamEl && streamBuffer) {
                    renderChatMarkdown(currentStreamEl, streamBuffer);
                }
                // Clean up empty assistant messages (no content was rendered)
                document.querySelectorAll('.chat-msg.assistant.chat-streaming').forEach(function(el) {
                    var contentEl = el.querySelector('.msg-content');
                    if (!contentEl || contentEl.textContent.trim() === '') {
                        el.remove();
                    }
                });
                break;
            case 'cancelled':
                showToast(t('chat.taskCancelled'), 'error');
                break;
            case 'alarm':
                // Alarm triggered — show notification
                var alarmMsg = event.content || '⏰ Alarm triggered';
                showToast(alarmMsg, 'info');
                // Also add as a system message in the chat
                addErrorMessage('⏰ ' + alarmMsg);
                break;
            case 'keepalive':
                break;
        }
    }

    function abortGeneration() {
        // Immediately reset frontend state so user can send new messages
        isProcessing = false;
        hideThinkingStatus();
        hideStopButton();
        document.getElementById('btn-send').disabled = false;
        if (currentAbortController) {
            currentAbortController.abort();
            currentAbortController = null;
        }
        // Notify server to stop/cancel the running task
        fetch('/api/chat/task/stop', { method: 'POST' }).catch(() => {});
    }

    // ── File Upload (direct send, no preview) ─────────────────
    function handleFileSelectAndSend(files) {
        if (!files || files.length === 0) return;
        for (const file of files) {
            const formData = new FormData();
            formData.append('file', file);
            fetch('/api/files/upload', {
                method: 'POST',
                body: formData
            }).then(function(r) { return r.json(); }).then(function(data) {
                if (data.status === 'uploaded') {
                    // Directly send without preview
                    sendFileDirectly(data);
                } else {
                    showToast(t('upload.failed') + ': ' + (data.error || t('wf.unknownError')), 'error');
                }
            }).catch(function(e) {
                showToast(t('upload.error') + ': ' + e.message, 'error');
            });
        }
    }

    // Send a file directly as a chat message (no preview step)
    async function sendFileDirectly(fileData) {
        if (isProcessing) {
            showToast(t('upload.taskRunning'), 'warning');
            return;
        }

        // Build message content
        let fullContent = '';
        let apiMessage = '';
        if (fileData.is_image) {
            fullContent = '![image](' + fileData.url + ')';
            apiMessage = '[Images: ' + fileData.url + ']';
        } else {
            fullContent = '[' + fileData.filename + '](' + fileData.url + ')';
            apiMessage = '[Files: ' + fileData.filename + ' (' + fileData.url + ')]';
        }

        // Create conversation if needed
        if (!currentConvId) {
            currentConvId = 'conv_' + Date.now();
            var autoTitle = fileData.is_image ? t('chat.photo') : fileData.filename;
            if (autoTitle.length > 15) autoTitle = autoTitle.substring(0, 15) + '...';
            try {
                const resp = await fetch('/api/conversations', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ title: autoTitle })
                });
                const data = await resp.json();
                currentConvId = data.id;
                try { localStorage.setItem('aicqbot_last_conv_id', currentConvId); } catch(_e) {}
                document.getElementById('chat-title').textContent = autoTitle;
                loadConversations();
            } catch(e) {
                console.error('Failed to create conversation:', e);
            }
        }

        const emptyState = document.getElementById('empty-state');
        if (emptyState) emptyState.style.display = 'none';

        addUserMessage(fullContent);
        isProcessing = true;
        showStopButton();
        document.getElementById('btn-send').disabled = true;

        const assistantEl = addAssistantMessage();

        try {
            currentAbortController = new AbortController();
            const modelSelect = document.getElementById('chat-model-select');
            const modelIndex = modelSelect.value ? parseInt(modelSelect.value) : 0;

            const resp = await fetch('/api/chat/send/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    message: apiMessage,
                    conv_id: currentConvId,
                    model_index: modelIndex
                }),
                signal: currentAbortController.signal
            });

            if (!resp.ok) {
                const errData = await resp.json().catch(() => ({ error: 'Request failed' }));
                if (resp.status === 409) {
                    isProcessing = false;
                    hideStopButton();
                    document.getElementById('btn-send').disabled = false;
                    if (assistantEl && assistantEl.parentNode) assistantEl.parentNode.removeChild(assistantEl);
                    await fetch('/api/chat/task/stop', { method: 'POST' }).catch(() => {});
                    await fetch('/api/chat/task/stop', { method: 'POST' }).catch(() => {});
                    await new Promise(r => setTimeout(r, 300));
                    showToast(t('chat.taskAutoStopped'), 'info');
                    sendFileDirectly(fileData);
                    return;
                }
                throw new Error(errData.error || 'HTTP ' + resp.status);
            }

            const reader = resp.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let currentToolEl = null;

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                const lines = buffer.split('\n');
                buffer = lines.pop();

                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const jsonStr = line.substring(6);
                    try {
                        const parsed = JSON.parse(jsonStr);
                        if (parsed.type === 'text_end') {
                            if (assistantEl) {
                                assistantEl.classList.remove('chat-streaming');
                            }
                            currentStreamEl = null;
                            streamBuffer = '';
                        }
                        else if (parsed.type === 'text_start') {
                            if (currentStreamEl && currentStreamEl.textContent.trim() === '') {
                                streamBuffer = '';
                            } else {
                                assistantEl = addAssistantMessage();
                            }
                        }
                        else if (parsed.type === 'thinking') {
                            if (parsed.content) showThinkingStatus(parsed.content);
                        }
                        else if (parsed.type === 'api_status') {
                            if (parsed.retries || parsed.last_error) {
                                var parts = [];
                                if (parsed.retries) parts.push(parsed.retries + ' retries');
                                if (parsed.last_error) parts.push(parsed.last_error);
                                showThinkingStatus('API: ' + parts.join(', '));
                            }
                        }
                        else if (parsed.type === 'tool_start') {
                            hideThinkingStatus();
                            currentToolEl = showToolProgress(parsed.tool, parsed.args);
                        }
                        else if (parsed.type === 'tool_result') {
                            if (currentToolEl) {
                                finalizeToolResult(currentToolEl, parsed.result, parsed.ok);
                                currentToolEl = null;
                            }
                        }
                        else {
                            handleSSEEvent(parsed, assistantEl, currentToolEl);
                        }
                    } catch(e) {}
                }
            }
        } catch(err) {
            if (err.name !== 'AbortError') {
                addErrorMessage(err.message);
            }
        }

        isProcessing = false;
        hideThinkingStatus();
        hideStopButton();
        document.getElementById('btn-send').disabled = false;
        currentAbortController = null;
        if (assistantEl) {
            assistantEl.classList.remove('chat-streaming');
            var contentEl = assistantEl.querySelector('.msg-content');
            if (contentEl && contentEl.textContent.trim() === '' && !assistantEl.querySelector('.tool-header')) {
                assistantEl.remove();
            }
            if (ttsEnabled && ttsAutoRead) {
                speakMessage(assistantEl);
            }
        }
        loadConversations();
    }

    function renderAttachments() {
        const container = document.getElementById('attachment-preview');
        if (!container) return;
        if (pendingAttachments.length === 0) {
            container.style.display = 'none';
            container.innerHTML = '';
            return;
        }
        container.style.display = 'flex';
        let html = '';
        pendingAttachments.forEach(function(att, i) {
            if (att.is_image) {
                html += '<div class="attachment-item"><img src="' + escapeHTML(att.url) + '" class="attachment-thumb" title="' + escapeHTML(att.filename) + '"><button class="attachment-remove" data-idx="' + i + '">✕</button></div>';
            } else {
                html += '<div class="attachment-item"><span class="attachment-file-icon">📎</span><span class="attachment-name">' + escapeHTML(att.filename) + '</span><button class="attachment-remove" data-idx="' + i + '">✕</button></div>';
            }
        });
        container.innerHTML = html;
    }

    function removeAttachment(idx) {
        pendingAttachments.splice(idx, 1);
        renderAttachments();
    }

    // ── Conversation Management ──────────────────────────────
    async function loadConversations() {
        try {
            const resp = await fetch('/api/conversations');
            conversations = await resp.json();
            renderConvList();
        } catch(e) {
            console.error('Failed to load conversations:', e);
        }
    }

    function renderConvList() {
        const list = document.getElementById('conv-list');
        if (!list) return;
        if (!conversations || conversations.length === 0) {
            list.innerHTML = '<div class="conv-empty">No conversations yet</div>';
            return;
        }
        list.innerHTML = '';
        conversations.forEach(function(conv) {
            const item = document.createElement('div');
            item.className = 'conv-item' + (conv.id === currentConvId ? ' active' : '');
            item.innerHTML = '<span class="conv-item-title">' + escapeHTML(conv.title || t('chat.untitled')) + '</span>' +
                '<button class="conv-item-delete" data-id="' + conv.id + '">🗑️</button>';

            item.addEventListener('click', function(e) {
                if (e.target.classList.contains('conv-item-delete')) {
                    deleteConversation(e.target.dataset.id);
                } else {
                    loadConversation(conv.id);
                }
            });

            // Right-click context menu for rename
            item.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                e.stopPropagation();
                showConvContextMenu(e, conv.id, conv.title);
            });
            list.appendChild(item);
        });
    }

    // ── Conversation Context Menu (Right-Click) ────────────────
    function showConvContextMenu(e, convId, currentTitle) {
        // Remove any existing context menu
        var existingMenu = document.getElementById('conv-ctx-menu');
        if (existingMenu) existingMenu.remove();

        var menu = document.createElement('div');
        menu.id = 'conv-ctx-menu';
        menu.className = 'conv-ctx-menu';
        menu.innerHTML =
            '<div class="conv-ctx-item" data-action="rename">✏️ ' + t('chat.rename') + '</div>' +
            '<div class="conv-ctx-item" data-action="delete">🗑️ ' + t('chat.deleteConv') + '</div>';

        // Position the menu at cursor
        var x = e.clientX;
        var y = e.clientY;
        // Adjust if near edges
        menu.style.left = x + 'px';
        menu.style.top = y + 'px';
        document.body.appendChild(menu);

        // Ensure menu stays within viewport
        requestAnimationFrame(function() {
            var rect = menu.getBoundingClientRect();
            if (rect.right > window.innerWidth) {
                menu.style.left = (x - rect.width) + 'px';
            }
            if (rect.bottom > window.innerHeight) {
                menu.style.top = (y - rect.height) + 'px';
            }
        });

        // Handle menu item clicks
        menu.addEventListener('click', function(ev) {
            var action = ev.target.dataset.action;
            if (action === 'rename') {
                renameConversation(convId, currentTitle);
            } else if (action === 'delete') {
                deleteConversation(convId);
            }
            menu.remove();
        });

        // Close menu on click outside or scroll
        function closeMenu() {
            var m = document.getElementById('conv-ctx-menu');
            if (m) m.remove();
            document.removeEventListener('click', closeMenu);
            document.removeEventListener('scroll', closeMenu, true);
        }
        setTimeout(function() {
            document.addEventListener('click', closeMenu);
            document.addEventListener('scroll', closeMenu, true);
        }, 10);
    }

    // ── Rename Conversation ────────────────────────────────────
    function renameConversation(convId, currentTitle) {
        // Create inline rename input
        var convItems = document.querySelectorAll('.conv-item');
        var targetItem = null;
        convItems.forEach(function(item) {
            var titleEl = item.querySelector('.conv-item-title');
            if (titleEl && titleEl.textContent === (currentTitle || 'Untitled')) {
                targetItem = item;
            }
        });

        if (!targetItem) {
            // Fallback: use prompt
            var newTitle = prompt(t('chat.renameConv'), currentTitle || '');
            if (newTitle !== null && newTitle.trim() !== '') {
                doRenameConversation(convId, newTitle.trim());
            }
            return;
        }

        // Replace title with input field
        var titleEl = targetItem.querySelector('.conv-item-title');
        var originalTitle = titleEl.textContent;
        var input = document.createElement('input');
        input.type = 'text';
        input.value = currentTitle || '';
        input.className = 'conv-rename-input';
        input.style.cssText = 'width:100%;padding:2px 6px;border:1px solid var(--accent);border-radius:3px;background:var(--bg-input);color:var(--text-primary);font-size:13px;outline:none;';

        titleEl.replaceWith(input);
        input.focus();
        input.select();

        function finishRename() {
            var newTitle = input.value.trim();
            if (newTitle && newTitle !== currentTitle) {
                doRenameConversation(convId, newTitle);
            } else {
                // Restore original title
                var newTitleEl = document.createElement('span');
                newTitleEl.className = 'conv-item-title';
                newTitleEl.textContent = originalTitle;
                input.replaceWith(newTitleEl);
            }
        }

        input.addEventListener('keydown', function(ev) {
            if (ev.key === 'Enter') {
                ev.preventDefault();
                finishRename();
            } else if (ev.key === 'Escape') {
                var newTitleEl = document.createElement('span');
                newTitleEl.className = 'conv-item-title';
                newTitleEl.textContent = originalTitle;
                input.replaceWith(newTitleEl);
            }
        });

        input.addEventListener('blur', function() {
            // Small delay to allow click events to fire
            setTimeout(finishRename, 150);
        });
    }

    async function doRenameConversation(convId, newTitle) {
        try {
            await fetch('/api/conversations/' + convId, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: newTitle })
            });
            // Update chat header if this is the current conversation
            if (convId === currentConvId) {
                document.getElementById('chat-title').textContent = newTitle;
            }
            // Update the title in conversations list
            if (conversations) {
                conversations.forEach(function(c) {
                    if (c.id === convId) c.title = newTitle;
                });
            }
            renderConvList();
            showToast(t('chat.convRenamed'), 'success');
        } catch(e) {
            showToast(t('chat.renameFailed'), 'error');
            renderConvList();
        }
    }

    async function loadConversation(id) {
        try {
            // Reset pagination state
            hasMoreMessages = false;
            oldestMsgTimestamp = null;
            isLoadingMore = false;

            // Load with pagination: only fetch the latest PAGE_SIZE messages
            const resp = await fetch('/api/conversations/' + id + '?limit=' + PAGE_SIZE);
            const conv = await resp.json();
            currentConvId = conv.id;
            try { localStorage.setItem('aicqbot_last_conv_id', currentConvId); } catch(_e) {}
            document.getElementById('chat-title').textContent = conv.title || t('chat.untitled');
            messages = conv.messages || [];
            hasMoreMessages = conv.has_more || false;

            // Track oldest message timestamp for loading more
            if (messages.length > 0) {
                oldestMsgTimestamp = messages[0].timestamp || null;
            }

            renderMessages(messages);
            renderConvList();

            // Setup scroll listener for loading more messages
            setupScrollPagination();
        } catch(e) {
            showToast(t('chat.failedLoadConv'), 'error');
        }
    }

    // ── Scroll-based Pagination ──────────────────────────────────
    function setupScrollPagination() {
        const container = document.getElementById('chat-messages');
        if (!container) return;

        // Remove existing scroll listener (if any)
        container.removeEventListener('scroll', handleScrollPagination);

        // Add scroll listener
        container.addEventListener('scroll', handleScrollPagination);
    }

    function handleScrollPagination() {
        const container = document.getElementById('chat-messages');
        if (!container) return;

        // When scrolled near the top, load more messages
        if (container.scrollTop <= 60 && hasMoreMessages && !isLoadingMore && currentConvId) {
            loadMoreMessages();
        }
    }

    async function loadMoreMessages() {
        if (isLoadingMore || !hasMoreMessages || !currentConvId || !oldestMsgTimestamp) return;
        isLoadingMore = true;

        try {
            const container = document.getElementById('chat-messages');
            // Save current scroll position and height to restore after prepend
            const prevScrollHeight = container.scrollHeight;
            const prevScrollTop = container.scrollTop;

            // Remove the "scroll up" hint before loading
            const oldHint = document.getElementById('scroll-more-hint');
            if (oldHint) oldHint.remove();

            // Show loading indicator at top
            const loadingEl = document.createElement('div');
            loadingEl.className = 'chat-msg loading-more';
            loadingEl.id = 'loading-more-indicator';
            loadingEl.innerHTML = '<div class="msg-content" style="text-align:center;color:#888;">' + t('general.loading') + '</div>';
            container.insertBefore(loadingEl, container.firstChild);

            const resp = await fetch('/api/conversations/' + currentConvId + '?limit=' + PAGE_SIZE + '&before=' + encodeURIComponent(oldestMsgTimestamp));
            const conv = await resp.json();

            // Remove loading indicator
            const indicator = document.getElementById('loading-more-indicator');
            if (indicator) indicator.remove();

            const olderMessages = conv.messages || [];
            hasMoreMessages = conv.has_more || false;

            if (olderMessages.length > 0) {
                // Update oldest timestamp
                oldestMsgTimestamp = olderMessages[0].timestamp || oldestMsgTimestamp;

                // Prepend older messages to the DOM
                const fragment = document.createDocumentFragment();
                olderMessages.forEach(function(msg) {
                    const el = createMessageElement(msg);
                    if (el) fragment.appendChild(el);
                });
                container.insertBefore(fragment, container.firstChild);

                // Add new "scroll up" hint if still more messages
                if (hasMoreMessages) {
                    const newHint = document.createElement('div');
                    newHint.className = 'chat-msg load-more-hint';
                    newHint.id = 'scroll-more-hint';
                    newHint.innerHTML = '<div class="msg-content" style="text-align:center;color:var(--text-muted);font-size:12px;padding:8px 0;">' + t('chat.loadMoreMessages') + '</div>';
                    container.insertBefore(newHint, container.firstChild);
                }

                // Restore scroll position so user stays at the same message
                const newScrollHeight = container.scrollHeight;
                container.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight);

                // Also prepend to messages array
                messages = olderMessages.concat(messages);
            }

            if (!hasMoreMessages) {
                // No more messages to load, remove scroll listener
                const cont = document.getElementById('chat-messages');
                if (cont) cont.removeEventListener('scroll', handleScrollPagination);
            }
        } catch(e) {
            console.error('Failed to load more messages:', e);
            const indicator = document.getElementById('loading-more-indicator');
            if (indicator) indicator.remove();
        } finally {
            isLoadingMore = false;
        }
    }

    function createMessageElement(msg) {
        if (msg.role === 'user') {
            const el = document.createElement('div');
            el.className = 'chat-msg user';
            el.innerHTML = '<div class="msg-role">You</div><div class="msg-content">' + renderMarkdownLite(msg.content) + '</div>';
            addMessageActions(el);
            return el;
        } else if (msg.role === 'assistant') {
            const el = document.createElement('div');
            el.className = 'chat-msg assistant';
            el.innerHTML = '<div class="msg-role">ZAgent</div><div class="msg-content"></div>';
            renderChatMarkdown(el.querySelector('.msg-content'), msg.content);
            addMessageActions(el);
            return el;
        } else if (msg.role === 'tool') {
            const el = document.createElement('div');
            el.className = 'chat-msg tool';
            const icon = TOOL_ICONS[msg.tool_name] || '🔧';
            const resultStr = msg.tool_result || '';
            const ok = msg.tool_ok !== false;
            el.innerHTML = '<div class="tool-header"><span class="tool-name">' + icon + ' ' + escapeHTML(msg.tool_name) + '</span><span class="tool-status ' + (ok ? 'tool-ok' : 'tool-fail') + '">' + (ok ? '✓' : '✗') + '</span></div>' +
                (msg.tool_args ? '<div class="tool-args">' + escapeHTML(formatToolArgs(msg.tool_args)) + '</div>' : '') +
                '<div class="tool-result"></div>';
            // Use renderToolResult for collapse/expand support
            const resultContainer = el.querySelector('.tool-result');
            if (resultContainer && resultStr) {
                renderToolResult(resultContainer, resultStr);
            }
            return el;
        }
        return null;
    }

    function renderMessages(msgs) {
        const container = document.getElementById('chat-messages');
        container.innerHTML = '';
        const emptyState = document.getElementById('empty-state');
        if (emptyState) emptyState.style.display = msgs.length > 0 ? 'none' : 'flex';

        // Add "scroll up for more" hint if there are older messages
        if (hasMoreMessages) {
            const hint = document.createElement('div');
            hint.className = 'chat-msg load-more-hint';
            hint.id = 'scroll-more-hint';
            hint.innerHTML = '<div class="msg-content" style="text-align:center;color:var(--text-muted);font-size:12px;padding:8px 0;">' + t('chat.loadMoreMessages') + '</div>';
            container.appendChild(hint);
        }

        msgs.forEach(function(msg) {
            if (msg.role === 'user') {
                const el = document.createElement('div');
                el.className = 'chat-msg user';
                el.innerHTML = '<div class="msg-role">You</div><div class="msg-content">' + renderMarkdownLite(msg.content) + '</div>';
                container.appendChild(el);
                addMessageActions(el);
            } else if (msg.role === 'assistant') {
                const el = document.createElement('div');
                el.className = 'chat-msg assistant';
                el.innerHTML = '<div class="msg-role">ZAgent</div><div class="msg-content"></div>';
                renderChatMarkdown(el.querySelector('.msg-content'), msg.content);
                container.appendChild(el);
                addMessageActions(el);
            } else if (msg.role === 'tool') {
                const el = document.createElement('div');
                el.className = 'chat-msg tool';
                const icon = TOOL_ICONS[msg.tool_name] || '🔧';
                const resultStr = msg.tool_result || '';
                const ok = msg.tool_ok !== false;
                el.innerHTML = '<div class="tool-header"><span class="tool-name">' + icon + ' ' + escapeHTML(msg.tool_name) + '</span><span class="tool-status ' + (ok ? 'tool-ok' : 'tool-fail') + '">' + (ok ? '✓' : '✗') + '</span></div>' +
                    (msg.tool_args ? '<div class="tool-args">' + escapeHTML(formatToolArgs(msg.tool_args)) + '</div>' : '') +
                    '<div class="tool-result"></div>';
                // Use renderToolResult for collapse/expand support
                const resultContainer = el.querySelector('.tool-result');
                if (resultContainer && resultStr) {
                    renderToolResult(resultContainer, resultStr);
                }
                container.appendChild(el);
            }
        });
        forceScrollToBottom();
    }

    async function deleteConversation(id) {
        if (!confirm(t('chat.confirmDeleteConv'))) return;
        await fetch('/api/conversations/' + id, { method: 'DELETE' });
        if (currentConvId === id) {
            currentConvId = null;
            try { localStorage.removeItem('aicqbot_last_conv_id'); } catch(_e) {}
            document.getElementById('chat-title').textContent = t('sidebar.newChat');
            document.getElementById('chat-messages').innerHTML = '<div class="empty-state" id="empty-state"><div class="empty-icon">🤖</div><h2>ZAgent AI Assistant</h2><p>Start a conversation to begin</p></div>';
        }
        loadConversations();
    }

    function newChat() {
        currentConvId = null;
        try { localStorage.removeItem('aicqbot_last_conv_id'); } catch(_e) {}
        document.getElementById('chat-title').textContent = t('sidebar.newChat');
        document.getElementById('chat-messages').innerHTML = '<div class="empty-state" id="empty-state"><div class="empty-icon">🤖</div><h2>ZAgent AI Assistant</h2><p>Start a conversation to begin</p></div>';
        renderConvList();
        if (document.getElementById('chat-input')) document.getElementById('chat-input').focus();
    }

    // ── LLM Config ───────────────────────────────────────────
    async function loadLLMConfig() {
        try {
            const resp = await fetch('/api/llm/config');
            llmConfig = await resp.json();
            refreshModelSelector();
        } catch(e) { console.error('Failed to load LLM config:', e); }
    }

    function refreshModelSelector() {
        const select = document.getElementById('chat-model-select');
        if (!select) return;
        const savedModel = localStorage.getItem('aicqbot_chat_model');
        const currentVal = select.value;
        select.innerHTML = '';
        if (llmConfig && llmConfig.models) {
            for (let i = 0; i < llmConfig.models.length; i++) {
                const m = llmConfig.models[i];
                const opt = document.createElement('option');
                opt.value = i;
                const modalityIcons = (m.modalities || ['text']).map(mod => {
                    const icons = {text:'📝', image:'🖼️', video:'🎬', audio:'🎵'};
                    return icons[mod] || '';
                }).filter(Boolean).join('');
                opt.textContent = (m.name || m.model || ('Model ' + (i + 1))) + (modalityIcons ? ' ' + modalityIcons : '');
                opt.disabled = !m.enabled;
                if (opt.disabled) opt.textContent += ' (disabled)';
                select.appendChild(opt);
            }
            if (savedModel !== null && select.querySelector('option[value="' + savedModel + '"]')) {
                select.value = savedModel;
            } else if (currentVal !== '' && select.querySelector('option[value="' + currentVal + '"]')) {
                select.value = currentVal;
            } else {
                const firstEnabled = select.querySelector('option:not([disabled])');
                if (firstEnabled) select.value = firstEnabled.value;
            }
            if (!select._changeBound) {
                select._changeBound = true;
                select.addEventListener('change', () => {
                    localStorage.setItem('aicqbot_chat_model', select.value);
                });
            }
            select._modelConfigs = llmConfig.models;
        }
    }

    // ── Settings Dialog (ported from IDE) ────────────────────
    function showSettings() {
        if (showPanelOrDialog('settings', 'settings-dialog')) return;
        const body = document.getElementById('settings-body');
        if (!llmConfig) return;

        workingModels = JSON.parse(JSON.stringify(llmConfig.models));

        let html = '';
        workingModels.forEach(function(m, idx) {
            const modalities = m.modalities || ['text'];
            html += '<div class="model-card" data-idx="' + idx + '">';
            html += '<div class="model-card-header">';
            html += '<span class="model-card-title">Model ' + (idx+1) + ': ' + escapeHTML(m.name) + '</span>';
            html += '<div class="model-card-actions">';
            html += '<button class="llm-test-model-btn" data-idx="' + idx + '" title="' + t('llm.testModel') + '">🔗</button>';
            if (workingModels.length > 1) html += '<button class="model-card-remove" data-idx="' + idx + '">✕</button>';
            html += '</div></div>';

            html += '<div class="form-row"><div class="form-group"><label>Name</label><input type="text" data-field="name" data-idx="' + idx + '" value="' + escapeHTML(m.name) + '"></div>';
            html += '<div class="form-group"><label>Model ID</label><input type="text" data-field="model" data-idx="' + idx + '" value="' + escapeHTML(m.model) + '"></div></div>';
            html += '<div class="form-group"><label>API Base URL</label><input type="text" data-field="api_base" data-idx="' + idx + '" value="' + escapeHTML(m.api_base) + '"></div>';
            html += '<div class="form-group"><label>API Key</label><input type="password" data-field="api_key" data-idx="' + idx + '" value="' + escapeHTML(m.api_key) + '"></div>';
            html += '<div class="form-row"><div class="form-group"><label>Temperature</label><input type="number" step="0.1" data-field="temperature" data-idx="' + idx + '" value="' + m.temperature + '"></div>';
            html += '<div class="form-group"><label>Max Tokens</label><input type="number" data-field="max_tokens" data-idx="' + idx + '" value="' + m.max_tokens + '"></div>';
            html += '<div class="form-group"><label>Max Context</label><input type="number" data-field="max_context" data-idx="' + idx + '" value="' + m.max_context + '"></div></div>';
            html += '<div class="form-row"><div class="form-group"><label>Provider</label><input type="text" data-field="provider" data-idx="' + idx + '" value="' + escapeHTML(m.provider) + '"></div>';
            html += '<div class="form-group"><label>Enabled</label><select data-field="enabled" data-idx="' + idx + '"><option value="true"' + (m.enabled ? ' selected' : '') + '>Yes</option><option value="false"' + (!m.enabled ? ' selected' : '') + '>No</option></select></div></div>';

            html += '<div class="llm-extras-row">';
            html += '<label class="llm-reasoning-label"><span>' + t('llm.reasoningMode') + '</span><input type="checkbox" class="llm-reasoning" data-idx="' + idx + '"' + (m.reasoning !== false ? ' checked' : '') + '></label>';
            html += '<div class="llm-modalities-row"><span>' + t('llm.modalities') + '</span>';
            ['text','image','video','audio'].forEach(function(mod) {
                const checked = modalities.includes(mod);
                const modIcons = {text:'📝', image:'🖼️', video:'🎬', audio:'🎵'};
                const disabled = mod === 'text' ? 'disabled' : '';
                html += '<label class="llm-modality-label" data-idx="' + idx + '" data-mod="' + mod + '">' +
                    '<input type="checkbox" class="llm-modality" data-idx="' + idx + '" data-mod="' + mod + '" ' + (checked ? 'checked' : '') + ' ' + disabled + '>' +
                    '<span>' + (modIcons[mod] || '') + ' ' + mod + '</span></label>';
            });
            html += '</div></div>';
            html += '</div>';
        });
        body.innerHTML = html;
        finishPanelOrDialog();
        bindSettingsEvents();
    }

    function bindSettingsEvents() {
        const overlay = document.getElementById('settings-body');
        overlay.querySelectorAll('.llm-test-model-btn').forEach(function(btn) {
            btn.addEventListener('click', function() { testModel(parseInt(btn.dataset.idx), btn); });
        });
        overlay.querySelectorAll('.model-card-remove').forEach(function(btn) {
            btn.addEventListener('click', function() { removeModel(parseInt(btn.dataset.idx)); });
        });
        overlay.querySelectorAll('.llm-reasoning').forEach(function(cb) {
            cb.addEventListener('change', function() {
                workingModels[parseInt(cb.dataset.idx)].reasoning = cb.checked;
            });
        });
        overlay.querySelectorAll('.llm-modality').forEach(function(cb) {
            cb.addEventListener('change', function() {
                const idx = parseInt(cb.dataset.idx);
                const mod = cb.dataset.mod;
                const m = workingModels[idx];
                if (!m.modalities) m.modalities = ['text'];
                if (cb.checked) {
                    if (!m.modalities.includes(mod)) m.modalities.push(mod);
                } else {
                    m.modalities = m.modalities.filter(function(x) { return x !== mod; });
                    if (m.modalities.length === 0) m.modalities = ['text'];
                }
                const label = cb.closest('.llm-modality-label');
                if (label) {
                    const span = label.querySelector('span');
                    if (span) span.style.color = cb.checked ? 'var(--text-primary)' : 'var(--text-muted)';
                }
            });
        });
    }

    function syncFieldsFromDOM() {
        const overlay = document.getElementById('settings-body');
        if (!overlay) return;
        overlay.querySelectorAll('[data-field]').forEach(function(input) {
            const idx = parseInt(input.dataset.idx);
            const field = input.dataset.field;
            if (idx >= 0 && idx < workingModels.length) {
                let val = input.value;
                if (field === 'temperature') val = parseFloat(val);
                else if (field === 'max_tokens' || field === 'max_context') val = parseInt(val) || 0;
                else if (field === 'enabled') val = val === 'true';
                workingModels[idx][field] = val;
            }
        });
        overlay.querySelectorAll('.llm-reasoning').forEach(function(cb) {
            workingModels[parseInt(cb.dataset.idx)].reasoning = cb.checked;
        });
        overlay.querySelectorAll('.llm-modality').forEach(function(cb) {
            const idx = parseInt(cb.dataset.idx);
            const mod = cb.dataset.mod;
            const m = workingModels[idx];
            if (!m.modalities) m.modalities = ['text'];
            if (cb.checked) {
                if (!m.modalities.includes(mod)) m.modalities.push(mod);
            } else {
                m.modalities = m.modalities.filter(function(x) { return x !== mod; });
                if (m.modalities.length === 0) m.modalities = ['text'];
            }
        });
    }

    async function testModel(idx, btnEl) {
        syncFieldsFromDOM();
        const origText = btnEl.textContent;
        btnEl.textContent = '⏳';
        btnEl.disabled = true;

        llmConfig.models = JSON.parse(JSON.stringify(workingModels));
        try {
            await fetch('/api/llm/config', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(llmConfig)
            });
        } catch(e) {}

        try {
            const resp = await fetch('/api/llm/test', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ model_index: idx })
            });
            const data = await resp.json();
            if (data.ok) {
                showToast('✅ ' + data.model + ' · ' + (data.tokens || 0) + ' tokens, reply: ' + (data.reply || '').substring(0, 80), 'success');
            } else {
                showToast('❌ ' + (data.error || 'Test failed'), 'error');
            }
        } catch(e) {
            showToast('❌ Test request failed: ' + e.message, 'error');
        }

        btnEl.textContent = origText;
        btnEl.disabled = false;
    }

    async function saveSettings() {
        syncFieldsFromDOM();
        llmConfig.models = JSON.parse(JSON.stringify(workingModels));
        try {
            await fetch('/api/llm/config', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(llmConfig)
            });
            showToast(t('settings.saved'), 'success');
            document.getElementById('settings-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
            refreshModelSelector();
        } catch(e) {
            showToast(t('settings.saveFailed'), 'error');
        }
    }

    function addModel() {
        syncFieldsFromDOM();
        workingModels.push({
            name: 'New Model', provider: 'openai', api_type: 'openai',
            api_key: '', api_base: 'https://api.openai.com/v1', model: 'gpt-4o-mini',
            enabled: true, temperature: 0.7, max_tokens: 4096, max_context: 128000,
            reasoning: true, modalities: ['text']
        });
        llmConfig.models = JSON.parse(JSON.stringify(workingModels));
        showSettings();
    }

    function removeModel(idx) {
        syncFieldsFromDOM();
        workingModels.splice(idx, 1);
        llmConfig.models = JSON.parse(JSON.stringify(workingModels));
        showSettings();
    }

    // ── Agent Status ─────────────────────────────────────────
    async function showAgentStatus() {
        if (showPanelOrDialog('status', 'status-dialog')) return;
        try {
            const resp = await fetch('/api/agent/status');
            const status = await resp.json();
            const body = document.getElementById('status-body');
            let html = '<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border)">' +
                '<div style="font-size:14px;font-weight:600;margin-bottom:10px;color:var(--accent)">&#128225; ' + t('status.commStatus') + '</div>' +
                '<div class="form-group"><label>Account ID</label><input readonly value="' + escapeHTML(status.account_id || 'Not authenticated') + '" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px"></div>' +
                '<div class="form-group"><label>Online</label><input readonly value="' + (status.online ? 'Yes' : 'No') + '" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px"></div>' +
                '</div>';
            // Owner section
            html += '<div style="margin-top:4px">' +
                '<div style="font-size:14px;font-weight:600;margin-bottom:10px;color:var(--accent)">&#128100; ' + t('owner.setOwner2') + '</div>' +
                '<div class="form-group"><label>' + t('owner.accountIdLabel') + '</label>' +
                '<input type="text" id="owner-id-input" placeholder="' + t('owner.enterId') + '" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px"></div>' +
                '<div style="font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:8px">' + t('owner.hint2') + '</div>' +
                '<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">' +
                '<button id="btn-clear-owner" class="btn-danger" style="font-size:12px">' + t('owner.clearOwner2') + '</button>' +
                '<button id="btn-add-friend" class="btn-primary" style="font-size:12px;background:var(--green)">📨 ' + t('owner.sendFriendReq2') + '</button>' +
                '</div>' +
                '<div id="owner-status" style="margin-top:8px;font-size:13px;color:var(--text-secondary)"></div>' +
                '<div style="margin-top:12px;text-align:right"><button id="btn-save-owner" class="btn-primary" style="font-size:13px">' + t('owner.bindOwner') + '</button></div>' +
                '</div>';
            body.innerHTML = html;
            finishPanelOrDialog();
            // Bind owner buttons
            var el;
            if (el = document.getElementById('btn-save-owner')) el.addEventListener('click', saveOwner);
            if (el = document.getElementById('btn-clear-owner')) el.addEventListener('click', clearOwner);
            if (el = document.getElementById('btn-add-friend')) el.addEventListener('click', sendFriendRequestToOwner);
            // Load current owner
            fetch('/api/owner').then(function(r) { return r.json(); }).then(function(data) {
                var input = document.getElementById('owner-id-input');
                var statusEl = document.getElementById('owner-status');
                if (!input || !statusEl) return;
                if (data.owner_id) {
                    input.value = data.owner_id;
                    statusEl.textContent = t('owner.currentOwner') + ': ' + data.owner_id;
                    statusEl.style.color = 'var(--green)';
                } else {
                    statusEl.textContent = t('owner.noOwner');
                    statusEl.style.color = 'var(--text-muted)';
                }
            }).catch(function() {});
        } catch(e) { showToast(t('status.failedGetStatus'), 'error'); }
    }

    // ── Clipboard Utility ──────────────────────────────────────
    function copyToClipboard(text) {
        // Try modern Clipboard API first (requires secure context: HTTPS or localhost)
        if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
            return navigator.clipboard.writeText(text).catch(function(err) {
                console.warn('[Clipboard] writeText failed, falling back:', err);
                return fallbackCopyToClipboard(text);
            });
        }
        // Fallback for non-secure contexts (e.g. HTTP on non-localhost)
        return Promise.resolve(fallbackCopyToClipboard(text));
    }

    function fallbackCopyToClipboard(text) {
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;';
        document.body.appendChild(ta);
        ta.focus();
        ta.select();
        try {
            var ok = document.execCommand('copy');
            if (!ok) console.warn('[Clipboard] execCommand("copy") returned false');
        } catch(err) {
            console.warn('[Clipboard] execCommand failed:', err);
        }
        document.body.removeChild(ta);
    }

    // ── Message Copy ──────────────────────────────────────
    function addCopyButton(msgEl) {
        const contentEl = msgEl.querySelector('.msg-content');
        if (!contentEl) return;
        const copyBtn = document.createElement('button');
        copyBtn.className = 'msg-copy-btn';
        copyBtn.title = 'Copy';
        copyBtn.textContent = '📋';
        copyBtn.style.cssText = 'position:absolute;top:4px;right:4px;opacity:0;transition:opacity 0.2s;background:none;border:none;cursor:pointer;font-size:14px;padding:4px;';
        msgEl.style.position = 'relative';
        msgEl.appendChild(copyBtn);
        msgEl.addEventListener('mouseenter', () => { copyBtn.style.opacity = '1'; });
        msgEl.addEventListener('mouseleave', () => { copyBtn.style.opacity = '0'; });
        copyBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            const text = contentEl.innerText || contentEl.textContent;
            copyToClipboard(text).then(() => {
                copyBtn.textContent = '✓';
                setTimeout(() => { copyBtn.textContent = '📋'; }, 1500);
            });
        });
    }

    // ── TTS Toggle & Per-Message Read ────────────────────────
    var _svgSpeakerOn = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>';
    var _svgSpeakerOff = '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>';

    function updateTTSIcon() {
        var iconEl = document.getElementById('tts-toggle-icon');
        if (iconEl) {
            iconEl.innerHTML = ttsEnabled ? _svgSpeakerOn : _svgSpeakerOff;
        }
    }

    function toggleTTSEnabled() {
        ttsEnabled = !ttsEnabled;
        ttsAutoRead = ttsEnabled; // When TTS is on, auto-read is on
        const btn = document.getElementById('btn-tts-toggle');
        if (btn) {
            btn.classList.toggle('active', ttsEnabled);
            btn.title = ttsEnabled ? t('tts.toggleOff') : t('tts.toggleOn');
        }
        updateTTSIcon();
        try { localStorage.setItem('aicqbot_tts_enabled', ttsEnabled ? '1' : '0'); } catch(e) {}
        // Also save auto-read setting to server
        fetch('/api/tts/config', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ auto_read: ttsAutoRead })
        }).catch(function() {});
        if (!ttsEnabled) stopSpeaking();
        showToast(ttsEnabled ? t('tts.enabled') : t('tts.disabled'), ttsEnabled ? 'success' : 'info');
    }

    function loadTTSToggleState() {
        try {
            ttsEnabled = localStorage.getItem('aicqbot_tts_enabled') === '1';
        } catch(e) {}
        const btn = document.getElementById('btn-tts-toggle');
        if (btn) {
            btn.classList.toggle('active', ttsEnabled);
            btn.title = ttsEnabled ? t('tts.toggleOff') : t('tts.toggleOn');
        }
        updateTTSIcon();
        // Also load TTS config from server
        fetch('/api/tts/config').then(function(r) { return r.json(); }).then(function(cfg) {
            ttsEngine = (cfg.engine === 'browser') ? 'edgetts' : (cfg.engine || 'edgetts');
            ttsServerUrl = cfg.server_url || '';
            ttsSpeed = cfg.speed || 1.0;
            ttsAutoRead = !!cfg.auto_read;
            ttsVoiceIndex = cfg.voice_index || 0;
        }).catch(function() {});
    }

    function stopSpeaking() {
        if (typeof speechSynthesis !== 'undefined') {
            speechSynthesis.cancel();
        }
        // Stop Android native TTS
        if (typeof window.AndroidTTS !== 'undefined') {
            try { window.AndroidTTS.stop(); } catch(e) {}
        }
        // Stop any playing audio from Edge TTS / IndexTTS
        if (currentTTSAudio) {
            currentTTSAudio.pause();
            currentTTSAudio.currentTime = 0;
            currentTTSAudio = null;
        }
        if (currentSpeakingEl) {
            var readBtn = currentSpeakingEl.querySelector('.msg-action-btn[data-action="read-aloud"]');
            if (readBtn) {
                readBtn.classList.remove('speaking');
                readBtn.innerHTML = '🔊';
                readBtn.title = t('tts.read');
            }
            currentSpeakingEl = null;
        }
    }

    // ── Android Native TTS (via JS bridge) ──
    var _androidTTSReady = false;
    var _androidTTSPending = null; // pending text to speak when TTS becomes ready
    var _androidTTSPollTimer = null; // polling timer for TTS readiness fallback

    // Called by Android when TTS engine initialization completes
    window.onTTSReady = function(success) {
        _androidTTSReady = success;
        console.log('[TTS] Android TTS ready callback fired, success=' + success);
        // Stop polling if it was running
        if (_androidTTSPollTimer) {
            clearInterval(_androidTTSPollTimer);
            _androidTTSPollTimer = null;
        }
        if (success && _androidTTSPending) {
            // Speak the pending text
            _speakAndroidTTS(_androidTTSPending.text, _androidTTSPending.readBtn);
            _androidTTSPending = null;
        }
    };

    // Fallback: proactively check if TTS is ready (in case onTTSReady callback
    // was missed due to race condition between page load and TTS initialization)
    function _startAndroidTTSPolling() {
        if (_androidTTSPollTimer) return; // already polling
        if (typeof window.AndroidTTS === 'undefined') return; // no bridge
        if (_androidTTSReady) return; // already ready

        var pollCount = 0;
        var maxPolls = 60; // 60 * 500ms = 30 seconds max wait
        _androidTTSPollTimer = setInterval(function() {
            pollCount++;
            try {
                if (window.AndroidTTS.isAvailable()) {
                    console.log('[TTS] TTS ready detected via polling (poll #' + pollCount + ')');
                    clearInterval(_androidTTSPollTimer);
                    _androidTTSPollTimer = null;
                    _androidTTSReady = true;
                    if (_androidTTSPending) {
                        _speakAndroidTTS(_androidTTSPending.text, _androidTTSPending.readBtn);
                        _androidTTSPending = null;
                    }
                    return;
                }
            } catch(e) {
                console.warn('[TTS] Poll error:', e);
            }
            if (pollCount >= maxPolls) {
                console.warn('[TTS] TTS polling timed out after ' + maxPolls + ' attempts');
                clearInterval(_androidTTSPollTimer);
                _androidTTSPollTimer = null;
                if (_androidTTSPending) {
                    // Fallback to browser speech synthesis directly (skip AndroidTTS to avoid recursion)
                    showToast(t('tts.androidInitTimeout'), 'warning');
                    var pending = _androidTTSPending;
                    _androidTTSPending = null;
                    _speakBrowserOnly(pending.text, pending.readBtn);
                }
            }
        }, 500);
    }

    function _speakAndroidTTS(text, readBtn) {
        if (typeof window.AndroidTTS === 'undefined') {
            return false; // Android TTS bridge not available
        }
        if (!_androidTTSReady || !window.AndroidTTS.isAvailable()) {
            // TTS not ready yet — start polling in background but also return false
            // so the caller can immediately try browser SpeechSynthesis as fallback
            _androidTTSPending = { text: text, readBtn: readBtn };
            _startAndroidTTSPolling();
            return false; // Return false to signal caller should try fallback NOW
        }
        // Stop any pending polling
        if (_androidTTSPollTimer) {
            clearInterval(_androidTTSPollTimer);
            _androidTTSPollTimer = null;
        }
        window._onAndroidTTSDone = function() {
            if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
            currentSpeakingEl = null;
        };
        window._onAndroidTTSError = function(msg) {
            showToast(t('tts.androidTTSFailed') + msg, 'error');
            if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
            currentSpeakingEl = null;
        };
        window.AndroidTTS.speak(text, 'zagent_tts_' + Date.now());
        return true;
    }

    // ── Proactive TTS readiness check on page load ──
    // If AndroidTTS bridge exists and is already available when the page loads,
    // set the ready flag immediately (no need to wait for callback or polling)
    (function() {
        try {
            if (typeof window.AndroidTTS !== 'undefined' && window.AndroidTTS.isAvailable()) {
                _androidTTSReady = true;
                console.log('[TTS] Android TTS already ready at page load');
            }
        } catch(e) {}
    })();

    // ── Edge TTS fallback for Android ──
    // When Android native TTS is not ready, try server-side Edge TTS first,
    // then browser SpeechSynthesis as last resort.
    function _speakEdgeTTSFallback(text, readBtn) {
        var ttsVoice = ttsVoiceIndex === 1 ? 'zh-CN-YunxiNeural' : (ttsVoiceIndex === 2 ? 'zh-CN-YunjianNeural' : 'zh-CN-XiaoxiaoNeural');
        fetch('/api/tts/speak', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text: text, voice: ttsVoice, speed: ttsSpeed || 1.0 })
        }).then(function(r) {
            if (!r.ok) throw new Error(t('tts.requestFailed'));
            return r.blob();
        }).then(function(blob) {
            if (blob.size < 100) throw new Error(t('tts.dataError'));
            var audio = new Audio(URL.createObjectURL(blob));
            currentTTSAudio = audio;
            // Cancel any pending Android TTS polling since we're using Edge TTS
            if (_androidTTSPollTimer) {
                clearInterval(_androidTTSPollTimer);
                _androidTTSPollTimer = null;
            }
            _androidTTSPending = null;
            audio.onended = function() {
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
                URL.revokeObjectURL(audio.src);
            };
            audio.onerror = function() {
                // Edge TTS playback failed, try browser fallback
                _speakBrowserOnly(text, readBtn);
            };
            audio.play().catch(function() {
                _speakBrowserOnly(text, readBtn);
            });
            // Update button to show it's using Edge TTS
            if (readBtn) { readBtn.innerHTML = '⏹'; readBtn.title = t('tts.stopRead'); }
        }).catch(function(e) {
            console.warn('[TTS] Edge TTS fallback failed on Android:', e.message);
            // Edge TTS failed, try browser SpeechSynthesis
            _speakBrowserOnly(text, readBtn);
        });
    }

    // ── Browser SpeechSynthesis fallback ──
    // _speakBrowserOnly uses ONLY the browser's SpeechSynthesis API (no AndroidTTS)
    // Used as a fallback when AndroidTTS initialization times out
    function _speakBrowserOnly(text, readBtn) {
        if (typeof speechSynthesis !== 'undefined' && speechSynthesis) {
            speechSynthesis.cancel(); // Stop any previous speech
            var utterance = new SpeechSynthesisUtterance(text);
            utterance.lang = 'zh-CN';
            utterance.rate = ttsSpeed || 1.0;
            // Try to find a Chinese voice
            var voices = speechSynthesis.getVoices();
            var zhVoice = voices.find(function(v) { return v.lang.indexOf('zh') === 0; });
            if (zhVoice) utterance.voice = zhVoice;
            utterance.onend = function() {
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
            };
            utterance.onerror = function() {
                showToast(t('tts.browserSynthFailed'), 'error');
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
            };
            speechSynthesis.speak(utterance);
            showToast(t('tts.switchedBrowser'), 'info');
            return;
        }

        // No TTS engine available at all
        showToast(t('tts.ttsUnavailable2'), 'error');
        if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
        currentSpeakingEl = null;
    }

    function _speakBrowserFallback(text, readBtn) {
        // Priority: Android Native TTS > Browser SpeechSynthesis
        if (_speakAndroidTTS(text, readBtn)) {
            return;
        }

        if (typeof speechSynthesis !== 'undefined' && speechSynthesis) {
            speechSynthesis.cancel(); // Stop any previous speech
            var utterance = new SpeechSynthesisUtterance(text);
            utterance.lang = 'zh-CN';
            utterance.rate = ttsSpeed || 1.0;
            // Try to find a Chinese voice
            var voices = speechSynthesis.getVoices();
            var zhVoice = voices.find(function(v) { return v.lang.indexOf('zh') === 0; });
            if (zhVoice) utterance.voice = zhVoice;
            utterance.onend = function() {
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
            };
            utterance.onerror = function() {
                showToast(t('tts.browserSynthFailed'), 'error');
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
            };
            speechSynthesis.speak(utterance);
            showToast(t('tts.browserFallback'), 'info');
            return;
        }

        // No TTS engine available at all
        showToast(t('tts.unavailable'), 'error');
        if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
        currentSpeakingEl = null;
    }

    // ── Notification Polling (alarm triggers, system notifications) ──
    var _lastNotifID = '';
    var _notifPollInterval = null;

    function startNotificationPolling() {
        if (_notifPollInterval) return;
        // Poll every 10 seconds for new notifications (reduced from 30s for faster AICQ message visibility)
        _notifPollInterval = setInterval(pollNotifications, 10000);
        // Also check immediately on page load
        setTimeout(pollNotifications, 2000);
    }

    function pollNotifications() {
        var url = '/api/notifications';
        if (_lastNotifID) url += '?after=' + encodeURIComponent(_lastNotifID);
        fetch(url).then(function(r) { return r.json(); }).then(function(data) {
            var notifs = data.notifications || [];
            notifs.forEach(function(n) {
                if (n.type === 'alarm') {
                    showToast(n.content, 'info');
                    addErrorMessage('⏰ ' + n.content);
                } else if (n.type === 'aicq_pm') {
                    // AICQ private message received - show toast and refresh conversation list
                    showToast(n.content, 'info');
                    // Refresh the sidebar conversation list so new messages appear immediately
                    loadConversations();
                } else if (n.type === 'friend_request') {
                    showToast(n.content, 'info');
                } else {
                    showToast(n.content, 'info');
                }
                _lastNotifID = n.id;
            });
        }).catch(function() {});
    }

    function speakMessage(msgEl) {
        if (!ttsEnabled) {
            showToast(t('tts.enableFirst'), 'error');
            return;
        }
        var contentEl = msgEl.querySelector('.msg-content');
        if (!contentEl) return;

        // If already speaking this message, stop it
        var readBtn = msgEl.querySelector('.msg-action-btn[data-action="read-aloud"]');
        if (currentSpeakingEl === msgEl) {
            stopSpeaking();
            return;
        }

        // Stop any previous speaking
        stopSpeaking();

        var text = contentEl.innerText || contentEl.textContent;
        // Filter out formulas, math symbols, code blocks, and special characters for TTS
        text = text
            .replace(/```[\s\S]*?```/g, '') // Remove code blocks
            .replace(/`[^`]+`/g, '') // Remove inline code
            .replace(/\$\$[\s\S]*?\$\$/g, '') // Remove display math ($$...$$)
            .replace(/\$[^$]+\$/g, '') // Remove inline math ($...$)
            .replace(/\\\([^)]*\\\)/g, '') // Remove \( ... \) math
            .replace(/\\\[[\s\S]*?\\\]/g, '') // Remove \[ ... \] math
            .replace(/[\u2200-\u22FF\u2A00-\u2AFF\u2300-\u23FF\u25A0-\u25FF\u2100-\u214F]/g, '') // Remove math/operator symbols
            .replace(/\b[a-zA-Z]_[a-zA-Z0-9]+\b/g, '') // Remove subscripted variables
            .replace(/\b[a-zA-Z]\^[0-9]+\b/g, '') // Remove superscripted variables
            .replace(/\s+/g, ' ') // Collapse whitespace
            .trim();
        if (!text) return;

        currentSpeakingEl = msgEl;

        // ── TTS Engine Selection ──
        // On Android: prefer native AndroidTTS (fast, no network needed, reliable)
        // On Desktop: prefer Edge TTS (server-side, best Chinese quality)
        var isAndroid = (typeof window.AndroidTTS !== 'undefined');

        if (isAndroid) {
            // Android: Try native TTS first (fastest, works offline)
            if (_speakAndroidTTS(text, readBtn)) {
                // Native TTS handled it
                if (readBtn) {
                    readBtn.classList.add('speaking');
                    readBtn.innerHTML = '⏹';
                    readBtn.title = t('tts.stopRead');
                }
            } else {
                // Android native TTS not ready yet — try Edge TTS as immediate fallback,
                // and keep polling Android TTS in background for next time
                if (readBtn) {
                    readBtn.classList.add('speaking');
                    readBtn.innerHTML = '⏳';
                    readBtn.title = t('tts.preparing');
                }
                // Try Edge TTS (server-side) as fallback on Android
                _speakEdgeTTSFallback(text, readBtn);
            }
            return;
        }

        // Desktop: Use Edge TTS (server-side, best quality for Chinese)
        if (ttsEngine === 'edgetts') {
            var ttsVoice = ttsVoiceIndex === 1 ? 'zh-CN-YunxiNeural' : (ttsVoiceIndex === 2 ? 'zh-CN-YunjianNeural' : 'zh-CN-XiaoxiaoNeural');
            fetch('/api/tts/speak', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ text: text, voice: ttsVoice, speed: ttsSpeed })
            }).then(function(r) {
                if (!r.ok) throw new Error(t('tts.requestFailed'));
                return r.blob();
            }).then(function(blob) {
                if (blob.size < 100) throw new Error(t('tts.dataError'));
                var audio = new Audio(URL.createObjectURL(blob));
                currentTTSAudio = audio;
                audio.onended = function() {
                    if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                    currentSpeakingEl = null;
                    URL.revokeObjectURL(audio.src);
                };
                audio.onerror = function() {
                    // Edge TTS playback failed, try browser fallback
                    _speakBrowserFallback(text, readBtn);
                };
                audio.play().catch(function() {
                    _speakBrowserFallback(text, readBtn);
                });
            }).catch(function(e) {
                console.warn('[TTS] Edge TTS failed, trying fallback:', e.message);
                _speakBrowserFallback(text, readBtn);
            });
        } else if (ttsEngine === 'indextts' && ttsServerUrl) {
            fetch(ttsServerUrl + '/tts', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ text: text, speed: ttsSpeed })
            }).then(function(r) { return r.blob(); }).then(function(blob) {
                var audio = new Audio(URL.createObjectURL(blob));
                currentTTSAudio = audio;
                audio.playbackRate = ttsSpeed;
                audio.onended = function() {
                    if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                    currentSpeakingEl = null;
                    URL.revokeObjectURL(audio.src);
                };
                audio.play().catch(function() {
                    showToast(t('tts.playFailed'), 'error');
                    if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                    currentSpeakingEl = null;
                });
            }).catch(function(e) {
                showToast(t('tts.requestFailed') + ': ' + e.message, 'error');
                if (readBtn) { readBtn.classList.remove('speaking'); readBtn.innerHTML = '🔊'; readBtn.title = t('tts.read'); }
                currentSpeakingEl = null;
            });
        }

        if (readBtn) {
            readBtn.classList.add('speaking');
            readBtn.innerHTML = '⏹';
            readBtn.title = t('tts.stopRead');
        }
    }

    function addMessageActions(msgEl) {
        var actionsRow = document.createElement('div');
        actionsRow.className = 'msg-actions';

        // Read aloud button
        var readBtn = document.createElement('button');
        readBtn.className = 'msg-action-btn';
        readBtn.setAttribute('data-action', 'read-aloud');
        readBtn.title = t('tts.read');
        readBtn.innerHTML = '🔊';
        readBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            speakMessage(msgEl);
        });
        actionsRow.appendChild(readBtn);

        // Copy button
        var copyBtn = document.createElement('button');
        copyBtn.className = 'msg-action-btn';
        copyBtn.setAttribute('data-action', 'copy-msg');
        copyBtn.title = t('chat.copy');
        copyBtn.innerHTML = '📋';
        copyBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            var contentEl = msgEl.querySelector('.msg-content');
            if (contentEl) {
                var text = contentEl.innerText || contentEl.textContent;
                copyToClipboard(text).then(function() {
                    copyBtn.innerHTML = '✓';
                    setTimeout(function() { copyBtn.innerHTML = '📋'; }, 1500);
                });
            }
        });
        actionsRow.appendChild(copyBtn);

        msgEl.appendChild(actionsRow);
    }

    // ── System Prompt ──────────────────────────────────────
    async function showSystemPrompt() {
        if (showPanelOrDialog('system-prompt', 'system-prompt-dialog')) return;
        try {
            const resp = await fetch('/api/system-prompt');
            const data = await resp.json();
            document.getElementById('system-prompt-text').value = data.prompt || '';
        } catch(e) { console.error('Failed to load system prompt:', e); }
        finishPanelOrDialog();
    }

    async function saveSystemPrompt() {
        const prompt = document.getElementById('system-prompt-text').value;
        try {
            await fetch('/api/system-prompt', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ prompt })
            });
            showToast(t('sysprompt.saved'), 'success');
            document.getElementById('system-prompt-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        } catch(e) { showToast(t('sysprompt.saveFailed'), 'error'); }
    }

    // ── Workflows (myagent-style) ────────────────────────────
    async function showWorkflows() {
        if (showPanelOrDialog('workflows', 'workflows-dialog')) return;
        const body = document.getElementById('workflows-body');
        finishPanelOrDialog();

        let wfRes = {workflows: []};
        let stRes = {};
        try {
            const [r1, r2] = await Promise.all([
                fetch('/api/workflows').then(r => r.json()).catch(() => ({workflows: []})),
                fetch('/api/workflows/stats').then(r => r.json()).catch(() => ({}))
            ]);
            wfRes = r1;
            stRes = r2;
        } catch(e) {}

        const workflows = wfRes.workflows || wfRes || [];
        const stats = stRes.stats || stRes || {};
        _workflowsCache = Array.isArray(workflows) ? workflows : [];
        _workflowStatsCache = stats;

        const totalWfs = _workflowsCache.length;
        const totalRuns = stats.total_runs || 0;
        const enabledCount = _workflowsCache.filter(w => w.enabled !== false).length;

        // Tab switcher
        let html = '<div style="display:flex;gap:8px;margin-bottom:16px">';
        html += '<button class="btn ' + (_wfTabPage === 'workflows' ? 'btn-primary' : 'btn-ghost') + '" data-wf-tab="workflows">⚡ 工作流</button>';
        html += '<button class="btn ' + (_wfTabPage === 'secrets' ? 'btn-primary' : 'btn-ghost') + '" data-wf-tab="secrets">🔐 密钥管理</button>';
        html += '</div>';

        if (_wfTabPage === 'secrets') {
            body.innerHTML = html;
            renderSecretsInDialog(body);
            _bindWfTabButtons(body);
            return;
        }

        // Stats cards
        html += '<div class="wf-stats">';
        html += '<div class="wf-stat"><div class="wf-stat-label">' + t('wf.total') + '</div><div class="wf-stat-value">' + totalWfs + '</div></div>';
        html += '<div class="wf-stat"><div class="wf-stat-label">' + t('wf.totalRuns') + '</div><div class="wf-stat-value">' + totalRuns + '</div></div>';
        html += '<div class="wf-stat"><div class="wf-stat-label">' + t('wf.statEnabled') + '</div><div class="wf-stat-value" style="color:var(--success)">' + enabledCount + '</div></div>';
        html += '</div>';

        // Toolbar
        html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">';
        html += '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">';
        html += '<button class="btn btn-sm btn-ghost" data-action="refresh-workflows">' + t('wf.refresh') + '</button>' + '';
        html += '</div>';
        html += '<div style="display:flex;gap:8px">';
        html += '<button class="btn btn-ghost" data-action="show-secrets-modal">' + t('wf.secretManage') + '</button>';
        html += '<button class="btn btn-primary" data-action="new-workflow">' + t('wf.new') + '</button>';
        html += '</div></div>';

        // Workflow cards
        if (!_workflowsCache.length) {
            html += '<div class="empty">' + t('wf.empty') + '</div>';
        } else {
            _workflowsCache.forEach(function(wf) {
                html += _workflowCardHtml(wf);
            });
        }

        body.innerHTML = html;
        _bindWfTabButtons(body);
        _bindWfActionButtons(body);
    }

    function _workflowCardHtml(wf) {
        const steps = wf.steps || [];
        const stepCount = steps.length;
        const isEnabled = wf.enabled !== false;
        const lastRun = wf.last_run;
        const runCount = wf.run_count || 0;
        const stepTypes = steps.map(s => s.type).filter(Boolean);

        const statusBadge = isEnabled
            ? '<span class="badge badge-green">' + t('wf.enabled') + '</span>'
            : '<span class="badge badge-red">' + t('wf.disabled') + '</span>';

        let lastRunBadge = '<span style="font-size:12px;color:var(--text3)">' + t('wf.notRun') + '</span>';
        if (lastRun) {
            const runStatus = lastRun.status || lastRun.result;
            if (runStatus === 'success' || runStatus === 'completed') {
                lastRunBadge = '<span class="badge badge-green" style="font-size:11px">✅ ' + t('wf.success') + '</span>';
            } else if (runStatus === 'failed' || runStatus === 'error') {
                lastRunBadge = '<span class="badge badge-red" style="font-size:11px">❌ ' + t('wf.failed') + '</span>';
            } else if (runStatus === 'running') {
                lastRunBadge = '<span class="badge badge-blue" style="font-size:11px">⏳ ' + t('wf.running') + '</span>';
            } else {
                lastRunBadge = '<span class="badge badge-yellow" style="font-size:11px">' + escapeHTML(String(runStatus)) + '</span>';
            }
        }

        const schedule = wf.schedule || '';
        const alarmId = wf.alarm_id || '';
        let scheduleBadge = '';
        if (schedule && alarmId) {
            const modeMap = {'once':t('alarm.modeOnce'),'daily':t('alarm.modeDaily'),'weekdays':t('alarm.modeWeekdays'),'weekly':t('alarm.modeWeekly'),'monthly':t('alarm.modeMonthly'),'interval':t('alarm.modeInterval')};
            const parts = schedule.split(':');
            const modeLabel = modeMap[parts[0]] || parts[0];
            var modeInfo = '';
            if (parts[0] === 'interval') {
                // format: "interval:3600s" — strip the 's' and convert to human-readable
                var secs = parseInt(parts[1]) || 0;
                if (secs >= 3600) modeInfo = Math.floor(secs/3600) + 'h';
                else if (secs >= 60) modeInfo = Math.floor(secs/60) + 'min';
                else modeInfo = secs + 's';
            } else {
                modeInfo = parts[1] || '';
            }
            scheduleBadge = '<span class="badge badge-blue" style="font-size:11px">⏰ ' + modeLabel + (modeInfo ? ' ' + modeInfo : '') + '</span>';
        }

        const stepIcons = stepTypes.slice(0, 6).map(function(t) {
            const def = STEP_TYPES[t];
            return def ? '<span title="' + escapeHTML(def.label) + '">' + def.icon + '</span>' : '';
        }).join(' ');
        const moreHint = stepTypes.length > 6 ? '<span style="font-size:11px;color:var(--text3)">+' + (stepTypes.length - 6) + '</span>' : '';

        return '<div class="model-card wf-card" style="margin-bottom:8px">' +
            '<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:8px">' +
            '<div style="min-width:0;flex:1">' +
            '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
            '<strong style="font-size:15px">' + escapeHTML(wf.name || t('wf.untitled2')) + '</strong>' +
            statusBadge +
            '</div>' +
            (wf.description ? '<p style="font-size:13px;color:var(--text-muted);margin-top:6px">' + escapeHTML(wf.description) + '</p>' : '') +
            '</div></div>' +
            '<div style="display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap;font-size:12px;color:var(--text3)">' +
            '<span class="tag">' + stepCount + ' ' + t('wf.steps') + '</span>' +
            '<span class="tag">' + runCount + ' ' + t('wf.runCountInline') + '</span>' +
            (lastRun && lastRun.time ? '<span style="color:var(--text3)">' + t('wf.lastRunInline') + ' ' + fmtTimeAgo(lastRun.time) + '</span>' : '') +
            scheduleBadge +
            '</div>' +
            '<div style="display:flex;gap:4px;margin-top:8px;font-size:16px;letter-spacing:2px">' + stepIcons + moreHint + '</div>' +
            '<div class="wf-card-actions">' +
            lastRunBadge +
            '<div style="flex:1"></div>' +
            '<button class="btn btn-sm btn-primary" data-action="run-workflow" data-id="' + escapeHTML(wf.id || '') + '">▶ ' + t('wf.run') + '</button>' +
            '<button class="btn btn-sm btn-ghost" data-action="schedule-workflow" data-id="' + escapeHTML(wf.id || '') + '">⏰ ' + t('wf.schedule') + '</button>' +
            '<button class="btn btn-sm btn-ghost" data-action="edit-workflow" data-id="' + escapeHTML(wf.id || '') + '">✏️ ' + t('wf.editWorkflow') + '</button>' +
            '<button class="btn btn-sm btn-ghost" data-action="toggle-workflow" data-id="' + escapeHTML(wf.id || '') + '" data-enabled="' + (!isEnabled) + '">' + (isEnabled ? '⏸ ' + t('wf.disable') : '▶ ' + t('wf.enable')) + '</button>' +
            '<button class="btn btn-sm btn-ghost wf-more-btn" data-action="wf-more" data-id="' + escapeHTML(wf.id || '') + '" data-name="' + escapeHTML(wf.name || '') + '">···</button>' +
            '</div></div>';
    }

    function _bindWfTabButtons(body) {
        body.querySelectorAll('[data-wf-tab]').forEach(function(btn) {
            btn.addEventListener('click', function() {
                _wfTabPage = btn.dataset.wfTab;
                showWorkflows();
            });
        });
    }

    function _bindWfActionButtons(body) {
        // Refresh
        var refreshBtn = body.querySelector('[data-action="refresh-workflows"]');
        if (refreshBtn) refreshBtn.addEventListener('click', showWorkflows);
        // New workflow
        var newBtn = body.querySelector('[data-action="new-workflows"]') || body.querySelector('[data-action="new-workflow"]');
        if (newBtn) newBtn.addEventListener('click', function() { showWorkflowFormModal(); });
        // Secrets modal
        var secretsBtn = body.querySelector('[data-action="show-secrets-modal"]');
        if (secretsBtn) secretsBtn.addEventListener('click', function() {
            _wfTabPage = 'secrets';
            showWorkflows();
        });
        // Card action buttons
        body.querySelectorAll('[data-action="run-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { runWorkflow(btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="schedule-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { showScheduleWorkflowModal(btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="edit-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { showWorkflowFormModal(null, btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="duplicate-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { duplicateWorkflow(btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="toggle-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { toggleWorkflow(btn.dataset.id, btn.dataset.enabled === 'true'); });
        });
        body.querySelectorAll('[data-action="delete-workflow"]').forEach(function(btn) {
            btn.addEventListener('click', function() { deleteWorkflow(btn.dataset.id, btn.dataset.name); });
        });
        // More menu (duplicate + delete)
        body.querySelectorAll('[data-action="wf-more"]').forEach(function(btn) {
            btn.addEventListener('click', function() {
                var id = btn.dataset.id;
                var name = btn.dataset.name;
                if (confirm(t('wf.confirmCopyOrDelete'))) {
                    duplicateWorkflow(id);
                } else {
                    deleteWorkflow(id, name);
                }
            });
        });
    }

    async function runWorkflow(id) {
        showWorkflowRunViewer(id);
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(id) + '/run', {method: 'POST'});
            const data = await resp.json();
            if (data.error || data.ok === false) {
                showToast(t('wf.runFailed') + ': ' + (data.error || data.message || t('wf.unknownError')), 'error');
            } else {
                showToast(t('wf.started'), 'success');
            }
            await _loadWorkflowRuns(id);
        } catch(e) {
            showToast(t('wf.runFailed') + ': ' + e.message, 'error');
        }
    }

    async function toggleWorkflow(id, enabled) {
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(id) + '/toggle', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({enabled: enabled})
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('wf.operationFailed') + ': ' + (data.error || data.message || t('wf.unknownError')), 'error'); return; }
            showToast(enabled ? t('wf.enabled') : t('skills.disabled'), 'success');
            showWorkflows();
        } catch(e) { showToast(t('wf.operationFailed') + ': ' + e.message, 'error'); }
    }

    async function duplicateWorkflow(id) {
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(id) + '/duplicate', {method: 'POST'});
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('wf.copyFailed') + ': ' + (data.error || data.message || t('wf.unknownError')), 'error'); return; }
            showToast(t('wf.copied'), 'success');
            showWorkflows();
        } catch(e) { showToast(t('wf.copyFailed') + ': ' + e.message, 'error'); }
    }

    function deleteWorkflow(id, name) {
        if (!confirm(t('wf.confirmDelete', (name || id)))) return;
        fetch('/api/workflows/' + encodeURIComponent(id), {method: 'DELETE'})
            .then(function(r) { return r.json(); })
            .then(function(data) {
                if (data.error) { showToast(data.error, 'error'); return; }
                showToast(t('wf.deleted'), 'success');
                showWorkflows();
            })
            .catch(function(e) { showToast(t('wf.deleteFailed') + ': ' + e.message, 'error'); });
    }

    // ── Workflow Form Modal (create/edit) ────────────────────
    async function showWorkflowFormModal(agent, wid) {
        let wf = null;
        if (wid) {
            try {
                const resp = await fetch('/api/workflows/' + encodeURIComponent(wid));
                wf = await resp.json();
                if (wf.error) { showToast(t('wf.loadFailed') + ': ' + wf.error, 'error'); return; }
            } catch(e) { showToast(t('wf.loadFailed'), 'error'); return; }
        }
        const isEdit = !!wf;
        const name = wf ? wf.name : '';
        const desc = wf ? (wf.description || '') : '';
        const variables = wf && wf.variables ? JSON.stringify(wf.variables, null, 2) : '{}';
        _editStepsData = wf && wf.steps ? JSON.parse(JSON.stringify(wf.steps)) : [];

        const title = isEdit ? t('wf.editTitle') : t('wf.newTitle');
        const submitLabel = isEdit ? t('wf.saveChanges') : t('wf.create');

        let html = '<div class="wf-sub-overlay" id="wfFormOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:680px;max-height:90vh;overflow-y:auto">';
        html += '<h3>' + (isEdit ? '✏️' : '+') + ' ' + title + '</h3>';
        html += '<div class="form-group"><label>' + t('wf.name') + '</label><input id="wfName" value="' + escapeHTML(name) + '" placeholder="输入工作流名称"></div>';
        html += '<div class="form-group"><label>' + t('wf.description') + '</label><input id="wfDesc" value="' + escapeHTML(desc) + '" placeholder="简要描述 (可选)"></div>';
        html += '<div class="form-group"><label>' + t('wf.variables') + '</label><textarea id="wfVariables" rows="3" placeholder=\'{"key": "value"}\' style="font-family:monospace;font-size:12px">' + escapeHTML(variables) + '</textarea></div>';
        html += '<div class="form-group"><label>' + t('wf.stepList') + ' <span id="wfStepCount" class="badge badge-blue" style="font-size:11px;vertical-align:middle">' + _editStepsData.length + '</span></label>';
        html += '<div id="wfStepsContainer">' + _renderStepsEditor() + '</div>';
        html += '<button class="btn btn-sm btn-ghost" style="margin-top:8px" data-action="add-step-modal">' + t('wf.addStep') + '</button>' + '';
        html += '</div>';
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        if (isEdit) {
            html += '<button class="btn btn-primary" data-action="save-workflow" data-id="' + escapeHTML(wf.id || wid) + '">' + submitLabel + '</button>';
        } else {
            html += '<button class="btn btn-primary" data-action="create-workflow">' + submitLabel + '</button>';
        }
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.cancel') + '</button>' + '';
        html += '</div></div></div>';

        const body = document.getElementById('workflows-body');
        body.insertAdjacentHTML('beforeend', html);

        // Bind sub-modal buttons
        var overlay = document.getElementById('wfFormOverlay');
        overlay.querySelector('[data-action="add-step-modal"]').addEventListener('click', function() { showAddStepModal(); });
        if (isEdit) {
            overlay.querySelector('[data-action="save-workflow"]').addEventListener('click', function() { doSaveWorkflow(this.dataset.id); });
        } else {
            overlay.querySelector('[data-action="create-workflow"]').addEventListener('click', function() { doCreateWorkflow(); });
        }
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() {
            overlay.remove();
        });
    }

    // ── Steps Editor ─────────────────────────────────────────
    function _renderStepsEditor() {
        if (!_editStepsData.length) {
            return '<div class="empty" style="padding:16px">' + t('wf.noSteps') + '</div>';
        }
        let html = '';
        _editStepsData.forEach(function(step, i) {
            const def = STEP_TYPES[step.type] || {label: step.type, icon: '❓', params: []};
            const paramEntries = Object.entries(step.params || {});

            const paramsPreview = paramEntries.map(function(entry) {
                const val = String(entry[1] || '');
                const display = val.length > 40 ? val.slice(0, 40) + '…' : val;
                return '<span style="color:var(--text3)">' + escapeHTML(entry[0]) + ':</span> <code style="font-size:11px">' + escapeHTML(display) + '</code>';
            }).join(' · ') || '<span style="color:var(--text3);font-size:12px">' + t('wf.noParams') + '</span>';

            const paramsDetail = paramEntries.map(function(entry) {
                const val = String(entry[1] || '');
                const isLong = val.length > 60 || val.includes('\\n');
                const valHtml = isLong
                    ? '<code style="font-size:11px;white-space:pre-wrap;word-break:break-all;display:block;margin-top:2px;max-height:120px;overflow-y:auto;padding:4px 6px;background:var(--bg2);border-radius:4px">' + escapeHTML(val) + '</code>'
                    : '<code style="font-size:11px">' + escapeHTML(val) + '</code>';
                return '<div style="margin-bottom:4px"><span style="color:var(--text3);font-size:11px">' + escapeHTML(entry[0]) + ':</span>' + valHtml + '</div>';
            }).join('') || '<span style="color:var(--text3);font-size:12px">' + t('wf.noParams') + '</span>';

            const needExpand = paramEntries.some(function(entry) { return String(entry[1] || '').length > 40; });
            const stepId = 'wfStep' + i;

            html += '<div class="wf-step-item">';
            html += '<div class="wf-step-main">';
            html += '<div class="wf-step-num">' + (i + 1) + '</div>';
            html += '<div class="wf-step-icon">' + def.icon + '</div>';
            html += '<div style="flex:1;min-width:0">';
            html += '<div style="font-size:13px;font-weight:500">' + escapeHTML(def.label) + '</div>';
            html += '<div id="' + stepId + 'Preview" style="font-size:11px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + paramsPreview + '</div>';
            html += '<div id="' + stepId + 'Detail" style="display:none;font-size:11px;margin-top:4px">' + paramsDetail + '</div>';
            html += '</div>';
            html += '</div>';
            html += '<div class="wf-step-actions">';
            if (needExpand) html += '<button class="btn btn-sm btn-ghost" data-action="toggle-step-detail" data-step-id="' + stepId + '" title="Expand">▾</button>';
            html += '<button class="btn btn-sm btn-ghost" data-action="edit-step" data-idx="' + i + '" title="' + t('wf.edit') + '">✏️</button>';
            html += '<button class="btn btn-sm btn-ghost" data-action="move-step" data-idx="' + i + '" data-dir="-1" title="' + t('wf.moveUp') + '"' + (i === 0 ? ' disabled style="opacity:0.3"' : '') + '>▲</button>';
            html += '<button class="btn btn-sm btn-ghost" data-action="move-step" data-idx="' + i + '" data-dir="1" title="' + t('wf.moveDown') + '"' + (i === _editStepsData.length - 1 ? ' disabled style="opacity:0.3"' : '') + '>▼</button>';
            html += '<button class="btn btn-sm btn-danger" data-action="remove-step" data-idx="' + i + '" title="' + t('wf.deleteStep') + '">✕</button>';
            html += '</div></div>';
        });
        return html;
    }

    function _refreshStepsEditor() {
        var container = document.getElementById('wfStepsContainer');
        if (container) container.innerHTML = _renderStepsEditor();
        var counter = document.getElementById('wfStepCount');
        if (counter) counter.textContent = _editStepsData.length;
        _bindStepEditorButtons();
    }

    function _bindStepEditorButtons() {
        var container = document.getElementById('wfStepsContainer');
        if (!container) return;
        container.querySelectorAll('[data-action="toggle-step-detail"]').forEach(function(btn) {
            btn.addEventListener('click', function() {
                var stepId = btn.dataset.stepId;
                var preview = document.getElementById(stepId + 'Preview');
                var detail = document.getElementById(stepId + 'Detail');
                if (!preview || !detail) return;
                var isExpanded = detail.style.display !== 'none';
                detail.style.display = isExpanded ? 'none' : 'block';
                preview.style.display = isExpanded ? '' : 'none';
                btn.textContent = isExpanded ? '▾' : '▴';
            });
        });
        container.querySelectorAll('[data-action="edit-step"]').forEach(function(btn) {
            btn.addEventListener('click', function() { editWorkflowStep(parseInt(btn.dataset.idx)); });
        });
        container.querySelectorAll('[data-action="move-step"]').forEach(function(btn) {
            btn.addEventListener('click', function() { moveWorkflowStep(parseInt(btn.dataset.idx), parseInt(btn.dataset.dir)); });
        });
        container.querySelectorAll('[data-action="remove-step"]').forEach(function(btn) {
            btn.addEventListener('click', function() { removeWorkflowStep(parseInt(btn.dataset.idx)); });
        });
    }

    function addWorkflowStep(type) {
        var def = STEP_TYPES[type];
        if (!def) return;
        var params = {};
        def.params.forEach(function(p) {
            if (p === 'method') params[p] = 'GET';
            else if (p === 'on_true' || p === 'on_false') params[p] = 'next';
            else if (p === 'headers') params[p] = '{}';
            else if (p === 'language') params[p] = 'shell';
            else if (p === 'timeout') params[p] = '120';
            else if (p === 'mode') params[p] = 'daily';
            else if (p === 'time') params[p] = '09:00';
            else if (p === 'interval_seconds') params[p] = '3600';
            else params[p] = '';
        });
        var cleanParams = {};
        for (var k in params) { if (params[k] !== '') cleanParams[k] = params[k]; }
        _editStepsData.push({type: type, name: def.label, params: cleanParams});
        _refreshStepsEditor();
    }

    function showAddStepModal() {
        var html = '<div class="wf-sub-overlay" id="wfAddStepOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:480px">';
        html += '<h3>' + t('wf.addStepTitle') + '</h3>';
        html += '<div class="form-group"><label>' + t('wf.stepType') + '</label>';
        html += '<select id="wfNewStepType">';
        var keys = Object.keys(STEP_TYPES);
        keys.forEach(function(key) {
            var val = STEP_TYPES[key];
            html += '<option value="' + escapeHTML(key) + '">' + val.icon + ' ' + val.label + '</option>';
        });
        html += '</select></div>';
        html += '<div id="wfNewStepParams"></div>';
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        html += '<button class="btn btn-primary" data-action="confirm-add-step">' + t('wf.add') + '</button>';
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.cancel') + '</button>' + '';
        html += '</div></div></div>';

        var body = document.getElementById('workflows-body');
        body.insertAdjacentHTML('beforeend', html);
        onNewStepTypeChange();
        var overlay = document.getElementById('wfAddStepOverlay');
        overlay.querySelector('#wfNewStepType').addEventListener('change', onNewStepTypeChange);
        overlay.querySelector('[data-action="confirm-add-step"]').addEventListener('click', confirmAddStep);
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() { overlay.remove(); });
    }

    function _renderParamInput(p, id, val, isRequired) {
        var inputHtml = '';
        var reqMark = isRequired ? ' <span style="color:var(--danger,#e53e3e)">*</span>' : '';
        var labelHtml = '<label>' + escapeHTML(p) + reqMark + '</label>';

        if (p === 'method') {
            inputHtml = '<select id="' + id + '_' + p + '"><option value="GET"' + (val==='GET'?' selected':'') + '>GET</option><option value="POST"' + (val==='POST'?' selected':'') + '>POST</option><option value="PUT"' + (val==='PUT'?' selected':'') + '>PUT</option><option value="DELETE"' + (val==='DELETE'?' selected':'') + '>DELETE</option></select>';
        } else if (p === 'language') {
            inputHtml = '<select id="' + id + '_' + p + '"><option value="shell"' + (val==='shell'||!val?' selected':'') + '>Shell</option><option value="python"' + (val==='python'?' selected':'') + '>Python</option><option value="bash"' + (val==='bash'?' selected':'') + '>Bash</option><option value="powershell"' + (val==='powershell'?' selected':'') + '>PowerShell</option><option value="cmd"' + (val==='cmd'?' selected':'') + '>Cmd</option></select>';
        } else if (p === 'mode') {
            inputHtml = '<select id="' + id + '_' + p + '"><option value="once"' + (val==='once'?' selected':'') + '>once — 一次性定时</option><option value="daily"' + (val==='daily'||!val?' selected':'') + '>daily — 每日</option><option value="weekdays"' + (val==='weekdays'?' selected':'') + '>weekdays — 工作日</option><option value="weekly"' + (val==='weekly'?' selected':'') + '>weekly — 每周指定日</option><option value="monthly"' + (val==='monthly'?' selected':'') + '>monthly — 每月指定日</option><option value="interval"' + (val==='interval'?' selected':'') + '>interval — 自定义间隔</option></select>';
        } else if (p === 'on_true' || p === 'on_false') {
            inputHtml = '<select id="' + id + '_' + p + '"><option value="next"' + (val==='next'||!val?' selected':'') + '>继续下一步</option><option value="stop"' + (val==='stop'?' selected':'') + '>停止</option></select>';
        } else if (p === 'code') {
            inputHtml = '<textarea id="' + id + '_' + p + '" rows="6" placeholder="' + t('wf.codePlaceholder') + '" style="font-family:monospace;font-size:12px">' + escapeHTML(val||'') + '</textarea>';
        } else if (p === 'prompt' || p === 'system') {
            var ph = p === 'prompt' ? '提示内容，支持 {{secrets.NAME}} 引用密钥、{{steps.1.result}} 引用步骤结果' : t('wf.systemPromptOptional');
            inputHtml = '<textarea id="' + id + '_' + p + '" rows="4" placeholder="' + ph + '" style="font-family:monospace;font-size:12px">' + escapeHTML(val||'') + '</textarea>';
        } else if (p === 'body' || p === 'headers') {
            inputHtml = '<textarea id="' + id + '_' + p + '" rows="2" placeholder="' + (p === 'headers' ? '{"key": "value"}' : t('wf.requestBody')) + '" style="font-family:monospace;font-size:12px">' + escapeHTML(val||'') + '</textarea>';
        } else if (p === 'seconds' || p === 'timeout' || p === 'max_tokens') {
            var min = p === 'seconds' ? '0' : '1';
            inputHtml = '<input id="' + id + '_' + p + '" type="number" min="' + min + '" value="' + escapeHTML(val||'') + '">';
        } else if (p === 'temperature') {
            inputHtml = '<input id="' + id + '_' + p + '" type="number" min="0" max="2" step="0.1" value="' + escapeHTML(val||'') + '">';
        } else {
            inputHtml = '<input id="' + id + '_' + p + '" value="' + escapeHTML(val||'') + '" placeholder="' + escapeHTML(p) + '">';
        }
        return '<div class="form-group">' + labelHtml + inputHtml + '</div>';
    }

    function onNewStepTypeChange() {
        var typeEl = document.getElementById('wfNewStepType');
        if (!typeEl) return;
        var type = typeEl.value;
        _curStepType = type;
        var def = STEP_TYPES[type];
        if (!def) return;
        var container = document.getElementById('wfNewStepParams');
        if (!container) return;
        var html = '';
        if (def.help) {
            html += '<div style="font-size:12px;color:var(--text2);margin-bottom:12px;padding:8px;background:var(--bg2);border-radius:6px;border-left:3px solid var(--primary)">' + escapeHTML(def.help) + '</div>';
        }
        def.params.forEach(function(p) {
            var isRequired = (def.required || []).indexOf(p) !== -1;
            html += _renderParamInput(p, 'wfParam', '', isRequired);
        });
        container.innerHTML = html;
    }

    function confirmAddStep() {
        var typeEl = document.getElementById('wfNewStepType');
        if (!typeEl) return;
        var type = typeEl.value;
        var def = STEP_TYPES[type];
        if (!def) return;
        var params = {};
        var required = def.required || [];
        var missing = [];
        def.params.forEach(function(p) {
            var el = document.getElementById('wfParam_' + p);
            if (!el) return;
            var val = el.value.trim();
            if (p === 'headers' && val) {
                try { val = JSON.parse(val); } catch (e) {}
            }
            if (val) params[p] = val;
            if (required.indexOf(p) !== -1 && !val) missing.push(p);
        });
        if (missing.length) {
            alert(t('wf.requiredEmpty') + missing.join(', '));
            return;
        }
        _editStepsData.push({type: type, name: def.label, params: params});
        var overlay = document.getElementById('wfAddStepOverlay');
        if (overlay) overlay.remove();
        _refreshStepsEditor();
    }

    function editWorkflowStep(idx) {
        var step = _editStepsData[idx];
        if (!step) return;
        var def = STEP_TYPES[step.type] || {label: step.type, icon: '❓', params: [], required: []};
        var params = step.params || {};
        _curStepType = step.type;

        var html = '<div class="wf-sub-overlay" id="wfEditStepOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:520px">';
        html += '<h3>✏️ ' + t('wf.editStepTitle') + ' ' + (idx + 1) + ': ' + def.icon + ' ' + escapeHTML(def.label) + '</h3>';
        html += '<div class="form-group"><label>' + t('wf.stepType') + '</label><input value="' + def.icon + ' ' + escapeHTML(def.label) + '" disabled></div>';
        if (def.help) {
            html += '<div style="font-size:12px;color:var(--text2);margin-bottom:12px;padding:8px;background:var(--bg2);border-radius:6px;border-left:3px solid var(--primary)">' + escapeHTML(def.help) + '</div>';
        }
        var knownParams = new Set(def.params);
        var extraParams = Object.keys(params).filter(function(k) { return !knownParams.has(k); });
        var displayParams = def.params.concat(extraParams);
        displayParams.forEach(function(p) {
            var val = params[p] != null ? (typeof params[p] === 'object' ? JSON.stringify(params[p], null, 2) : String(params[p])) : '';
            var isRequired = (def.required || []).indexOf(p) !== -1;
            html += _renderParamInput(p, 'wfEditParam', val, isRequired);
        });
        if (step.type === 'skill' || step.type === 'llm' || step.type === 'code') {
            html += '<div style="margin-top:8px;padding-top:8px;border-top:1px dashed var(--border)">';
            html += '<button class="btn btn-sm btn-ghost" data-action="add-custom-param" data-idx="' + idx + '">' + t('wf.addCustomParam') + '</button>';
            html += '</div>';
        }
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        html += '<button class="btn btn-primary" data-action="confirm-edit-step" data-idx="' + idx + '">' + t('wf.save2') + '</button>';
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.cancel') + '</button>' + '';
        html += '</div></div></div>';

        var body = document.getElementById('workflows-body');
        body.insertAdjacentHTML('beforeend', html);
        var overlay = document.getElementById('wfEditStepOverlay');
        overlay.querySelector('[data-action="confirm-edit-step"]').addEventListener('click', function() { confirmEditStep(idx); });
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() { overlay.remove(); });
        var customBtn = overlay.querySelector('[data-action="add-custom-param"]');
        if (customBtn) customBtn.addEventListener('click', function() {
            var key = prompt(t('wf.enterParamName'));
            if (!key || !key.trim()) return;
            var val = prompt(t('wf.enterParamValue'));
            if (val === null) return;
            step.params[key.trim()] = val;
            overlay.remove();
            editWorkflowStep(idx);
        });
    }

    function confirmEditStep(idx) {
        var step = _editStepsData[idx];
        if (!step) return;
        var def = STEP_TYPES[step.type] || {params: [], required: []};
        var required = def.required || [];
        var knownParams = new Set(def.params);
        var extraParams = Object.keys(step.params || {}).filter(function(k) { return !knownParams.has(k); });
        var displayParams = def.params.concat(extraParams);
        var missing = [];
        var newParams = {};
        displayParams.forEach(function(p) {
            var el = document.getElementById('wfEditParam_' + p);
            if (!el) {
                if (step.params[p] != null) newParams[p] = step.params[p];
                return;
            }
            var val = el.value.trim();
            if (p === 'headers' && val) {
                try { val = JSON.parse(val); } catch (e) {}
            }
            if (val) newParams[p] = val;
            if (required.indexOf(p) !== -1 && !val) missing.push(p);
        });
        if (missing.length) {
            alert(t('wf.requiredEmpty') + missing.join(', '));
            return;
        }
        step.params = newParams;
        var overlay = document.getElementById('wfEditStepOverlay');
        if (overlay) overlay.remove();
        _refreshStepsEditor();
    }

    function moveWorkflowStep(idx, dir) {
        var newIdx = idx + dir;
        if (newIdx < 0 || newIdx >= _editStepsData.length) return;
        var tmp = _editStepsData[idx];
        _editStepsData[idx] = _editStepsData[newIdx];
        _editStepsData[newIdx] = tmp;
        _refreshStepsEditor();
    }

    function removeWorkflowStep(idx) {
        _editStepsData.splice(idx, 1);
        _refreshStepsEditor();
    }

    // ── Save / Create Workflow ───────────────────────────────
    async function doCreateWorkflow() {
        var nameEl = document.getElementById('wfName');
        var descEl = document.getElementById('wfDesc');
        var varsEl = document.getElementById('wfVariables');
        var name = nameEl ? nameEl.value.trim() : '';
        var desc = descEl ? descEl.value.trim() : '';
        var variables = {};
        try { variables = JSON.parse(varsEl ? varsEl.value : '{}'); } catch(e) {
            showToast(t('wf.invalidJson'), 'error'); return;
        }
        if (!name) { showToast(t('wf.enterName'), 'error'); return; }
        if (!_editStepsData.length) { showToast(t('wf.addOneStep'), 'error'); return; }
        try {
            const resp = await fetch('/api/workflows', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({name: name, description: desc, steps: _editStepsData, variables: variables})
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('wf.createFailed') + ': ' + (data.error || data.message || t('wf.unknownError')), 'error'); return; }
            showToast(t('wf.created'), 'success');
            var overlay = document.getElementById('wfFormOverlay');
            if (overlay) overlay.remove();
            showWorkflows();
        } catch(e) { showToast(t('wf.createFailed') + ': ' + e.message, 'error'); }
    }

    async function doSaveWorkflow(wid) {
        var nameEl = document.getElementById('wfName');
        var descEl = document.getElementById('wfDesc');
        var varsEl = document.getElementById('wfVariables');
        var name = nameEl ? nameEl.value.trim() : '';
        var desc = descEl ? descEl.value.trim() : '';
        var variables = {};
        try { variables = JSON.parse(varsEl ? varsEl.value : '{}'); } catch(e) {
            showToast(t('wf.invalidJson'), 'error'); return;
        }
        if (!name) { showToast(t('wf.enterName'), 'error'); return; }
        if (!_editStepsData.length) { showToast(t('wf.addOneStep'), 'error'); return; }
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(wid), {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({name: name, description: desc, steps: _editStepsData, variables: variables})
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('wf.saveFailed2') + ': ' + (data.error || data.message || t('wf.unknownError')), 'error'); return; }
            showToast(t('wf.saved2'), 'success');
            var overlay = document.getElementById('wfFormOverlay');
            if (overlay) overlay.remove();
            showWorkflows();
        } catch(e) { showToast(t('wf.saveFailed2') + ': ' + e.message, 'error'); }
    }

    // ── Workflow Run Viewer ──────────────────────────────────
    function showWorkflowRunViewer(wid) {
        if (_runPollTimer) { clearInterval(_runPollTimer); _runPollTimer = null; }

        var html = '<div class="wf-sub-overlay" id="wfRunOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:720px;max-height:85vh;overflow-y:auto">';
        html += '<h3>' + t('wf.runDetails') + '</h3>';
        html += '<div id="wfRunContent" style="text-align:center;padding:24px;color:var(--text3)">' + t('general.loading') + '</div>';
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        html += '<button class="btn btn-ghost" data-action="refresh-wf-runs" data-id="' + escapeHTML(wid) + '">' + t('wf.refreshBtn') + '</button>';
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.closeBtn') + '</button>';
        html += '</div></div></div>';

        var body = document.getElementById('workflows-body');
        body.insertAdjacentHTML('beforeend', html);
        var overlay = document.getElementById('wfRunOverlay');
        overlay.querySelector('[data-action="refresh-wf-runs"]').addEventListener('click', function() { _loadWorkflowRuns(this.dataset.id); });
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() {
            if (_runPollTimer) { clearInterval(_runPollTimer); _runPollTimer = null; }
            overlay.remove();
        });

        _loadWorkflowRuns(wid);
        _runPollTimer = setInterval(function() { _loadWorkflowRuns(wid); }, 1500);
    }

    async function _loadWorkflowRuns(wid) {
        var container = document.getElementById('wfRunContent');
        if (!container) { if (_runPollTimer) { clearInterval(_runPollTimer); _runPollTimer = null; } return; }
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(wid) + '/runs');
            const r = await resp.json();
            if (r.error) {
                container.innerHTML = '<div style="color:var(--danger)">' + t('wf.loadFailed') + ': ' + escapeHTML(r.error) + '</div>';
                return;
            }
            var runs = r.runs || r || [];
            if (!Array.isArray(runs) || !runs.length) {
                container.innerHTML = '<div class="empty" style="padding:16px">' + t('wf.noRuns') + '</div>';
                if (_runPollTimer) { clearInterval(_runPollTimer); _runPollTimer = null; }
                return;
            }
            var latest = runs[0];
            var steps = latest.steps_log || latest.steps || latest.step_results || [];
            var html = '<div style="margin-bottom:12px;font-size:13px;color:var(--text2)">';
            html += t('wf.latestRun') + ' <strong>#' + escapeHTML(String(latest.id || latest.run_id || runs.length)) + '</strong>';
            html += (latest.started_at ? ' · ' + fmtDate(latest.started_at) : '');
            html += (latest.status ? ' <span class="badge ' + (latest.status === 'success' || latest.status === 'completed' ? 'badge-green' : latest.status === 'failed' || latest.status === 'error' ? 'badge-red' : latest.status === 'running' ? 'badge-blue' : 'badge-yellow') + '">' + (latest.status === 'running' ? '⏳ 执行中' : escapeHTML(latest.status)) + '</span>' : '');
            html += '</div>';

            if (steps.length) {
                html += '<div style="display:flex;flex-direction:column;gap:4px">';
                steps.forEach(function(s, i) {
                    var st = s.status || 'pending';
                    var icon = st === 'success' || st === 'completed' ? '✅' :
                               st === 'failed' || st === 'error' ? '❌' :
                               st === 'running' ? '⏳' : '⬜';
                    var def = STEP_TYPES[s.type] || {label: s.type || t('wf.stepCount'), icon: '❓'};
                    var duration = s.duration_ms != null ? ' <span style="font-size:11px;color:var(--text3)">(' + s.duration_ms + 'ms)</span>' : '';
                    var runningHint = st === 'running' ? ' <span style="color:var(--primary);font-size:11px">' + t('wf.executing') + '</span>' : '';
                    html += '<div style="display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:4px">';
                    html += '<span>' + icon + '</span>';
                    html += '<div style="flex:1;min-width:0">';
                    html += '<div style="font-size:13px;font-weight:500">' + def.icon + ' ' + escapeHTML(def.label) + duration + runningHint + '</div>';
                    if (s.error) html += '<div style="font-size:11px;color:var(--danger);margin-top:2px;word-break:break-all">' + escapeHTML(s.error.length > 200 ? s.error.slice(0, 200) + '...' : s.error) + '</div>';
                    if (s.result && typeof s.result === 'string') html += '<div style="font-size:11px;color:var(--text3);margin-top:2px;word-break:break-all">' + escapeHTML(s.result.length > 200 ? s.result.slice(0, 200) + '...' : s.result) + '</div>';
                    html += '</div></div>';
                });
                html += '</div>';
            }
            container.innerHTML = html;
        } catch(e) {
            container.innerHTML = '<div style="color:var(--danger)">' + t('wf.loadFailed') + '</div>';
        }
    }

    // ── Schedule Workflow Modal ──────────────────────────────
    async function showScheduleWorkflowModal(wid) {
        var wf = null;
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(wid));
            wf = await resp.json();
            if (wf.error) { showToast(t('wf.loadFailed') + ': ' + wf.error, 'error'); return; }
        } catch(e) { showToast(t('wf.loadFailed'), 'error'); return; }

        var currentSchedule = wf.schedule || '';
        var currentAlarmId = wf.alarm_id || '';
        var isScheduled = !!(currentSchedule && currentAlarmId);

        var curMode = 'daily', curTime = '09:00';
        if (currentSchedule) {
            var parts = currentSchedule.split(':');
            curMode = parts[0] || 'daily';
            if (curMode === 'interval') {
                curTime = ''; // interval uses seconds, not HH:MM
            } else {
                curTime = parts[1] || '09:00';
            }
        }

        var html = '<div class="wf-sub-overlay" id="wfScheduleOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:520px">';
        html += '<h3>⏰ 定时调度: ' + escapeHTML(wf.name || t('wf.untitled2')) + '</h3>';
        if (isScheduled) {
            html += '<div style="padding:8px 12px;background:var(--bg2);border-radius:6px;margin-bottom:12px;font-size:13px"><span style="color:var(--success)">✅ 当前已设置调度:</span> <code>' + escapeHTML(currentSchedule) + '</code></div>';
        }
        html += '<div class="form-group"><label>调度模式 *</label>';
        html += '<select id="wfSchedMode">';
        html += '<option value="once"' + (curMode==='once'?' selected':'') + '>once — 一次性定时</option>';
        html += '<option value="daily"' + (curMode==='daily'?' selected':'') + '>daily — 每日</option>';
        html += '<option value="weekdays"' + (curMode==='weekdays'?' selected':'') + '>weekdays — 工作日</option>';
        html += '<option value="weekly"' + (curMode==='weekly'?' selected':'') + '>weekly — 每周指定日</option>';
        html += '<option value="monthly"' + (curMode==='monthly'?' selected':'') + '>monthly — 每月指定日</option>';
        html += '<option value="interval"' + (curMode==='interval'?' selected':'') + '>interval — 自定义间隔</option>';
        html += '</select></div>';
        html += '<div id="wfSchedParams"><div class="form-group"><label>触发时间 (HH:MM)</label><input id="wfSchedTime" type="time" value="' + escapeHTML(curTime) + '"></div></div>';
        html += '<div class="form-group"><label>最大触发次数 (0=无限)</label><input id="wfSchedMaxTriggers" type="number" min="0" value="0"></div>';
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        html += '<button class="btn btn-primary" data-action="do-schedule" data-id="' + escapeHTML(wid) + '">💾 保存调度</button>';
        if (isScheduled) html += '<button class="btn btn-danger" data-action="do-unschedule" data-id="' + escapeHTML(wid) + '">🚫 取消调度</button>';
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.cancel') + '</button>' + '';
        html += '</div></div></div>';

        var body = document.getElementById('workflows-body');
        body.insertAdjacentHTML('beforeend', html);
        var overlay = document.getElementById('wfScheduleOverlay');
        overlay.querySelector('#wfSchedMode').addEventListener('change', onScheduleModeChange);
        overlay.querySelector('[data-action="do-schedule"]').addEventListener('click', function() { doScheduleWorkflow(this.dataset.id); });
        var unschedBtn = overlay.querySelector('[data-action="do-unschedule"]');
        if (unschedBtn) unschedBtn.addEventListener('click', function() { doUnscheduleWorkflow(this.dataset.id); });
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() { overlay.remove(); });
        onScheduleModeChange();
    }

    function onScheduleModeChange() {
        var modeEl = document.getElementById('wfSchedMode');
        var container = document.getElementById('wfSchedParams');
        if (!modeEl || !container) return;
        var mode = modeEl.value;
        var timeVal = document.getElementById('wfSchedTime') ? document.getElementById('wfSchedTime').value : '09:00';
        var html = '';
        if (mode === 'interval') {
            html = '<div class="form-group"><label>间隔秒数 (最小60)</label><input id="wfSchedInterval" type="number" min="60" value="3600"></div>';
        } else {
            html = '<div class="form-group"><label>触发时间 (HH:MM)</label><input id="wfSchedTime" type="time" value="' + escapeHTML(timeVal) + '"></div>';
            if (mode === 'weekly') {
                html += '<div class="form-group"><label>星期几 (0=周一...6=周日, 如 0,2,4)</label><input id="wfSchedWeekdays" placeholder="0,2,4"></div>';
            }
            if (mode === 'monthly') {
                html += '<div class="form-group"><label>每月几号 (如 1,15)</label><input id="wfSchedMonthDays" placeholder="1,15"></div>';
            }
        }
        container.innerHTML = html;
    }

    async function doScheduleWorkflow(wid) {
        var mode = document.getElementById('wfSchedMode') ? document.getElementById('wfSchedMode').value : 'daily';
        var maxTriggers = document.getElementById('wfSchedMaxTriggers') ? parseInt(document.getElementById('wfSchedMaxTriggers').value) || 0 : 0;
        var body = {mode: mode, max_triggers: maxTriggers};
        if (mode === 'interval') {
            body.interval_seconds = document.getElementById('wfSchedInterval') ? parseInt(document.getElementById('wfSchedInterval').value) || 3600 : 3600;
        } else {
            body.time = document.getElementById('wfSchedTime') ? document.getElementById('wfSchedTime').value || '09:00' : '09:00';
            if (mode === 'weekly') body.weekdays = document.getElementById('wfSchedWeekdays') ? document.getElementById('wfSchedWeekdays').value || '' : '';
            if (mode === 'monthly') body.month_days = document.getElementById('wfSchedMonthDays') ? document.getElementById('wfSchedMonthDays').value || '' : '';
        }
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(wid) + '/schedule', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('alarm.scheduleSetFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); return; }
            showToast(t('alarm.scheduleSet'), 'success');
            var overlay = document.getElementById('wfScheduleOverlay');
            if (overlay) overlay.remove();
            showWorkflows();
        } catch(e) { showToast(t('alarm.scheduleSetFailed') + ': ' + e.message, 'error'); }
    }

    async function doUnscheduleWorkflow(wid) {
        try {
            const resp = await fetch('/api/workflows/' + encodeURIComponent(wid) + '/schedule', {method: 'DELETE'});
            const data = await resp.json();
            if (data.error) { showToast(t('alarm.cancelFailed') + ': ' + data.error, 'error'); return; }
            showToast(t('alarm.scheduleCancelled'), 'success');
            var overlay = document.getElementById('wfScheduleOverlay');
            if (overlay) overlay.remove();
            showWorkflows();
        } catch(e) { showToast(t('alarm.cancelFailed') + ': ' + e.message, 'error'); }
    }

    // ── Secrets Modal ────────────────────────────────────────
    function renderSecretsInDialog(body) {
        var html = '<div style="margin-top:12px">';
        html += '<p style="font-size:13px;color:var(--text2);margin-bottom:12px">为工作流存储敏感信息（API密钥、Token等），在步骤中通过 <code style="background:var(--bg2);padding:2px 6px;border-radius:4px">{{secrets.xxx}}</code> 引用</p>';
        html += '<div class="form-group"><label>密钥名称</label><input id="secretName" placeholder="如 API_KEY"></div>';
        html += '<div class="form-group"><label>密钥值</label><input id="secretValue" type="password" placeholder="输入密钥值"></div>';
        html += '<button class="btn btn-primary" data-action="add-secret">💾 保存密钥</button>';
        html += '<div id="secretsListContainer" style="margin-top:16px"></div>';
        html += '</div>';
        body.insertAdjacentHTML('beforeend', html);
        body.querySelector('[data-action="add-secret"]').addEventListener('click', doAddSecret);
        _loadSecrets();
    }

    async function _loadSecrets() {
        var container = document.getElementById('secretsListContainer');
        if (!container) return;
        try {
            const resp = await fetch('/api/secrets');
            const data = await resp.json();
            _secretsCache = data.secrets || [];
            if (!_secretsCache.length) {
                container.innerHTML = '<div class="empty" style="padding:16px">暂无密钥，在上方添加</div>';
                return;
            }
            var listHtml = '<div style="display:flex;flex-direction:column;gap:6px">';
            _secretsCache.forEach(function(s) {
                listHtml += '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--border);border-radius:6px">';
                listHtml += '<span style="font-size:16px">🔑</span>';
                listHtml += '<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:500">' + escapeHTML(s.name) + '</div>';
                listHtml += '<div style="font-size:11px;color:var(--text3)"><code>{{secrets.' + escapeHTML(s.name) + '}}</code>' + (s.description ? ' · ' + escapeHTML(s.description) : '') + '</div></div>';
                listHtml += '<button class="btn btn-sm btn-danger" data-action="delete-secret" data-name="' + escapeHTML(s.name) + '">🗑</button>';
                listHtml += '</div>';
            });
            listHtml += '</div>';
            container.innerHTML = listHtml;
            container.querySelectorAll('[data-action="delete-secret"]').forEach(function(btn) {
                btn.addEventListener('click', function() { doDeleteSecret(btn.dataset.name); });
            });
        } catch(e) {
            container.innerHTML = '<div class="empty" style="padding:16px">加载密钥失败</div>';
        }
    }

    async function doAddSecret() {
        var name = document.getElementById('secretName') ? document.getElementById('secretName').value.trim() : '';
        var value = document.getElementById('secretValue') ? document.getElementById('secretValue').value : '';
        if (!name) { showToast(t('secret.enterName'), 'error'); return; }
        if (!value) { showToast(t('secret.enterValue'), 'error'); return; }
        try {
            const resp = await fetch('/api/secrets', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({name: name, value: value})
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('secret.saveFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); return; }
            showToast(t('secret.saved'), 'success');
            if (document.getElementById('secretName')) document.getElementById('secretName').value = '';
            if (document.getElementById('secretValue')) document.getElementById('secretValue').value = '';
            _loadSecrets();
        } catch(e) { showToast(t('wf.saveFailed2') + ': ' + e.message, 'error'); }
    }

    async function doDeleteSecret(name) {
        if (!confirm(t('secret.confirmDelete', name))) return;
        try {
            const resp = await fetch('/api/secrets/' + encodeURIComponent(name), {method: 'DELETE'});
            const data = await resp.json();
            if (data.error) { showToast(t('secret.deleteFailed') + ': ' + data.error, 'error'); return; }
            showToast(t('secret.deleted'), 'success');
            _loadSecrets();
        } catch(e) { showToast(t('wf.deleteFailed') + ': ' + e.message, 'error'); }
    }

    // ── Alarms (myagent-style) ───────────────────────────────
    function _modeDesc(alarm) {
        var m = alarm.mode || 'once';
        var t = alarm.time || '??:??';
        var modeInfo = ALARM_MODES[m] || {label: m};
        if (m === 'once') return modeInfo.label + ' ' + t;
        if (m === 'daily') return t('alarm.everyDay') + t;
        if (m === 'weekdays') return t('alarm.weekdays2') + t;
        if (m === 'weekly') {
            var days = (alarm.weekdays || []).map(function(d) { return WEEKDAY_LABELS[d] || d; }).join('、');
            return t('alarm.modeWeekly') + days + ' ' + t;
        }
        if (m === 'monthly') {
            var mdays = (alarm.month_days || []).join('、');
            return t('alarm.modeMonthly') + mdays + '号 ' + t;
        }
        if (m === 'interval') {
            var secs = alarm.interval_seconds || 0;
            if (secs >= 3600) return t('alarm.every2') + Math.floor(secs/3600) + t('alarm.everyHour');
            if (secs >= 60) return t('alarm.every2') + Math.floor(secs/60) + t('alarm.everyMinute');
            return t('alarm.every2') + secs + t('alarm.everySecond');
        }
        return modeInfo.label;
    }

    function _alarmCardHtml(a) {
        var enabled = a.enabled !== false;
        var modeInfo = ALARM_MODES[a.mode] || {label: a.mode || '?', icon:'❓', color:'#999'};
        var nextTrigger = a.next_trigger ? fmtDate(a.next_trigger) : '-';
        var triggerCount = a.trigger_count || 0;

        return '<div class="alarm-card ' + (enabled ? 'alarm-card-active' : 'alarm-card-paused') + '">' +
            '<div class="alarm-card-header">' +
                '<div class="alarm-card-title">' +
                    '<span class="alarm-card-icon" style="background:' + modeInfo.color + '22;color:' + modeInfo.color + '">' + modeInfo.icon + '</span>' +
                    '<span class="alarm-card-label">' + escapeHTML(a.label || t('wf.untitled2')) + '</span>' +
                '</div>' +
                '<span class="badge ' + (enabled ? 'badge-green' : 'badge-yellow') + '">' + (enabled ? '启用' : '暂停') + '</span>' +
            '</div>' +
            '<div class="alarm-card-body">' +
                '<div class="alarm-card-row">' +
                    '<span class="alarm-card-key">模式</span>' +
                    '<span class="badge" style="background:' + modeInfo.color + '22;color:' + modeInfo.color + '">' + escapeHTML(_modeDesc(a)) + '</span>' +
                '</div>' +
                (a.message ? '<div class="alarm-card-row"><span class="alarm-card-key">消息</span><span class="alarm-card-val">' + escapeHTML(a.message) + '</span></div>' : '') +
                '<div class="alarm-card-row">' +
                    '<span class="alarm-card-key">下次触发</span>' +
                    '<span class="alarm-card-val">' + escapeHTML(nextTrigger) + '</span>' +
                '</div>' +
                '<div class="alarm-card-row">' +
                    '<span class="alarm-card-key">已触发</span>' +
                    '<span class="alarm-card-val">' + triggerCount + ' 次</span>' +
                '</div>' +
                (a.workflow_id ? '<div class="alarm-card-row"><span class="alarm-card-key">工作流</span><span class="alarm-card-val" style="font-family:monospace;font-size:11px">' + escapeHTML(a.workflow_id) + '</span></div>' : '') +
            '</div>' +
            '<div class="alarm-card-actions">' +
            (enabled
                ? '<button class="btn btn-ghost btn-sm" data-action="pause-alarm" data-id="' + escapeHTML(a.id) + '">⏸ 暂停</button>'
                : '<button class="btn btn-ghost btn-sm" data-action="resume-alarm" data-id="' + escapeHTML(a.id) + '">▶ 恢复</button>'
            ) +
            '<button class="btn btn-danger btn-sm" data-action="delete-alarm" data-id="' + escapeHTML(a.id) + '" data-label="' + escapeHTML(a.label || '') + '">🗑 删除</button>' +
            '</div></div>';
    }

    function _alarmsStatsHtml() {
        var s = _alarmsStats;
        if (!s || !s.total) return '';
        return '<div class="wf-stats">' +
            '<div class="wf-stat"><div class="wf-stat-label">总闹钟数</div><div class="wf-stat-value">' + (s.total || 0) + '</div></div>' +
            '<div class="wf-stat"><div class="wf-stat-label">启用中</div><div class="wf-stat-value" style="color:var(--success)">' + (s.active || 0) + '</div></div>' +
            '<div class="wf-stat"><div class="wf-stat-label">已暂停</div><div class="wf-stat-value" style="color:var(--warn)">' + (s.paused || 0) + '</div></div>' +
            '</div>';
    }

    async function showAlarms() {
        var isRefresh = !document.getElementById('alarms-dialog').classList.contains('hidden');
        if (!isRefresh) {
            if (showPanelOrDialog('alarms', 'alarms-dialog')) return;
            finishPanelOrDialog();
        }
        const body = document.getElementById('alarms-body');
        if (!body) return;

        // Show loading indicator with timeout
        body.innerHTML = '<div style="text-align:center;padding:30px;color:var(--text-secondary)"><div class="spinner" style="margin:0 auto 12px"></div>加载中...</div>';

        try {
            // Add timeout to prevent infinite loading if backend hangs
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 15000);

            const resp = await fetch('/api/alarms', { signal: controller.signal });
            clearTimeout(timeoutId);

            if (!resp.ok) {
                var errText = '';
                try { var errData = await resp.json(); errText = errData.error || ''; } catch(ex) {}
                throw new Error('HTTP ' + resp.status + (errText ? ': ' + errText : ''));
            }
            const data = await resp.json();
            _alarmsData = data.alarms || [];
            _alarmsStats = data.stats || {};
        } catch(e) {
            _alarmsData = [];
            _alarmsStats = {};
            var errMsg = e.name === 'AbortError' ? t('alarm.timeoutRetry') : e.message;
            body.innerHTML = '<div style="text-align:center;color:var(--danger);padding:20px">加载失败: ' + escapeHTML(errMsg) + '</div>' +
                '<div style="text-align:center;margin-top:12px"><button class="btn btn-ghost btn-sm" data-action="refresh-alarms">🔄 重试</button></div>';
            var retryBtn = body.querySelector('[data-action="refresh-alarms"]');
            if (retryBtn) retryBtn.addEventListener('click', showAlarms);
            return;
        }

        var html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">';
        html += '<div style="display:flex;align-items:center;gap:8px">';
        html += '<button class="btn btn-ghost btn-sm" data-action="refresh-alarms">🔄 刷新</button>';
        html += '</div>';
        html += '<button class="btn btn-primary" data-action="create-alarm-form">+ 新建闹钟</button>';
        html += '</div>';

        html += _alarmsStatsHtml();

        var filtered = _alarmsData;
        if (!filtered.length) {
            html += '<div class="empty">暂无闹钟，点击上方按钮创建</div>';
        } else {
            html += '<div class="alarm-cards">';
            filtered.forEach(function(a) { html += _alarmCardHtml(a); });
            html += '</div>';
        }

        body.innerHTML = html;

        // Bind buttons
        var refreshBtn = body.querySelector('[data-action="refresh-alarms"]');
        if (refreshBtn) refreshBtn.addEventListener('click', showAlarms);
        var createBtn = body.querySelector('[data-action="create-alarm-form"]');
        if (createBtn) createBtn.addEventListener('click', createAlarmForm);
        body.querySelectorAll('[data-action="pause-alarm"]').forEach(function(btn) {
            btn.addEventListener('click', function() { pauseAlarm(btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="resume-alarm"]').forEach(function(btn) {
            btn.addEventListener('click', function() { resumeAlarm(btn.dataset.id); });
        });
        body.querySelectorAll('[data-action="delete-alarm"]').forEach(function(btn) {
            btn.addEventListener('click', function() { deleteAlarm(btn.dataset.id, btn.dataset.label); });
        });
    }

    async function pauseAlarm(id) {
        try {
            const resp = await fetch('/api/alarms/' + encodeURIComponent(id) + '/pause', {method: 'POST'});
            const data = await resp.json();
            if (data.ok) {
                showToast(t('alarm.paused'), 'success');
                showAlarms();
            } else {
                showToast(t('alarm.pauseFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
            }
        } catch(e) { showToast(t('alarm.pauseFailed') + ': ' + e.message, 'error'); }
    }

    async function resumeAlarm(id) {
        try {
            const resp = await fetch('/api/alarms/' + encodeURIComponent(id) + '/resume', {method: 'POST'});
            const data = await resp.json();
            if (data.ok) {
                showToast(t('alarm.resumed'), 'success');
                showAlarms();
            } else {
                showToast(t('alarm.resumeFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
            }
        } catch(e) { showToast(t('alarm.resumeFailed') + ': ' + e.message, 'error'); }
    }

    async function deleteAlarm(id, label) {
        if (!confirm(t('alarm.confirmDelete', (label || id)))) return;
        try {
            const resp = await fetch('/api/alarms/' + encodeURIComponent(id), {method: 'DELETE'});
            const data = await resp.json();
            if (data.ok) {
                showToast(t('alarm.deleted'), 'success');
                showAlarms();
            } else {
                showToast(t('alarm.deleteFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
            }
        } catch(e) { showToast(t('wf.deleteFailed') + ': ' + e.message, 'error'); }
    }

    function createAlarmForm() {
        var html = '<div class="wf-sub-overlay" id="alarmFormOverlay">';
        html += '<div class="wf-sub-modal" style="max-width:520px">';
        html += '<h3>⏰ 新建闹钟</h3>';
        html += '<div class="form-group"><label>名称 *</label><input id="alarmLabel" placeholder="闹钟名称"></div>';
        html += '<div class="form-group"><label>模式 *</label>';
        html += '<select id="alarmMode">';
        Object.keys(ALARM_MODES).forEach(function(key) {
            html += '<option value="' + key + '">' + ALARM_MODES[key].icon + ' ' + ALARM_MODES[key].label + '</option>';
        });
        html += '</select></div>';
        html += '<div id="alarmModeParams">';
        html += '<div class="form-group"><label>时间 (HH:MM)</label><input id="alarmTime" type="time" value="09:00"></div>';
        html += '</div>';
        html += '<div class="form-group"><label>提醒消息</label><textarea id="alarmMessage" rows="2" placeholder="闹钟触发时发送的消息"></textarea></div>';
        html += '<div class="form-group"><label>最大触发次数 (0=无限)</label><input id="alarmMaxTriggers" type="number" min="0" value="0"></div>';
        html += '<div class="form-group"><label>绑定工作流ID (可选)</label><input id="alarmWorkflowId" placeholder="触发时自动执行的工作流ID"></div>';
        html += '<div style="display:flex;gap:8px;margin-top:16px">';
        html += '<button class="btn btn-primary" data-action="do-create-alarm">💾 创建</button>';
        html += '<button class="btn btn-ghost" data-action="close-wf-sub">' + t('wf.cancel') + '</button>' + '';
        html += '</div></div></div>';

        var body = document.getElementById('alarms-body');
        body.insertAdjacentHTML('beforeend', html);
        var overlay = document.getElementById('alarmFormOverlay');
        overlay.querySelector('#alarmMode').addEventListener('change', onAlarmModeChange);
        overlay.querySelector('[data-action="do-create-alarm"]').addEventListener('click', doCreateAlarm);
        overlay.querySelector('[data-action="close-wf-sub"]').addEventListener('click', function() { overlay.remove(); });
    }

    function onAlarmModeChange() {
        var modeEl = document.getElementById('alarmMode');
        var container = document.getElementById('alarmModeParams');
        if (!modeEl || !container) return;
        var mode = modeEl.value;
        var html = '';
        if (mode === 'interval') {
            html = '<div class="form-group"><label>间隔秒数</label><input id="alarmInterval" type="number" min="60" value="3600"></div>';
        } else {
            html = '<div class="form-group"><label>时间 (HH:MM)</label><input id="alarmTime" type="time" value="09:00"></div>';
            if (mode === 'weekly') {
                html += '<div class="form-group"><label>星期几 (0=周一, 1=周二, ..., 6=周日)</label><input id="alarmWeekdays" placeholder="0,2,4"></div>';
            }
            if (mode === 'monthly') {
                html += '<div class="form-group"><label>每月几号</label><input id="alarmMonthDays" placeholder="1,15"></div>';
            }
        }
        container.innerHTML = html;
    }

    async function doCreateAlarm() {
        var label = document.getElementById('alarmLabel') ? document.getElementById('alarmLabel').value.trim() : '';
        var mode = document.getElementById('alarmMode') ? document.getElementById('alarmMode').value : 'daily';
        var message = document.getElementById('alarmMessage') ? document.getElementById('alarmMessage').value.trim() : '';
        if (!label) { showToast(t('alarm.enterName'), 'error'); return; }

        var body = {label: label, mode: mode, message: message};
        // max_triggers
        var maxTriggers = document.getElementById('alarmMaxTriggers') ? parseInt(document.getElementById('alarmMaxTriggers').value) || 0 : 0;
        body.max_triggers = maxTriggers;
        // workflow_id
        var workflowId = document.getElementById('alarmWorkflowId') ? document.getElementById('alarmWorkflowId').value.trim() : '';
        if (workflowId) body.workflow_id = workflowId;
        if (mode === 'interval') {
            body.interval_seconds = document.getElementById('alarmInterval') ? parseInt(document.getElementById('alarmInterval').value) || 3600 : 3600;
        } else {
            body.time = document.getElementById('alarmTime') ? document.getElementById('alarmTime').value || '09:00' : '09:00';
            if (mode === 'weekly') body.weekdays = document.getElementById('alarmWeekdays') ? document.getElementById('alarmWeekdays').value.split(',').map(Number) : [];
            if (mode === 'monthly') body.month_days = document.getElementById('alarmMonthDays') ? document.getElementById('alarmMonthDays').value.split(',').map(Number) : [];
        }

        try {
            const resp = await fetch('/api/alarms', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            const data = await resp.json();
            if (data.error || data.ok === false) { showToast(t('alarm.createFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); return; }
            showToast(t('alarm.created'), 'success');
            var overlay = document.getElementById('alarmFormOverlay');
            if (overlay) overlay.remove();
            showAlarms();
        } catch(e) { showToast(t('wf.createFailed') + ': ' + e.message, 'error'); }
    }

    // ── Sidebar Toggle (following ide pattern) ──────────
    var sidebarState = {
        el: null,
        overlay: null,
        open: false,
        touchStartX: 0,
        touchStartY: 0,
        swiping: false
    };

    function openSidebar() {
        var sidebar = sidebarState.el || document.getElementById('sidebar-left');
        var overlay = sidebarState.overlay || document.getElementById('sidebar-overlay');
        if (!sidebar) return;
        sidebar.classList.add('open');
        sidebarState.open = true;
        if (overlay) overlay.classList.remove('hidden');
    }

    function closeSidebar() {
        var sidebar = sidebarState.el || document.getElementById('sidebar-left');
        var overlay = sidebarState.overlay || document.getElementById('sidebar-overlay');
        if (!sidebar) return;
        sidebar.classList.remove('open');
        sidebarState.open = false;
        if (overlay) overlay.classList.add('hidden');
    }

    function toggleSidebar() {
        if (sidebarState.open) {
            closeSidebar();
        } else {
            openSidebar();
        }
    }

    function onSwipeStart(e) {
        var touch = e.touches[0];
        sidebarState.touchStartX = touch.clientX;
        sidebarState.touchStartY = touch.clientY;
        sidebarState.swiping = true;
    }

    function onSwipeEnd(e) {
        if (!sidebarState.swiping) return;
        sidebarState.swiping = false;
        var touch = e.changedTouches[0];
        var dx = touch.clientX - sidebarState.touchStartX;
        var dy = touch.clientY - sidebarState.touchStartY;
        var width = window.innerWidth;

        // Only handle horizontal swipes
        if (Math.abs(dx) < 50 || Math.abs(dy) > Math.abs(dx)) return;

        // Swipe from left edge -> open sidebar
        if (dx > 0 && sidebarState.touchStartX < 30 && !sidebarState.open) {
            openSidebar();
            return;
        }

        // Swipe left on open sidebar -> close
        if (dx < -60 && sidebarState.open) {
            closeSidebar();
            return;
        }
    }

    function initSidebar() {
        sidebarState.el = document.getElementById('sidebar-left');
        sidebarState.overlay = document.getElementById('sidebar-overlay');

        // Toggle button (☰) in chat header
        var toggleBtn = document.getElementById('btn-toggle-sidebar');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', function(e) {
                e.stopPropagation(); // Prevent chat-main click handler from closing it immediately
                toggleSidebar();
            });
        }

        // Close button (✕) in sidebar header
        var closeBtn = document.getElementById('btn-close-sidebar');
        if (closeBtn) closeBtn.addEventListener('click', closeSidebar);

        // Overlay click closes sidebar
        if (sidebarState.overlay) {
            sidebarState.overlay.addEventListener('click', closeSidebar);
        }

        // Escape key closes sidebar
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && sidebarState.open) {
                closeSidebar();
            }
        });

        // Swipe gestures on chat main area
        var chatMain = document.getElementById('chat-main');
        if (chatMain) {
            chatMain.addEventListener('touchstart', onSwipeStart, { passive: true });
            chatMain.addEventListener('touchend', onSwipeEnd, { passive: true });
        }

        // Also handle swipe on sidebar itself (to close by swiping left)
        if (sidebarState.el) {
            sidebarState.el.addEventListener('touchstart', onSwipeStart, { passive: true });
            sidebarState.el.addEventListener('touchend', onSwipeEnd, { passive: true });
        }
    }

    // ── Owner Settings Dialog ─────────────────────────────────
    function showOwnerDialog() {
        // Owner is now inside AICQ通信 dialog
        showAgentStatus();
    }

    function saveOwner() {
        var ownerId = document.getElementById('owner-id-input').value.trim();
        if (!ownerId) {
            showToast(t('owner.enterId'), 'error');
            return;
        }
        fetch('/api/owner', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ owner_id: ownerId })
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (data.error) {
                showToast(t('owner.setFailed') + ': ' + data.error, 'error');
            } else {
                var msg = t('owner.setSuccess');
                if (data.friend_sent) {
                    msg = t('owner.setSuccess') + '，' + (data.friend_msg || t('owner.friendSent'));
                } else if (data.friend_msg) {
                    msg = t('owner.setSuccess') + '，' + t('owner.friendReqFailed') + ': ' + data.friend_msg;
                } else {
                    msg = t('owner.friendFailed');
                }
                showToast(msg, data.friend_sent ? 'success' : 'info');
                var statusEl = document.getElementById('owner-status');
                if (statusEl) { statusEl.textContent = t('owner.currentOwner') + ': ' + ownerId; statusEl.style.color = 'var(--green)'; }
            }
        }).catch(function(e) {
            showToast(t('owner.setFailed') + ': ' + e.message, 'error');
        });
    }

    function getOwner() {
        fetch('/api/owner').then(function(r) { return r.json(); }).then(function(data) {
            var statusEl = document.getElementById('owner-status');
            if (data.owner_id) {
                document.getElementById('owner-id-input').value = data.owner_id;
                statusEl.textContent = t('owner.currentOwner') + ': ' + data.owner_id;
                statusEl.style.color = 'var(--green)';
            } else {
                statusEl.textContent = t('owner.noOwner');
                statusEl.style.color = 'var(--text-muted)';
            }
        }).catch(function() {
            document.getElementById('owner-status').textContent = t('owner.queryFailed');
            document.getElementById('owner-status').style.color = 'var(--red)';
        });
    }

    function clearOwner() {
        if (!confirm(t('owner.confirmClear'))) return;
        fetch('/api/owner', {
            method: 'DELETE'
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (data.error) {
                showToast(t('owner.clearFailed') + ': ' + data.error, 'error');
            } else {
                showToast(t('owner.cleared'), 'success');
                document.getElementById('owner-id-input').value = '';
                document.getElementById('owner-status').textContent = t('owner.cleared');
                document.getElementById('owner-status').style.color = 'var(--text-muted)';
            }
        }).catch(function(e) {
            showToast('清除失败: ' + e.message, 'error');
        });
    }

    function sendFriendRequestToOwner() {
        var ownerId = document.getElementById('owner-id-input').value.trim();
        if (!ownerId) {
            showToast(t('owner.enterOrBind'), 'error');
            return;
        }
        var btn = document.getElementById('btn-add-friend');
        if (btn) { btn.disabled = true; btn.textContent = t('owner.sending'); }
        fetch('/api/friends/request', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                to_id: ownerId,
                message: t('owner.friendMsg2')
            })
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (data.error) {
                var errMsg = data.error || '';
                // Check if already friends or request already sent
                if (errMsg.indexOf('already') >= 0 || errMsg.indexOf('已') >= 0 || errMsg.indexOf('409') >= 0 ||
                    errMsg.indexOf('exist') >= 0 || errMsg.indexOf('好友') >= 0 || errMsg.indexOf('重复') >= 0) {
                    showToast(t('owner.alreadyFriends'), 'success');
                } else {
                    showToast(t('owner.friendReqFailed') + ': ' + errMsg, 'error');
                }
            } else {
                showToast(t('owner.friendReqSent'), 'success');
            }
        }).catch(function(e) {
            showToast(t('owner.sendFailed') + ': ' + e.message, 'error');
        }).finally(function() {
            if (btn) { btn.disabled = false; btn.textContent = '📨 ' + t('owner.sendFriendReq'); }
        });
    }


    // ═══════════════════════════════════════════════════════════
    // ── Skills Management (本机技能 / ClawHub市场) ────────────
    // ═══════════════════════════════════════════════════════════

    const CLI_CAT_ICONS = {perception:'\u{1F441}',search:'\u{1F50D}',file:'\u{1F4C1}',document:'\u{1F4C4}',system:'\u{1F4BB}',browser:'\u{1F310}',gui:'\u{1F5A5}',memory:'\u{1F9E0}'};
    const CLI_CAT_LABELS = {perception:t('clicat.perception'),search:t('clicat.search'),file:t('clicat.file'),document:t('clicat.document'),system:t('clicat.system'),browser:t('clicat.browser'),gui:t('clicat.gui'),memory:t('clicat.memory')};

    let _skillsTab = 'local'; // local | market | installed
    let _clawhubSearchResults = [];
    let _clawhubSearching = false;

    async function showSkillsDialog() {
        if (showPanelOrDialog('skills', 'skills-dialog')) return;
        finishPanelOrDialog();
        renderSkillsContent();
    }

    async function renderSkillsContent() {
        var el = document.getElementById('skills-body');
        if (!el) return;

        // Tab navigation
        var html = '<div class="skills-tabs">' +
            '<div id="skillsTabLocal" class="skills-tab' + (_skillsTab==='local'?' skills-tab-active':'') + '">&#9881; 本机技能</div>' +
            '<div id="skillsTabMarket" class="skills-tab' + (_skillsTab==='market'?' skills-tab-active':'') + '">&#127978; ClawHub 市场</div>' +
            '<div id="skillsTabInstalled" class="skills-tab' + (_skillsTab==='installed'?' skills-tab-active':'') + '">&#128230; 已安装</div>' +
            '</div><div id="skillsTabContent"></div>';
        el.innerHTML = html;

        // Tab click handlers
        document.getElementById('skillsTabLocal').addEventListener('click', function() { _skillsTab='local'; renderSkillsContent(); });
        document.getElementById('skillsTabMarket').addEventListener('click', function() { _skillsTab='market'; renderSkillsContent(); });
        document.getElementById('skillsTabInstalled').addEventListener('click', function() { _skillsTab='installed'; renderSkillsContent(); });

        switch (_skillsTab) {
            case 'local': await renderLocalSkills(); break;
            case 'market': renderMarketTab(); break;
            case 'installed': await renderInstalledTab(); break;
        }
    }

    // ── 本机技能 Tab ──
    async function renderLocalSkills() {
        var el = document.getElementById('skillsTabContent');
        if (!el) return;

        try {
            var toolsResp = await fetch('/api/tools');
            var tools = await toolsResp.json();
            var skillsResp = await fetch('/api/skills?view=categorized');
            var cats = await skillsResp.json();

            if (cats.error && !tools.length) {
                el.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">加载失败: ' + escHtml(cats.error || t('wf.unknownError')) + '</div>';
                return;
            }

            var bt = cats.builtin_tools || [];
            var cc = cats.cli_commands || [];
            var ps = cats.python_skills || [];
            var sg = cats.skill_guides || [];
            // Also add tools from /api/tools as builtin if no categorized data
            if (!bt.length && tools.length) { bt = tools; }
            var total = bt.length + cc.length + ps.length + sg.length;
            var disabledAll = [].concat(ps, sg).filter(function(s) { return s.disabled; }).length;

            var html = '<div class="skills-header">' +
                '<div class="skills-summary">共 <strong>' + total + '</strong> 个工具/技能 (内置' + bt.length + ' · CLI' + cc.length + ' · Python' + ps.length + ' · 指南' + sg.length + ')' + (disabledAll > 0 ? ' · ' + disabledAll + ' 已禁用' : '') + '</div>' +
                '<input id="skillSearch" class="skills-search" placeholder="搜索工具/技能..." oninput="document.querySelectorAll(\'.skill-row\').forEach(function(el){var q=(document.getElementById(\'skillSearch\')?.value||\'\').toLowerCase();el.style.display=(el.dataset.name||\'\').toLowerCase().includes(q)||(el.dataset.desc||\'\').toLowerCase().includes(q)?\'\':\'none\'})"></div>';

            if (bt.length) {
                html += '<div class="skills-section">' +
                    '<div class="skills-section-header"><span>&#9881;</span> <strong>内置平台工具</strong> <span class="badge badge-blue">' + bt.length + '</span>' +
                    '<span class="skills-section-hint">LLM 直接调用 · 不可禁用</span></div>';
                for (var i = 0; i < bt.length; i++) { html += skillBuiltinRowHtml(bt[i]); }
                html += '</div>';
            }

            if (cc.length) {
                var groups = {};
                for (var i = 0; i < cc.length; i++) {
                    var cat = cc[i].category || 'other';
                    if (!groups[cat]) groups[cat] = [];
                    groups[cat].push(cc[i]);
                }
                html += '<div class="skills-section">' +
                    '<div class="skills-section-header"><span>&#9889;</span> <strong>CLI 命令</strong> <span class="badge badge-teal">' + cc.length + '</span>' +
                    '<span class="skills-section-hint">通过 command 工具调用 · 不可禁用</span></div>';
                var catKeys = Object.keys(groups);
                for (var ci = 0; ci < catKeys.length; ci++) {
                    var catKey = catKeys[ci];
                    var cmds = groups[catKey];
                    var icon = CLI_CAT_ICONS[catKey] || '&#128295;';
                    var label = CLI_CAT_LABELS[catKey] || catKey;
                    html += '<div class="skills-cat-label">' + icon + ' ' + label + ' (' + cmds.length + ')</div>';
                    for (var j = 0; j < cmds.length; j++) { html += skillCliCmdRowHtml(cmds[j]); }
                }
                html += '</div>';
            }

            if (ps.length) {
                html += '<div class="skills-section">' +
                    '<div class="skills-section-header"><span>&#128295;</span> <strong>Python 技能</strong> <span class="badge badge-green">' + ps.length + '</span>' +
                    '<span class="skills-section-hint">SkillRegistry 管理</span></div>';
                for (var i = 0; i < ps.length; i++) { html += skillPythonRowHtml(ps[i]); }
                html += '</div>';
            }

            if (sg.length) {
                html += '<div class="skills-section">' +
                    '<div class="skills-section-header"><span>&#128221;</span> <strong>技能指南</strong> <span class="badge badge-mauve">' + sg.length + '</span>' +
                    '<span class="skills-section-hint">Markdown Prompt · RAG 按需检索</span></div>';
                for (var i = 0; i < sg.length; i++) { html += skillGuideRowHtml(sg[i]); }
                html += '</div>';
            }

            if (!total) html += '<div style="text-align:center;color:var(--text-muted);padding:40px">暂无工具/技能</div>';
            el.innerHTML = html;
        } catch(e) {
            el.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">加载失败: ' + escHtml(e.message || '') + '</div>';
        }
    }

    // ── Skill Row HTML generators ──
    function skillBuiltinRowHtml(s) {
        var name = s.name || (s.Function && s.Function.name) || '';
        var desc = s.description || (s.Function && s.Function.description) || t('skills.noDesc');
        var safeName = escHtml(name);
        return '<div class="skill-row" data-name="' + safeName.toLowerCase() + '" data-desc="' + escHtml(desc).toLowerCase() + '">' +
            '<div class="skill-icon">&#9881;</div>' +
            '<div class="skill-info"><div class="skill-name">' + safeName + ' <span class="badge badge-blue">内置</span></div>' +
            '<div class="skill-desc">' + escHtml(desc) + '</div></div>' +
            '<div class="skill-actions"><button class="btn-secondary" style="font-size:11px;padding:4px 10px" onclick="viewSkillDetail(\'' + safeName.replace(/'/g, "\\'") + '\')">详情</button></div>' +
            '</div>';
    }

    function skillCliCmdRowHtml(s) {
        var name = s.name || '';
        var desc = s.description || '';
        var cli = s.cli ? '<code class="skill-cli-cmd">' + escHtml(s.cli) + '</code>' : '';
        var safeName = escHtml(name);
        return '<div class="skill-row skill-row-indent" data-name="' + safeName.toLowerCase() + '" data-desc="' + escHtml(desc).toLowerCase() + '">' +
            '<div class="skill-info"><div class="skill-name">' + safeName + ' <span class="badge badge-teal">CLI</span></div>' +
            '<div class="skill-desc">' + escHtml(desc) + ' ' + cli + '</div></div>' +
            '<div class="skill-actions"><button class="btn-secondary" style="font-size:11px;padding:4px 10px" onclick="viewSkillDetail(\'' + safeName.replace(/'/g, "\\'") + '\')">详情</button></div>' +
            '</div>';
    }

    function skillPythonRowHtml(s) {
        var isDisabled = s.disabled;
        var name = s.name || '';
        var desc = s.description || t('skills.noDesc');
        var safeName = escHtml(name);
        var paramCount = (s.parameters || []).length;
        var paramTag = paramCount > 0 ? '<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary)">' + paramCount + ' 参数</span>' : '';
        return '<div class="skill-row" data-name="' + safeName.toLowerCase() + '" data-desc="' + escHtml(desc).toLowerCase() + '">' +
            '<div class="skill-icon">&#128295;</div>' +
            '<div class="skill-info"><div class="skill-name">' + safeName + (isDisabled ? ' <span class="badge badge-red">已禁用</span>' : '') + (s.dangerous ? ' <span class="badge badge-yellow">&#9888; 危险</span>' : '') + '</div>' +
            '<div class="skill-desc">' + escHtml(desc) + '</div></div>' +
            '<div class="skill-actions">' + paramTag +
            '<label class="toggle-switch" title="' + (isDisabled?t('skills.enable'):t('skills.disableSkill')) + '">' +
            '<input type="checkbox" ' + (!isDisabled?'checked':'') + ' onchange="toggleSkill(\'' + safeName.replace(/'/g, "\\'") + '\',this.checked)">' +
            '<span class="toggle-slider' + (isDisabled?'':' toggle-on') + '"></span>' +
            '<span class="toggle-knob' + (isDisabled?'':' knob-on') + '"></span>' +
            '</label>' +
            '<button class="btn-secondary" style="font-size:11px;padding:4px 10px" onclick="viewSkillDetail(\'' + safeName.replace(/'/g, "\\'") + '\')">详情</button></div>' +
            '</div>';
    }

    function skillGuideRowHtml(s) {
        var isDisabled = s.disabled;
        var name = s.name || '';
        var desc = s.description || t('skills.noDesc');
        var safeName = escHtml(name);
        var refTag = (s.references || []).length > 0 ? '<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary)">' + s.references.length + ' 参考资料</span>' : '';
        return '<div class="skill-row" data-name="' + safeName.toLowerCase() + '" data-desc="' + escHtml(desc).toLowerCase() + '">' +
            '<div class="skill-icon">&#128221;</div>' +
            '<div class="skill-info"><div class="skill-name">' + safeName + (isDisabled ? ' <span class="badge badge-red">已禁用</span>' : '') + '</div>' +
            '<div class="skill-desc">' + escHtml(desc) + '</div></div>' +
            '<div class="skill-actions">' + refTag +
            '<label class="toggle-switch" title="' + (isDisabled?t('skills.enable'):t('skills.disableSkill')) + '">' +
            '<input type="checkbox" ' + (!isDisabled?'checked':'') + ' onchange="toggleSkill(\'' + safeName.replace(/'/g, "\\'") + '\',this.checked)">' +
            '<span class="toggle-slider' + (isDisabled?'':' toggle-on') + '"></span>' +
            '<span class="toggle-knob' + (isDisabled?'':' knob-on') + '"></span>' +
            '</label>' +
            '<button class="btn-secondary" style="font-size:11px;padding:4px 10px" onclick="viewSkillDetail(\'' + safeName.replace(/'/g, "\\'") + '\')">详情</button></div>' +
            '</div>';
    }

    // ── ClawHub Market Tab ──
    function renderMarketTab() {
        var el = document.getElementById('skillsTabContent');
        if (!el) return;
        var html = '<div class="market-search-row">' +
            '<div class="market-search-input-wrap"><input id="clawhubSearchInput" class="market-search-input" placeholder="搜索 ClawHub 技能市场... (如 pdf, chart, seo)" onkeydown="if(event.key===\'Enter\')doClawhubSearch()">' +
            '<button class="btn-primary" id="clawhubSearchBtn" onclick="doClawhubSearch()" style="white-space:nowrap">&#128269; 搜索</button></div>' +
            '<div class="market-sort-btns">' +
            '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="doClawhubExplore(\'newest\')">&#128336; 最新</button>' +
            '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="doClawhubExplore(\'trending\')">&#128293; 热门</button>' +
            '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="doClawhubExplore(\'downloads\')">&#128229; 下载</button></div></div>' +
            '<div style="font-size:12px;color:var(--text-muted);margin-bottom:12px">ClawHub 是 OpenClaw 生态的技能注册中心，包含 13,000+ 社区技能。安装后会自动注册到本地技能系统。</div>' +
            '<div id="clawhubResults"></div>';
        el.innerHTML = html;
    }

    async function doClawhubSearch() {
        var input = document.getElementById('clawhubSearchInput');
        var q = input ? input.value.trim() : '';
        if (!q) { showToast(t('market.enterKeyword'), 'error'); return; }
        var resultsEl = document.getElementById('clawhubResults');
        if (!resultsEl) return;
        resultsEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:40px"><div style="font-size:24px;margin-bottom:8px">&#9203;</div>正在搜索 ClawHub...</div>';
        _clawhubSearching = true;
        try {
            var r = await fetch('/api/clawhub/search?q=' + encodeURIComponent(q) + '&limit=30');
            var data = await r.json();
            _clawhubSearching = false;
            if (data.error) { resultsEl.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">搜索失败: ' + escHtml(data.error) + '</div>'; return; }
            _clawhubSearchResults = data.results || [];
            renderClawhubResults(_clawhubSearchResults, q);
        } catch(e) {
            _clawhubSearching = false;
            resultsEl.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">搜索出错: ' + escHtml(e.message || '') + '</div>';
        }
    }

    async function doClawhubExplore(sort) {
        var resultsEl = document.getElementById('clawhubResults');
        if (!resultsEl) return;
        resultsEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:40px"><div style="font-size:24px;margin-bottom:8px">&#9203;</div>正在浏览 ' + sort + '...</div>';
        try {
            var r = await fetch('/api/clawhub/explore?sort=' + sort + '&limit=30');
            var data = await r.json();
            if (data.error) { resultsEl.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">浏览失败: ' + escHtml(data.error) + '</div>'; return; }
            _clawhubSearchResults = data.results || [];
            renderClawhubResults(_clawhubSearchResults, sort);
        } catch(e) {
            resultsEl.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">浏览出错: ' + escHtml(e.message || '') + '</div>';
        }
    }

    function renderClawhubResults(results, query) {
        var el = document.getElementById('clawhubResults');
        if (!el) return;
        if (!results || results.length === 0) {
            el.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:40px">未找到匹配的技能</div>';
            return;
        }
        var html = '<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">找到 ' + results.length + ' 个技能</div>';
        html += '<div style="display:flex;flex-direction:column;gap:8px">';
        for (var i = 0; i < results.length; i++) {
            var s = results[i];
            var slug = s.slug || s.name || s.id || '';
            var name = s.name || s.displayName || s.slug || slug;
            var desc = s.description || s.summary || '';
            // ClawHub API returns owner as object {handle, displayName} or string
            var author = '';
            if (s.owner && typeof s.owner === 'object') { author = s.owner.handle || s.owner.displayName || ''; }
            else if (typeof s.owner === 'string') { author = s.owner; }
            else if (s.author) { author = s.author; }
            // ClawHub API returns tags.latest for version
            var version = s.version || '';
            if (!version && s.tags && typeof s.tags === 'object') { version = s.tags.latest || ''; }
            // ClawHub API returns stats.downloads
            var downloads = 0;
            if (s.stats && typeof s.stats === 'object') { downloads = s.stats.downloads || s.stats.installsAllTime || 0; }
            else { downloads = s.downloads || s.installCount || 0; }
            var safeSlug = escHtml(slug);
            html += '<div class="clawhub-result-card">' +
                '<div class="clawhub-result-icon">&#129513;</div>' +
                '<div class="clawhub-result-info"><div class="skill-name">' + escHtml(name) + ' <span class="badge badge-green">ClawHub</span>' + (version ? ' <span class="clawhub-version">v' + escHtml(version) + '</span>' : '') + '</div>' +
                '<div class="skill-desc">' + escHtml(desc.slice(0, 150)) + (desc.length > 150 ? '...' : '') + '</div>' +
                '<div class="clawhub-meta">' + (author ? '<span>&#128100; ' + escHtml(author) + '</span>' : '') + (downloads ? '<span>&#128229; ' + downloads + '</span>' : '') + (slug ? '<span style="color:var(--accent)">clawhub.ai/' + escHtml(slug) + '</span>' : '') + '</div></div>' +
                '<div class="clawhub-result-actions">' +
                '<button class="btn-primary" style="font-size:12px;padding:6px 12px" onclick="installClawhubSkill(\'' + safeSlug.replace(/'/g, "\\'") + '\')" id="installBtn_' + safeSlug.replace(/[^a-zA-Z0-9]/g, '_') + '">&#128229; 安装</button>' +
                '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="viewClawhubDetail(\'' + safeSlug.replace(/'/g, "\\'") + '\')">详情</button></div></div>';
        }
        html += '</div>';
        el.innerHTML = html;
    }

    async function installClawhubSkill(slug) {
        if (!slug) return;
        var btnId = 'installBtn_' + slug.replace(/[^a-zA-Z0-9]/g, '_');
        var btn = document.getElementById(btnId);
        if (btn) { btn.disabled = true; btn.textContent = '⏳ ' + t('market.installing'); }
        try {
            var r = await fetch('/api/clawhub/install/' + encodeURIComponent(slug), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) });
            var data = await r.json();
            if (data.ok) {
                showToast(t('market.installSuccess', slug), 'success');
                if (btn) { btn.textContent = '✅ ' + t('market.installed'); btn.className = 'btn-secondary'; btn.disabled = true; }
            } else {
                showToast(t('market.installFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
                if (btn) { btn.textContent = '📥 安装'; btn.disabled = false; }
            }
        } catch(e) {
            showToast(t('market.installError') + ': ' + (e.message || ''), 'error');
            if (btn) { btn.textContent = '📥 安装'; btn.disabled = false; }
        }
    }

    async function viewClawhubDetail(slug) {
        if (!slug) return;
        showToast(t('skills.gettingDetail'), 'info');
        try {
            var r = await fetch('/api/clawhub/info/' + encodeURIComponent(slug));
            var s = await r.json();
            if (s.error) { showToast(s.error, 'error'); return; }
            var mod = s.moderation || {};
            var verdict = mod.verdict || 'unknown';
            var verdictBadge = verdict === 'clean' ? '<span style="background:var(--green);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">安全</span>' :
                verdict === 'suspicious' ? '<span style="background:var(--orange);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">可疑</span>' :
                verdict === 'malicious' ? '<span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">恶意</span>' :
                '<span style="background:var(--bg-tertiary);color:var(--text-secondary);padding:2px 8px;border-radius:8px;font-size:11px">未知</span>';

            var html = '<div style="padding:20px;max-width:560px;margin:0 auto">' +
                '<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:16px">&#129513; ' + escHtml(s.name || s.slug || slug) + ' <span style="background:var(--green);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">ClawHub</span></h3>' +
                '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">描述</label><div style="font-size:13px">' + escHtml(s.description || s.summary || t('skills.no2')) + '</div></div>' +
                '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap"><div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">Slug</label><code style="font-size:12px;background:var(--bg-tertiary);padding:2px 6px;border-radius:3px">' + escHtml(s.slug || slug) + '</code></div>' +
                '<div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">版本</label><span style="font-size:13px">' + escHtml(s.version || 'latest') + '</span></div></div>' +
                (s.author || s.owner ? '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap"><div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">作者</label><span style="font-size:13px">' + escHtml(s.author || s.owner || '') + '</span></div>' +
                '<div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">许可证</label><span style="font-size:13px">' + escHtml(s.license || '') + '</span></div></div>' : '') +
                '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap"><div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">安全扫描</label>' + verdictBadge + ' ' + (mod.summary ? escHtml(mod.summary) : '') + '</div>' +
                '<div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">下载量</label><span style="font-size:13px">' + (s.downloads || s.installCount || 0) + '</span></div></div>' +
                '<div style="text-align:right;margin-top:16px"><button class="btn-primary" style="font-size:13px;padding:8px 16px" onclick="installClawhubSkill(\'' + escHtml(s.slug || slug).replace(/'/g, "\\'") + '\');this.closest(\'.skill-detail-modal\').remove()">&#128229; 安装此技能</button> ' +
                '<button class="btn-secondary" style="font-size:13px;padding:8px 16px" onclick="this.closest(\'.skill-detail-modal\').remove()">关闭</button></div></div>';

            var overlay = document.createElement('div');
            overlay.className = 'skill-detail-modal';
            overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10001';
            overlay.innerHTML = '<div style="background:var(--bg-primary);border-radius:var(--radius);max-height:80vh;overflow:auto;box-shadow:0 8px 32px rgba(0,0,0,0.2);max-width:90vw">' + html + '</div>';
            overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
            document.body.appendChild(overlay);
        } catch(e) {
            showToast(t('skills.getDetailFailed') + ': ' + (e.message || ''), 'error');
        }
    }

    // ── Installed Market Skills Tab ──
    async function renderInstalledTab() {
        var el = document.getElementById('skillsTabContent');
        if (!el) return;
        el.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:40px"><div style="font-size:24px;margin-bottom:8px">&#9203;</div>正在加载...</div>';
        try {
            var r = await fetch('/api/clawhub/installed');
            var data = await r.json();
            if (data.error) { el.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">加载失败: ' + escHtml(data.error) + '</div>'; return; }
            var installed = data.installed || [];
            if (installed.length === 0) {
                el.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:40px">' +
                    '<div style="font-size:32px;margin-bottom:12px">&#128230;</div>' +
                    '<div style="font-size:14px;color:var(--text-secondary);margin-bottom:12px">尚未从 ClawHub 安装任何技能</div>' +
                    '<button class="btn-primary" onclick="switchSkillsTab(\'market\')">&#127978; 去市场看看</button></div>';
                return;
            }
            var html = '<div class="installed-header">' +
                '<div style="font-size:13px;color:var(--text-secondary)">已安装 <strong>' + installed.length + '</strong> 个 ClawHub 技能</div>' +
                '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="updateAllClawhubSkills()">&#128260; 全部更新</button></div>';
            html += '<div style="display:flex;flex-direction:column;gap:8px">';
            for (var i = 0; i < installed.length; i++) {
                var s = installed[i];
                var slug = s.slug || '';
                var name = s.skill_name || s.name || slug;
                var desc = s.description || '';
                var version = s.version || '';
                var exists = s.exists !== false;
                var date = s.installed_at ? new Date(s.installed_at * 1000).toLocaleDateString() : '';
                var safeSlug = escHtml(slug);
                html += '<div class="clawhub-result-card' + (!exists ? ' clawhub-missing' : '') + '">' +
                    '<div class="clawhub-result-icon">' + (exists ? '&#129513;' : '&#9888;') + '</div>' +
                    '<div class="clawhub-result-info"><div class="skill-name">' + escHtml(name) + ' <span class="badge badge-green">ClawHub</span>' + (version ? ' <span class="clawhub-version">v' + escHtml(version) + '</span>' : '') + (!exists ? ' <span class="badge badge-red">文件缺失</span>' : '') + '</div>' +
                    '<div class="skill-desc">' + escHtml(desc.slice(0, 150)) + (desc.length > 150 ? '...' : '') + '</div>' +
                    '<div class="clawhub-meta">' + (date ? '<span>&#128197; 安装于 ' + date + '</span>' : '') + '<span style="color:var(--accent)">' + escHtml(slug) + '</span></div></div>' +
                    '<div class="clawhub-result-actions">' +
                    (exists ? '<button class="btn-secondary" style="font-size:12px;padding:6px 12px" onclick="updateClawhubSkill(\'' + safeSlug.replace(/'/g, "\\'") + '\')">&#128260; 更新</button>' : '') +
                    '<button class="btn-secondary" style="font-size:12px;padding:6px 12px;color:var(--red)" onclick="uninstallClawhubSkill(\'' + safeSlug.replace(/'/g, "\\'") + '\')">&#128465; 卸载</button></div></div>';
            }
            html += '</div>';
            el.innerHTML = html;
        } catch(e) {
            el.innerHTML = '<div style="color:var(--red);text-align:center;padding:20px">加载失败: ' + escHtml(e.message || '') + '</div>';
        }
    }

    async function toggleSkill(name, enabled) {
        try {
            var r = await fetch('/api/skills/' + encodeURIComponent(name) + '/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled }) });
            var data = await r.json();
            if (data.error) { showToast(data.error, 'error'); return; }
            showToast(enabled ? t('wf.enabled') : t('skills.disabled'), 'success');
            renderLocalSkills();
        } catch(e) { showToast(t('skills.enableFailed') + ': ' + (e.message || ''), 'error'); }
    }

    async function viewSkillDetail(name) {
        showToast(t('skills.gettingDetail'), 'info');
        try {
            var r = await fetch('/api/skills/' + encodeURIComponent(name));
            var s = await r.json();
            if (s.error) { showToast(s.error, 'error'); return; }

            var st = s.skill_type || '';
            var typeBadge = '';
            if (st === 'builtin_platform' || st === 'builtin_agent_tool') {
                typeBadge = '<span style="background:var(--blue);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">内置平台</span>';
            } else if (st === 'cli_command') {
                typeBadge = '<span style="background:var(--teal);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">CLI 命令</span>';
            } else if (st === 'markdown') {
                typeBadge = '<span style="background:var(--mauve);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">技能指南</span>';
            } else {
                typeBadge = '<span style="background:var(--green);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">Python 技能</span>';
            }

            var html = '<div style="padding:20px;max-width:560px;margin:0 auto">' +
                '<h3 style="margin-bottom:16px">' + escHtml(s.name) + '</h3>' +
                '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">描述</label><div style="font-size:13px">' + escHtml(s.description || t('skills.no2')) + '</div></div>' +
                '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap"><div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">类型</label>' + typeBadge + '</div>' +
                '<div><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">类别</label><span style="background:var(--blue);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">' + escHtml(s.category || 'general') + '</span></div></div>' +
                (s.note ? '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">备注</label><div style="font-size:13px;color:var(--text-secondary)">' + escHtml(s.note) + '</div></div>' : '') +
                '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px">危险操作</label>' + (s.dangerous ? '<span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">是</span>' : '<span style="background:var(--green);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px">否</span>') + '</div>';

            if (s.parameters && s.parameters.length > 0) {
                html += '<div style="margin-bottom:12px"><label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:8px">参数 (' + s.parameters.length + ')</label>' +
                    '<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">' +
                    '<tr style="border-bottom:1px solid var(--border)"><th style="text-align:left;padding:6px 8px">名称</th><th style="text-align:left;padding:6px 8px">类型</th><th style="text-align:left;padding:6px 8px">必需</th><th style="text-align:left;padding:6px 8px">描述</th></tr>';
                for (var i = 0; i < s.parameters.length; i++) {
                    var p = s.parameters[i];
                    html += '<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 8px"><code>' + escHtml(p.name) + '</code></td><td style="padding:6px 8px">' + (p.type || 'string') + '</td><td style="padding:6px 8px">' + (p.required ? '✅' : '❌') + '</td><td style="padding:6px 8px">' + escHtml(p.description || '') + '</td></tr>';
                }
                html += '</table></div></div>';
            }

            html += '<div style="text-align:right;margin-top:16px"><button class="btn-secondary" onclick="this.closest(\'.skill-detail-modal\').remove()">关闭</button></div></div>';

            var overlay = document.createElement('div');
            overlay.className = 'skill-detail-modal';
            overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10001';
            overlay.innerHTML = '<div style="background:var(--bg-primary);border-radius:var(--radius);max-height:80vh;overflow:auto;box-shadow:0 8px 32px rgba(0,0,0,0.2);max-width:90vw">' + html + '</div>';
            overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
            document.body.appendChild(overlay);
        } catch(e) {
            showToast(t('skills.getDetailFailed') + ': ' + (e.message || ''), 'error');
        }
    }

    async function uninstallClawhubSkill(slug) {
        if (!confirm(t('market.confirmUninstall', slug))) return;
        try {
            var r = await fetch('/api/clawhub/uninstall/' + encodeURIComponent(slug), { method: 'POST' });
            var data = await r.json();
            if (data.ok) { showToast(t('market.uninstalled', slug), 'success'); renderInstalledTab(); }
            else { showToast(t('market.uninstallFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); }
        } catch(e) { showToast(t('market.uninstallError') + ': ' + (e.message || ''), 'error'); }
    }

    async function updateClawhubSkill(slug) {
        showToast(t('market.updating', slug), 'info');
        try {
            var r = await fetch('/api/clawhub/update/' + encodeURIComponent(slug), { method: 'POST' });
            var data = await r.json();
            if (data.ok) { showToast(t('market.updateDone', slug), 'success'); renderInstalledTab(); }
            else { showToast(t('market.updateFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); }
        } catch(e) { showToast(t('market.updateError') + ': ' + (e.message || ''), 'error'); }
    }

    async function updateAllClawhubSkills() {
        showToast(t('market.updatingAll'), 'info');
        try {
            var r = await fetch('/api/clawhub/update/', { method: 'POST' });
            var data = await r.json();
            if (data.ok) { showToast(t('market.allUpdated'), 'success'); renderInstalledTab(); }
            else { showToast(t('market.updateFailed') + ': ' + (data.error || t('wf.unknownError')), 'error'); }
        } catch(e) { showToast(t('market.updateError') + ': ' + (e.message || ''), 'error'); }
    }

    // ── Customer Service (客服模式) ─────────────────────────────

    function showCustomerService() {
        if (showPanelOrDialog('cs', 'cs-dialog')) return;
        // Load current config
        fetch('/api/customer-service').then(function(r) { return r.json(); }).then(function(cfg) {
            var isEnabled = cfg.enabled || false;
            var currentMode = cfg.mode || 'domain';
            var currentDomains = cfg.domains || '';
            var currentPrompt = cfg.system_prompt || '';
            var currentTools = cfg.enabled_tools || [];
            var currentTransfer = cfg.transfer_to_human || false;
            var currentTarget = cfg.transfer_target || '';

            // Update UI
            var banner = document.getElementById('cs-status-banner');
            if (banner) banner.style.display = isEnabled ? 'flex' : 'none';
            if (isEnabled) {
                var statusText = document.getElementById('cs-status-text');
                if (statusText) statusText.textContent = t('cs.enabledWithMode') + '（' + (currentMode === 'domain' ? t('cs.domainRestricted') : t('cs.openAccess')) + '）';
            }

            // Set radio buttons
            var radios = document.querySelectorAll('input[name="csMode"]');
            radios.forEach(function(r) { r.checked = (r.value === currentMode); });

            // Domain input
            var domainInput = document.getElementById('cs-domain-input');
            if (domainInput) domainInput.value = currentDomains;

            // System prompt
            var promptEl = document.getElementById('cs-system-prompt');
            if (promptEl) promptEl.value = currentPrompt;

            // Transfer
            var transferCb = document.getElementById('cs-transfer-enabled');
            if (transferCb) transferCb.checked = currentTransfer;
            var transferTarget = document.getElementById('cs-transfer-target');
            if (transferTarget) transferTarget.value = currentTarget;

            // Show/hide sections
            toggleCsDomainSection();
            toggleCsTransferSection();

            // Add change listeners for radio buttons
            document.querySelectorAll('input[name="csMode"]').forEach(function(r) {
                r.removeEventListener('change', toggleCsDomainSection);
                r.addEventListener('change', toggleCsDomainSection);
            });
            var transferCb = document.getElementById('cs-transfer-enabled');
            if (transferCb) {
                transferCb.removeEventListener('change', toggleCsTransferSection);
                transferCb.addEventListener('change', toggleCsTransferSection);
            }

            // Populate tools
            populateCsTools(currentTools);

            // Save button text
            var saveBtn = document.getElementById('btn-save-cs');
            var cancelBtn = document.getElementById('btn-cancel-cs');
            if (saveBtn) saveBtn.textContent = isEnabled ? t('cs.updateCS') : t('cs.enableCS');
            if (cancelBtn) cancelBtn.style.display = isEnabled ? '' : 'none';

            // Embed code
            if (isEnabled) showCsEmbedCode();
            finishPanelOrDialog();
        }).catch(function() { finishPanelOrDialog(); });
    }

    function toggleCsDomainSection() {
        var mode = document.querySelector('input[name="csMode"]:checked');
        var section = document.getElementById('csDomainSection');
        if (section) section.style.display = (mode && mode.value === 'domain') ? '' : 'none';
    }

    function toggleCsTransferSection() {
        var cb = document.getElementById('cs-transfer-enabled');
        var section = document.getElementById('cs-transfer-section');
        if (section) section.style.display = (cb && cb.checked) ? '' : 'none';
    }

    function populateCsTools(selectedTools) {
        var container = document.getElementById('cs-tools-list');
        if (!container) return;
        var tools = [
            {id: 'command', name: t('cs.toolCommand')},
            {id: 'web_search', name: t('cs.toolWebSearch')},
            {id: 'web_reader', name: t('cs.toolWebReader')},
            {id: 'file-send', name: t('cs.toolFileSend')},
            {id: 'save_memory', name: t('cs.toolSaveMemory')},
            {id: 'recall_memory', name: t('cs.toolRecallMemory')}
        ];
        var hasSaved = Array.isArray(selectedTools) && selectedTools.length > 0;
        container.innerHTML = tools.map(function(t) {
            var isChecked = hasSaved ? selectedTools.indexOf(t.id) >= 0 : true;
            return '<label style="display:flex;align-items:center;gap:4px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:12px;background:var(--bg-tertiary)">' +
                '<input type="checkbox" value="' + t.id + '" ' + (isChecked ? 'checked' : '') + '> ' + t.name + '</label>';
        }).join('');
    }

    function showCsEmbedCode() {
        var section = document.getElementById('cs-embed-section');
        var codeEl = document.getElementById('cs-embed-code');
        if (!section || !codeEl) return;
        var accountId = '';
        try { accountId = localStorage.getItem('aicqbot_account_id') || ''; } catch(e) {}
        var embedCode = '<!-- AICQ Customer Service Widget -->\n' +
            '<script src="https://aicq.me/static/cs-widget.js"><\/script>\n' +
            '<script>\n' +
            '  AicqCS.init({ accountId: "' + accountId + '", theme: "light" });\n' +
            '<\/script>';
        codeEl.value = embedCode;
        section.style.display = '';
    }

    function saveCustomerService() {
        var mode = document.querySelector('input[name="csMode"]:checked');
        var domains = (mode && mode.value === 'domain') ? (document.getElementById('cs-domain-input')?.value?.trim() || '') : '';
        var systemPrompt = document.getElementById('cs-system-prompt')?.value?.trim() || '';
        var toolCheckboxes = document.querySelectorAll('#cs-tools-list input[type="checkbox"]');
        var enabledTools = Array.from(toolCheckboxes).filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
        var transferToHuman = document.getElementById('cs-transfer-enabled')?.checked || false;
        var transferTarget = document.getElementById('cs-transfer-target')?.value?.trim() || '';

        if (mode && mode.value === 'domain' && !domains) {
            showToast(t('cs.enterDomain'), 'error');
            return;
        }
        if (transferToHuman && !transferTarget) {
            showToast(t('cs.enterTransferTarget'), 'error');
            return;
        }

        fetch('/api/customer-service', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                enabled: true,
                mode: mode ? mode.value : 'open',
                domains: domains,
                system_prompt: systemPrompt,
                enabled_tools: enabledTools,
                transfer_to_human: transferToHuman,
                transfer_target: transferTarget
            })
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (data.status === 'ok') {
                showToast(t('cs.saveSuccess'), 'success');
                closeDialog('cs-dialog');
            } else {
                showToast(t('cs.saveFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
            }
        }).catch(function(e) {
            showToast(t('owner.setFailed') + ': ' + e.message, 'error');
        });
    }

    function cancelCustomerService() {
        if (!confirm(t('cs.confirmCancel'))) return;
        fetch('/api/customer-service', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ enabled: false })
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (data.status === 'ok') {
                showToast(t('cs.cancelSuccess'), 'info');
                closeDialog('cs-dialog');
            } else {
                showToast(t('cs.cancelFailed') + ': ' + (data.error || t('wf.unknownError')), 'error');
            }
        }).catch(function(e) {
            showToast(t('cs.cancelFailed') + ': ' + e.message, 'error');
        });
    }

    // ── TTS Settings Dialog ─────────────────────────────────
    function showTTSSettings() {
        if (showPanelOrDialog('tts', 'tts-settings-dialog')) return;
        fetch('/api/tts/config').then(function(r) { return r.json(); }).then(function(cfg) {
            var engineSelect = document.getElementById('tts-engine-select');
            var serverUrl = document.getElementById('tts-server-url');
            var serverGroup = document.getElementById('tts-server-url-group');
            var speedRange = document.getElementById('tts-speed');
            var speedVal = document.getElementById('tts-speed-val');
            var autoRead = document.getElementById('tts-auto-read');

            if (engineSelect) engineSelect.value = (cfg.engine || 'edgetts');
            if (serverUrl) serverUrl.value = (cfg.server_url || '');
            if (serverGroup) serverGroup.style.display = (cfg.engine === 'indextts') ? '' : 'none';
            if (speedRange) speedRange.value = (cfg.speed || 1.0);
            if (speedVal) speedVal.textContent = (cfg.speed || 1.0);
            if (autoRead) autoRead.checked = !!cfg.auto_read;

            var voiceSelect = document.getElementById('tts-voice-select');
            if (voiceSelect) {
                // Show Edge TTS voices for edgetts engine
                var isEdge = (cfg.engine || 'edgetts') === 'edgetts';
                if (isEdge) {
                    voiceSelect.innerHTML = '';
                    var voices = [
                        { name: t('tts.voiceXiaoxiao'), value: '0' },
                        { name: t('tts.voiceYunxi'), value: '1' },
                        { name: t('tts.voiceYunjian'), value: '2' }
                    ];
                    voices.forEach(function(v, i) {
                        var opt = document.createElement('option');
                        opt.value = v.value;
                        opt.textContent = v.name;
                        if (cfg.voice_index === i) opt.selected = true;
                        voiceSelect.appendChild(opt);
                    });
                } else {
                    // Non-edge engine: show Android TTS or browser voice options
                    voiceSelect.innerHTML = '';
                    // Update the display text based on current readiness
                    var currentTTSStatus = _androidTTSReady ? t('tts.androidVoiceReady') : t('tts.androidVoiceInit');
                    if (typeof window.AndroidTTS !== 'undefined') {
                        try {
                            if (window.AndroidTTS.isAvailable()) {
                                currentTTSStatus = t('tts.androidVoiceReady');
                                _androidTTSReady = true;
                            }
                        } catch(e) {}
                    }
                    if (typeof window.AndroidTTS !== 'undefined' && (_androidTTSReady || typeof window.onTTSReady === 'function')) {
                        var opt = document.createElement('option');
                        opt.value = '0';
                        opt.textContent = currentTTSStatus;
                        opt.id = 'android-tts-voice-option';
                        voiceSelect.appendChild(opt);
                    } else if (typeof speechSynthesis !== 'undefined' && speechSynthesis) {
                        var browserVoices = speechSynthesis.getVoices();
                        browserVoices.forEach(function(v, i) {
                            var opt = document.createElement('option');
                            opt.value = i;
                            opt.textContent = v.name + ' (' + v.lang + ')';
                            if (cfg.voice_index === i) opt.selected = true;
                            voiceSelect.appendChild(opt);
                        });
                    } else {
                        var opt = document.createElement('option');
                        opt.value = '0';
                        opt.textContent = t('tts.noVoice');
                        voiceSelect.appendChild(opt);
                    }
                }
            }
        }).catch(function() {});

        finishPanelOrDialog();
    }

    function saveTTSSettings() {
        var cfg = {
            engine: document.getElementById('tts-engine-select').value,
            server_url: document.getElementById('tts-server-url').value,
            speed: parseFloat(document.getElementById('tts-speed').value),
            auto_read: document.getElementById('tts-auto-read').checked,
            voice_index: parseInt(document.getElementById('tts-voice-select').value) || 0
        };
        fetch('/api/tts/config', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(cfg)
        }).then(function(r) {
            if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || 'Save failed'); });
            return r.json();
        }).then(function() {
            // Sync local TTS state
            ttsEngine = cfg.engine;
            ttsServerUrl = cfg.server_url;
            ttsSpeed = cfg.speed;
            ttsAutoRead = cfg.auto_read;
            ttsVoiceIndex = cfg.voice_index;
            showToast(t('tts.saved'), 'success');
            document.getElementById('tts-settings-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        }).catch(function(e) {
            showToast(t('tts.saveFailed') + ': ' + e.message, 'error');
        });
    }

    function testTTS() {
        var speed = document.getElementById('tts-speed') ? parseFloat(document.getElementById('tts-speed').value) : 1.0;
        var engineSelect = document.getElementById('tts-engine-select');
        var currentEngine = engineSelect ? engineSelect.value : ttsEngine;

        // IndexTTS
        if (currentEngine === 'indextts' && ttsServerUrl) {
            fetch(ttsServerUrl + '/tts', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ text: t('tts.testText'), speed: speed })
            }).then(function(r) { return r.blob(); }).then(function(blob) {
                var audio = new Audio(URL.createObjectURL(blob));
                audio.play().catch(function() { showToast(t('tts.playFailed'), 'error'); });
                showToast(t('tts.indexTesting'), 'success');
            }).catch(function(e) {
                showToast('Index TTS测试失败: ' + e.message, 'error');
            });
            return;
        }

        // Edge TTS (default and primary)
        var voiceSelect = document.getElementById('tts-voice-select');
        var ttsVoice = 'zh-CN-XiaoxiaoNeural';
        if (voiceSelect) {
            var vi = parseInt(voiceSelect.value) || 0;
            ttsVoice = vi === 1 ? 'zh-CN-YunxiNeural' : (vi === 2 ? 'zh-CN-YunjianNeural' : 'zh-CN-XiaoxiaoNeural');
        }
        fetch('/api/tts/speak', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text: t('tts.testText'), voice: ttsVoice, speed: speed })
        }).then(function(r) {
            if (!r.ok) throw new Error(t('tts.requestFailed2') + '(HTTP ' + r.status + ')');
            return r.blob();
        }).then(function(blob) {
            if (blob.size < 100) throw new Error(t('tts.dataError'));
            var audio = new Audio(URL.createObjectURL(blob));
            audio.play().catch(function() { showToast(t('tts.playFailed'), 'error'); });
            showToast(t('tts.edgeTesting'), 'success');
        }).catch(function(e) {
            // Edge TTS failed, fallback chain: Android TTS > Browser SpeechSynthesis
            console.warn('[TTS] testTTS Edge TTS failed, trying fallback:', e.message);
            if (_speakAndroidTTS(t('tts.testText'), null)) {
                showToast(t('tts.androidFallback2'), 'info');
            } else if (typeof speechSynthesis !== 'undefined' && speechSynthesis) {
                var utterance = new SpeechSynthesisUtterance(t('tts.testText'));
                utterance.lang = 'zh-CN';
                utterance.rate = speed;
                var voices = speechSynthesis.getVoices();
                var zhVoice = voices.find(function(v) { return v.lang.indexOf('zh') === 0; });
                if (zhVoice) utterance.voice = zhVoice;
                speechSynthesis.speak(utterance);
                showToast(t('tts.browserFallback2'), 'info');
            } else {
                showToast('TTS不可用: ' + e.message + '（Edge TTS未安装，Android语音未就绪，浏览器不支持语音合成）', 'error');
            }
        });
    }

    // ── Init ─────────────────────────────────────────────────
    // ── History Restore & Stream Reconnect (ported from IDE) ─────
    async function loadHistory() {
        // Try to restore the last active conversation
        try {
            const lastConvId = localStorage.getItem('aicqbot_last_conv_id');
            if (lastConvId) {
                await loadConversation(lastConvId);
                return;
            }
        } catch(_e) {}

        // No saved conversation — try to load the most recent one from server
        try {
            if (conversations && conversations.length > 0) {
                const latestConv = conversations[0];
                if (latestConv && latestConv.id) {
                    await loadConversation(latestConv.id);
                    return;
                }
            }
        } catch(_e) {}
    }

    async function checkAndRecoverTask() {
        // Check if there's a running task on the server (page was refreshed mid-stream)
        try {
            const resp = await fetch('/api/chat/task/status');
            if (!resp.ok) return;
            const data = await resp.json();
            if (data.running) {
                // A task is still running — reconnect to it
                const taskConvId = data.conv_id || currentConvId;
                if (taskConvId && taskConvId !== currentConvId) {
                    await loadConversation(taskConvId);
                }
                await reconnectTask(taskConvId);
            }
        } catch(_e) {}
    }

    async function reconnectTask(convId) {
        // Reconnect to a running SSE task: first replay buffered events, then receive live events
        isProcessing = true;
        showStopButton();
        document.getElementById('btn-send').disabled = true;

        const emptyState = document.getElementById('empty-state');
        if (emptyState) emptyState.style.display = 'none';

        let assistantEl = addAssistantMessage();

        try {
            currentAbortController = new AbortController();
            const signal = currentAbortController.signal;
            const resp = await fetch('/api/chat/task/stream', { signal });

            if (!resp.ok) {
                throw new Error('Failed to reconnect to task');
            }

            const reader = resp.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let currentToolEl = null;

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                const lines = buffer.split('\n');
                buffer = lines.pop();

                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    const jsonStr = line.substring(6);
                    try {
                        const parsed = JSON.parse(jsonStr);
                        if (parsed.type === 'text_end') {
                            if (assistantEl) assistantEl.classList.remove('chat-streaming');
                            currentStreamEl = null;
                            streamBuffer = '';
                        } else if (parsed.type === 'text_start') {
                            assistantEl = addAssistantMessage();
                        } else if (parsed.type === 'tool_start') {
                            currentToolEl = showToolProgress(parsed.tool, parsed.args);
                        } else if (parsed.type === 'tool_result') {
                            if (currentToolEl) {
                                finalizeToolResult(currentToolEl, parsed.result, parsed.ok);
                                currentToolEl = null;
                            }
                        } else {
                            handleSSEEvent(parsed, assistantEl, currentToolEl);
                        }
                    } catch(_e) {}
                }
            }
        } catch(err) {
            if (err.name !== 'AbortError') {
                console.error('Reconnect error:', err);
            }
        }

        isProcessing = false;
        hideStopButton();
        document.getElementById('btn-send').disabled = false;
        currentAbortController = null;
        if (assistantEl) assistantEl.classList.remove('chat-streaming');
        // Reload conversation from server to get the final saved state
        if (convId) {
            await loadConversation(convId);
        }
        loadConversations();
    }

        async function init() {
        loadTheme();
        // ── Mobile: handle virtual keyboard resizing ──
        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', function() {
                document.documentElement.style.setProperty('--vh', window.visualViewport.height + 'px');
                var inputArea = document.getElementById('chat-input-area');
                if (inputArea) {
                    inputArea.scrollIntoView({ block: 'nearest' });
                }
            });
        }
        // Add event listeners with null checks for safety
        var el;
        // ── Sidebar controls ──
        initSidebar();
        // ── Mobile: tap chat area to close sidebar ──
        var chatMain = document.getElementById('chat-main');
        if (chatMain) {
            chatMain.addEventListener('click', function(e) {
                // Don't close if clicking the ☰ toggle button (it opens sidebar)
                if (e.target.closest('#btn-toggle-sidebar')) return;
                if (sidebarState.open) {
                    closeSidebar();
                }
            });
        }
        // ── TTS toggle ──
        loadTTSToggleState();
        if (el = document.getElementById('btn-tts-toggle')) el.addEventListener('click', toggleTTSEnabled);
        // ── Core controls ──
        if (el = document.getElementById('btn-theme')) el.addEventListener('click', toggleTheme);
        if (el = document.getElementById('btn-send')) el.addEventListener('click', sendMessage);
        if (el = document.getElementById('btn-stop')) el.addEventListener('click', abortGeneration);
        if (el = document.getElementById('btn-new-chat')) el.addEventListener('click', newChat);
        if (el = document.getElementById('btn-settings')) el.addEventListener('click', showSettings);
        if (el = document.getElementById('btn-agent-status')) el.addEventListener('click', showAgentStatus);
        if (el = document.getElementById('btn-skills')) el.addEventListener('click', showSkillsDialog);
        if (el = document.getElementById('btn-system-prompt')) el.addEventListener('click', showSystemPrompt);
        if (el = document.getElementById('btn-workflows')) el.addEventListener('click', showWorkflows);
        if (el = document.getElementById('btn-alarms')) el.addEventListener('click', showAlarms);
        if (el = document.getElementById('btn-tts-settings')) el.addEventListener('click', showTTSSettings);
        if (el = document.getElementById('btn-customer-service')) el.addEventListener('click', showCustomerService);
        if (el = document.getElementById('btn-close-cs')) el.addEventListener('click', function() { closeDialog('cs-dialog'); });
        if (el = document.getElementById('btn-save-cs')) el.addEventListener('click', saveCustomerService);
        if (el = document.getElementById('btn-cancel-cs')) el.addEventListener('click', cancelCustomerService);

        if (el = document.getElementById('btn-sites')) el.addEventListener('click', showSites);
        if (el = document.getElementById('btn-files')) el.addEventListener('click', showFiles);
        if (el = document.getElementById('btn-close-sites')) el.addEventListener('click', function() {
            document.getElementById('sites-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-close-files')) el.addEventListener('click', function() {
            document.getElementById('files-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-add-site')) el.addEventListener('click', showAddSiteModal);
        if (el = document.getElementById('btn-init-all-profiles')) el.addEventListener('click', initAllProfiles);
        if (el = document.getElementById('btn-refresh-files')) el.addEventListener('click', function() { showFiles(); });
        if (el = document.getElementById('btn-change-workdir')) el.addEventListener('click', changeWorkdir);
        if (el = document.getElementById('site-search')) el.addEventListener('input', filterSites);
        if (el = document.getElementById('btn-close-site-modal')) el.addEventListener('click', function() {
            document.getElementById('site-modal').classList.add('hidden');
        });
        // Panel close button (desktop back-to-chat)
        if (el = document.getElementById('btn-close-panel')) el.addEventListener('click', closePanel);
        // Also close panel when clicking any dialog-close button (for dialogs that switch between panel/dialog)
        // Dialog close buttons also close panel on desktop
        document.querySelectorAll('.dialog-close').forEach(function(btn) {
            var origClick = null;
            btn.addEventListener('click', function() { 
                if (_activePanelId) closePanel(); 
                // Also hide the parent dialog
                var dlg = btn.closest('.dialog-overlay');
                if (dlg) dlg.classList.add('hidden');
            });
        });
        // ── Dialog close buttons ──
        if (el = document.getElementById('btn-close-system-prompt')) el.addEventListener('click', function() {
            document.getElementById('system-prompt-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-save-system-prompt')) el.addEventListener('click', saveSystemPrompt);
        if (el = document.getElementById('btn-close-workflows')) el.addEventListener('click', function() {
            document.getElementById('workflows-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-refresh-workflows')) el.addEventListener('click', showWorkflows);
        if (el = document.getElementById('btn-close-alarms')) el.addEventListener('click', function() {
            document.getElementById('alarms-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-refresh-alarms')) el.addEventListener('click', showAlarms);
        if (el = document.getElementById('btn-close-settings')) el.addEventListener('click', function() {
            document.getElementById('settings-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-close-status')) el.addEventListener('click', function() {
            document.getElementById('status-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-save-settings')) el.addEventListener('click', saveSettings);
        if (el = document.getElementById('btn-add-model')) el.addEventListener('click', addModel);
        if (el = document.getElementById('btn-close-tts-settings')) el.addEventListener('click', function() {
            document.getElementById('tts-settings-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-save-tts-settings')) el.addEventListener('click', saveTTSSettings);
        // ── Skills dialog buttons ──
        if (el = document.getElementById('btn-close-skills')) el.addEventListener('click', function() {
            document.getElementById('skills-dialog').classList.add('hidden'); if(_activePanelId) closePanel();
        });
        if (el = document.getElementById('btn-refresh-skills')) el.addEventListener('click', showSkillsDialog);
        // ── TTS engine/speed controls ──
        if (el = document.getElementById('tts-engine-select')) el.addEventListener('change', function() {
            var serverGroup = document.getElementById('tts-server-url-group');
            if (serverGroup) serverGroup.style.display = this.value === 'indextts' ? '' : 'none';
        });
        if (el = document.getElementById('tts-speed')) el.addEventListener('input', function() {
            var valEl = document.getElementById('tts-speed-val');
            if (valEl) valEl.textContent = this.value;
        });
        // ── TTS test button ──
        if (el = document.getElementById('tts-test-btn')) el.addEventListener('click', testTTS);

        // ── File upload (direct send, no preview) ──
        if (el = document.getElementById('btn-attach')) el.addEventListener('click', function() {
            var fileInput = document.getElementById('chat-file-input');
            if (fileInput) fileInput.click();
        });
        if (el = document.getElementById('chat-file-input')) el.addEventListener('change', function() {
            if (this.files && this.files.length) {
                handleFileSelectAndSend(this.files);
                this.value = '';
            }
        });

        // ── Camera (direct send, no preview) ──
        // On Android: use native CameraBridge for reliable camera launch
        // On Desktop: use HTML5 input capture attribute
        if (el = document.getElementById('btn-camera')) el.addEventListener('click', function() {
            if (typeof window.CameraBridge !== 'undefined' && window.CameraBridge.isAvailable()) {
                // Android: Use native CameraBridge (reliable, bypasses WebView input.click() issues)
                window.CameraBridge.launchCamera();
            } else {
                // Desktop: Use HTML5 file input with capture attribute
                var cameraInput = document.getElementById('chat-camera-input');
                if (cameraInput) cameraInput.click();
            }
        });
        // Camera result callback (called from native CameraBridge after photo upload)
        window._onCameraResult = function(data) {
            if (data && data.status === 'uploaded') {
                sendFileDirectly(data);
            } else if (data === null) {
                showToast(t('upload.photoFailed'), 'info');
            }
        };
        if (el = document.getElementById('chat-camera-input')) el.addEventListener('change', function() {
            if (this.files && this.files.length) {
                handleFileSelectAndSend(this.files);
                this.value = '';
            }
        });

        // ── Attachment remove (event delegation) ──
        var attachPreview = document.getElementById('attachment-preview');
        if (attachPreview) {
            attachPreview.addEventListener('click', function(e) {
                if (e.target.classList.contains('attachment-remove')) {
                    var idx = parseInt(e.target.dataset.idx);
                    if (!isNaN(idx)) removeAttachment(idx);
                }
            });
        }

        if (el = document.getElementById('chat-input')) el.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
        });
        if (el = document.getElementById('chat-input')) el.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = Math.min(this.scrollHeight, 120) + 'px';
        });
        
        // ── Settings dropdown (gear icon) ──
        var gearBtn = document.getElementById('btn-settings-gear');
        var gearDropdown = document.getElementById('settings-dropdown');
        if (gearBtn && gearDropdown) {
            gearBtn.addEventListener('click', function(e) {
                e.stopPropagation();
                gearDropdown.classList.toggle('show');
            });
            // Delete conversation button inside dropdown
            var deleteBtn = document.getElementById('dropdown-delete-conv');
            if (deleteBtn) deleteBtn.addEventListener('click', function() {
                gearDropdown.classList.remove('show');
                if (currentConvId) deleteConversation(currentConvId);
            });
            // Language buttons
            var langEnBtn = document.getElementById('dropdown-lang-en');
            var langZhBtn = document.getElementById('dropdown-lang-zh');
            if (langEnBtn) langEnBtn.addEventListener('click', function() {
                setLang('en');
                gearDropdown.classList.remove('show');
                renderConvList();
            });
            if (langZhBtn) langZhBtn.addEventListener('click', function() {
                setLang('zh');
                gearDropdown.classList.remove('show');
                renderConvList();
            });
        }
        // Close dropdown on click outside
        document.addEventListener('click', function(e) {
            var dd = document.getElementById('settings-dropdown');
            if (dd && dd.classList.contains('show') && !e.target.closest('#btn-settings-gear') && !e.target.closest('#settings-dropdown')) {
                dd.classList.remove('show');
            }
        });

        // ── Delete conv button (now removed from header, but keep for safety) ──
        if (el = document.getElementById('btn-delete-conv')) el.addEventListener('click', function() {
            if (currentConvId) deleteConversation(currentConvId);
        });
        const chatMessages = document.getElementById('chat-messages');
        chatMessages.addEventListener('scroll', function() {
            autoScrollEnabled = chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < 50;
        });
        document.addEventListener('click', function(e) {
            // Code copy buttons
            if (e.target.classList.contains('code-copy-btn')) {
                const idx = parseInt(e.target.dataset.rawIdx);
                if (_chatCodeBlockStore[idx]) {
                    copyToClipboard(_chatCodeBlockStore[idx].code).then(function() {
                        e.target.textContent = '✓';
                        setTimeout(function() { e.target.textContent = '📋'; }, 1500);
                    });
                }
            }
        });
        applyI18nToPage();
        loadConversations();
        loadLLMConfig();
        // ── Restore last conversation & reconnect if needed (ported from IDE) ──
        await loadHistory();
        await checkAndRecoverTask();
        // ── Page visibility: auto-reconnect on tab switch ──
        let _sseConnectionLostWhileHidden = false;
        document.addEventListener('visibilitychange', async function() {
            if (document.visibilityState !== 'visible') return;
            if (isProcessing) return; // already processing
            try {
                const resp = await fetch('/api/chat/task/status');
                const data = await resp.json();
                if (data.running) {
                    await reconnectTask(data.conv_id || currentConvId);
                } else if (_sseConnectionLostWhileHidden) {
                    await loadHistory();
                    _sseConnectionLostWhileHidden = false;
                }
            } catch(_e) {}
        });
        // ── Notification polling (for alarm triggers etc.) ──
        startNotificationPolling();
        if (document.getElementById('chat-input')) document.getElementById('chat-input').focus();
    }



    // Show dialog content as panel (desktop) or dialog overlay (mobile)
    function showPanelOrDialog(panelId, dialogId) {
        // Close any existing panel first
        if (_activePanelId && _activePanelId !== panelId) closePanel();
        _currentPanelId = panelId;
        _currentDialogId = dialogId;
        if (isMobile()) {
            closeSidebar();
            _panelMode = false;
            return false; // dialog mode
        }
        // Desktop: close sidebar, use panel mode
        closeSidebar();
        _panelMode = true;
        return false; // always false - caller continues to populate content
    }

    var _panelMode = false;
    var _currentPanelId = null;
    var _currentDialogId = null;

    // Called AFTER populating dialog content - moves content to panel or shows dialog
    function finishPanelOrDialog() {
        if (_panelMode) {
            // Desktop: move dialog-body/dialog-footer into panel-view
            var panelView = document.getElementById('panel-view');
            var panelBody = document.getElementById('panel-body');
            var panelFooter = document.getElementById('panel-footer');
            var dialogEl = document.getElementById(_currentDialogId);

            if (dialogEl && panelView && panelBody) {
                var dialog = dialogEl.querySelector('.dialog');
                var dialogBody = dialog ? dialog.querySelector('.dialog-body') : null;
                var dialogFooter = dialog ? dialog.querySelector('.dialog-footer') : null;
                var titleEl = dialog ? dialog.querySelector('.dialog-header h3') : null;

                // Set panel title
                var panelTitle = document.getElementById('panel-title');
                if (panelTitle) panelTitle.textContent = titleEl ? titleEl.textContent : '';

                // Extract action buttons from dialog header (not the close button)
                var panelActions = document.getElementById('panel-actions');
                if (panelActions) panelActions.innerHTML = '';
                var headerBtns = dialog ? dialog.querySelectorAll('.dialog-header button:not(.dialog-close)') : [];
                if (panelActions) {
                    headerBtns.forEach(function(btn) {
                        var clone = btn.cloneNode(true);
                        clone.addEventListener('click', function() { btn.click(); });
                        panelActions.appendChild(clone);
                    });
                }

                // Move dialog-body into panel-body (preserves events + IDs)
                if (dialogBody) {
                    panelBody.innerHTML = '';
                    panelBody.appendChild(dialogBody);
                }

                // Move dialog-footer into panel-footer
                if (dialogFooter && panelFooter) {
                    panelFooter.innerHTML = '';
                    panelFooter.appendChild(dialogFooter);
                }

                // Keep dialog overlay hidden
                dialogEl.classList.add('hidden');
            }

            _activePanelId = _currentPanelId;
            var chatMain = document.getElementById('chat-main');
            if (chatMain) chatMain.classList.add('panel-open');
            if (panelView) panelView.classList.remove('hidden');
        } else {
            // Mobile: show dialog overlay
            var dialogEl = document.getElementById(_currentDialogId);
            if (dialogEl) dialogEl.classList.remove('hidden');
        }
        _panelMode = false;
    }

    // ─── Sites Management ─────────────────────────────────────────
    const SITE_CAT_ICONS = {
        email: '\u{1F4E7}', social: '\u{1F4AC}', cloud: '\u2601\uFE0F',
        ai_chat: '\u{1F916}', dev: '\u{1F6E0}\uFE0F', content: '\u{1F3AC}', custom: '\u2699\uFE0F'
    };
    const SITE_CAT_LABELS = {
        email: '\u90AE\u7BB1\u670D\u52A1', social: '\u793E\u4EA4\u5A92\u4F53', cloud: '\u4E91\u670D\u52A1',
        ai_chat: 'AI \u5BF9\u8BDD', dev: '\u5F00\u53D1\u5DE5\u5177', content: '\u5185\u5BB9\u5E73\u53F0', custom: '\u81EA\u5B9A\u4E49'
    };

    let _editingSiteName = '';

    async function showSites() {
        if (showPanelOrDialog('sites', 'sites-dialog')) return;
        finishPanelOrDialog();
        const r = await api('/api/sites');
        if (r.error) {
            document.getElementById('sites-body').innerHTML = '<div style="color:var(--danger)">\u52A0\u8F7D\u5931\u8D25: ' + escHtml(r.error) + '</div>';
            return;
        }
        const sites = r.sites || [];
        const summary = r.summary || {};
        let profiles = [];
        try { profiles = (await api('/api/sites/profiles')).profiles || []; } catch(e) {}

        const groups = {};
        for (const s of sites) {
            const cat = s.category || 'custom';
            if (!groups[cat]) groups[cat] = [];
            groups[cat].push(s);
        }

        let html = '<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">' +
            '\u5171 <strong>' + sites.length + '</strong> \u4E2A\u7F51\u7AD9' +
            ' (\u5185\u7F6E' + (summary.builtin_count || 0) +
            ' \u00B7 \u81EA\u5B9A\u4E49' + (summary.custom_count || 0) + ')' +
            ' \u00B7 Profile ' + profiles.length + ' \u4E2A</div>';

        for (const [cat, catSites] of Object.entries(groups)) {
            const icon = SITE_CAT_ICONS[cat] || '\u{1F310}';
            const label = SITE_CAT_LABELS[cat] || cat;
            html += '<div style="margin-bottom:12px">' +
                '<h4 style="margin:0 0 8px 0;font-size:14px">' + icon + ' ' + label +
                ' <span style="background:var(--bg-primary);padding:2px 8px;border-radius:10px;font-size:11px">' + catSites.length + '</span></h4>';
            for (const s of catSites) {
                const name = escHtml(s.name || '');
                const displayName = escHtml(s.display_name || s.name || '');
                const typeTag = s.is_builtin ? '<span style="background:#3b82f620;color:#3b82f6;padding:1px 6px;border-radius:4px;font-size:11px">\u5185\u7F6E</span>' :
                               s.is_custom ? '<span style="background:#10b98120;color:#10b981;padding:1px 6px;border-radius:4px;font-size:11px">\u81EA\u5B9A\u4E49</span>' : '';
                const profileTag = s.has_profile ? '<span style="background:#10b98120;color:#10b981;padding:1px 6px;border-radius:4px;font-size:11px">\u5DF2\u767B\u5F55</span>' :
                                   '<span style="background:#f59e0b20;color:#f59e0b;padding:1px 6px;border-radius:4px;font-size:11px">\u672A\u767B\u5F55</span>';
                html += '<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)" data-site-name="' + name + '">' +
                    '<div style="flex-shrink:0;font-size:16px">' + (SITE_CAT_ICONS[s.category] || '\u{1F310}') + '</div>' +
                    '<div style="flex:1;min-width:0">' +
                    '<div style="font-size:13px">' + displayName + ' ' + typeTag + ' ' + profileTag + '</div>' +
                    '<div style="font-size:11px;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
                    (s.login_url ? '<code style="font-size:11px;color:var(--accent)">' + escHtml(s.login_url) + '</code>' : escHtml(s.home_url || '')) +
                    '</div></div><div style="display:flex;gap:4px;flex-shrink:0;flex-wrap:wrap">';
                html += '<button class="btn-secondary" style="padding:2px 8px;font-size:11px" data-action="viewSite" data-name="' + name + '">\u8BE6\u60C5</button>';
                if (!s.is_builtin) {
                    html += '<button class="btn-secondary" style="padding:2px 8px;font-size:11px" data-action="editSite" data-name="' + name + '">\u7F16\u8F91</button>';
                    html += '<button style="padding:2px 8px;font-size:11px;background:#ef444420;color:#ef4444;border:1px solid #ef444440;border-radius:4px;cursor:pointer" data-action="removeSite" data-name="' + name + '">\u5220\u9664</button>';
                }
                if (s.login_url) html += '<button class="btn-primary" style="padding:2px 8px;font-size:11px" data-action="openBrowser" data-name="' + name + '">\u767B\u5F55</button>';
                if (s.has_profile) {
                    html += '<button style="padding:2px 8px;font-size:11px;background:#f59e0b20;color:#f59e0b;border:1px solid #f59e0b40;border-radius:4px;cursor:pointer" data-action="closeBrowser" data-name="' + name + '">\u5173\u95ED</button>';
                    html += '<button style="padding:2px 8px;font-size:11px;background:#f59e0b20;color:#f59e0b;border:1px solid #f59e0b40;border-radius:4px;cursor:pointer" data-action="deleteProfile" data-name="' + name + '">\u6E05\u9664</button>';
                } else {
                    html += '<button class="btn-secondary" style="padding:2px 8px;font-size:11px" data-action="initProfile" data-name="' + name + '">Profile</button>';
                }
                html += '</div></div>';
            }
            html += '</div>';
        }
        if (!sites.length) html += '<div style="text-align:center;color:var(--text-muted);padding:20px">\u6682\u65E0\u7F51\u7AD9</div>';
        document.getElementById('sites-body').innerHTML = html;
    }

    function filterSites() {
        const q = (document.getElementById('site-search')?.value || '').toLowerCase();
        document.querySelectorAll('#sites-body [data-site-name]').forEach(function(el) {
            const n = (el.dataset.siteName || '').toLowerCase();
            el.style.display = n.includes(q) ? '' : 'none';
        });
    }

    async function viewSiteDetail(name) {
        _editingSiteName = name;
        const s = await api('/api/sites/' + encodeURIComponent(name));
        if (s.error) { showToast(s.error, 'danger'); return; }
        let html = '<div style="font-size:13px;line-height:1.8">';
        html += '<div><strong>\u540D\u79F0:</strong> ' + escHtml(s.name) + '</div>';
        html += '<div><strong>\u663E\u793A\u540D:</strong> ' + escHtml(s.display_name || '') + '</div>';
        html += '<div><strong>\u5206\u7C7B:</strong> ' + escHtml(SITE_CAT_LABELS[s.category] || s.category) + '</div>';
        html += '<div><strong>\u7C7B\u578B:</strong> ' + (s.is_builtin ? '\u5185\u7F6E' : '\u81EA\u5B9A\u4E49') + '</div>';
        if (s.login_url) html += '<div><strong>\u767B\u5F55\u9875:</strong> <a href="' + escHtml(s.login_url) + '" target="_blank" style="color:var(--accent)">' + escHtml(s.login_url) + '</a></div>';
        if (s.home_url) html += '<div><strong>\u9996\u9875:</strong> <a href="' + escHtml(s.home_url) + '" target="_blank" style="color:var(--accent)">' + escHtml(s.home_url) + '</a></div>';
        if (s.urls && Object.keys(s.urls).length) {
            html += '<div style="margin-top:8px"><strong>\u5E38\u7528\u94FE\u63A5:</strong></div>';
            for (const [k, v] of Object.entries(s.urls)) {
                html += '<div style="margin-left:12px"><code style="font-size:11px">' + escHtml(k) + '</code>: <a href="' + escHtml(v) + '" target="_blank" style="color:var(--accent);font-size:12px">' + escHtml(v) + '</a></div>';
            }
        }
        if (s.tips && s.tips.length) {
            html += '<div style="margin-top:8px"><strong>\u4F7F\u7528\u63D0\u793A:</strong></div><ul style="padding-left:20px;font-size:12px">';
            for (const t of s.tips) html += '<li>' + escHtml(t) + '</li>';
            html += '</ul>';
        }
        html += '<div style="margin-top:12px;display:flex;gap:8px">';
        html += '<button class="btn-secondary" data-action="initProfileModal" data-name="' + escHtml(name) + '">\u521D\u59CB\u5316 Profile</button>';
        if (!s.is_builtin) html += '<button class="btn-secondary" data-action="editSiteModal" data-name="' + escHtml(name) + '">\u7F16\u8F91</button>';
        html += '</div></div>';
        document.getElementById('site-modal-title').textContent = s.display_name || s.name;
        document.getElementById('site-modal-body').innerHTML = html;
        document.getElementById('site-modal').classList.remove('hidden');
    }

    function showAddSiteModal() {
        let html = '<div style="font-size:13px">' +
            '<div style="margin-bottom:8px"><label>\u7F51\u7AD9\u540D\u79F0 (\u82F1\u6587\u6807\u8BC6)</label><br>' +
            '<input id="siteFormName" placeholder="\u4F8B\u5982: my_website" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u663E\u793A\u540D\u79F0</label><br>' +
            '<input id="siteFormDisplay" placeholder="\u4F8B\u5982: \u6211\u7684\u7F51\u7AD9" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u5206\u7C7B</label><br>' +
            '<select id="siteFormCategory" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)">' +
            '<option value="custom">\u81EA\u5B9A\u4E49</option><option value="email">\u90AE\u7BB1\u670D\u52A1</option>' +
            '<option value="social">\u793E\u4EA4\u5A92\u4F53</option><option value="cloud">\u4E91\u670D\u52A1</option>' +
            '<option value="ai_chat">AI \u5BF9\u8BDD</option><option value="dev">\u5F00\u53D1\u5DE5\u5177</option>' +
            '<option value="content">\u5185\u5BB9\u5E73\u53F0</option></select></div>' +
            '<div style="margin-bottom:8px"><label>\u767B\u5F55\u9875 URL</label><br>' +
            '<input id="siteFormLoginUrl" placeholder="https://example.com/login" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u9996\u9875 URL</label><br>' +
            '<input id="siteFormHomeUrl" placeholder="https://example.com/dashboard" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u767B\u5F55\u68C0\u6D4B URL</label><br>' +
            '<input id="siteFormDetectUrl" placeholder="https://example.com/dashboard" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-top:12px"><button class="btn-primary" data-action="doAddSite">\u6DFB\u52A0</button></div></div>';
        document.getElementById('site-modal-title').textContent = '\u6DFB\u52A0\u81EA\u5B9A\u4E49\u7F51\u7AD9';
        document.getElementById('site-modal-body').innerHTML = html;
        document.getElementById('site-modal').classList.remove('hidden');
    }

    async function doAddSite() {
        const name = (document.getElementById('siteFormName')?.value || '').trim().toLowerCase().replace(/\s+/g, '_');
        if (!name) { showToast('\u7F51\u7AD9\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A', 'danger'); return; }
        if (!/^[a-z0-9_-]+$/.test(name)) { showToast('\u7F51\u7AD9\u540D\u79F0\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26', 'danger'); return; }
        const data = {
            name: name,
            display_name: document.getElementById('siteFormDisplay')?.value || name,
            category: document.getElementById('siteFormCategory')?.value || 'custom',
            login_url: document.getElementById('siteFormLoginUrl')?.value || '',
            home_url: document.getElementById('siteFormHomeUrl')?.value || '',
            detect_url: document.getElementById('siteFormDetectUrl')?.value || '',
        };
        const r = await api('/api/sites', { method: 'POST', body: JSON.stringify(data) });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('\u5DF2\u6DFB\u52A0\u7F51\u7AD9: ' + name, 'success');
        document.getElementById('site-modal').classList.add('hidden');
        showSites();
    }

    async function editSiteModal(name) {
        _editingSiteName = name;
        const s = await api('/api/sites/' + encodeURIComponent(name));
        if (s.error) { showToast(s.error, 'danger'); return; }
        let html = '<div style="font-size:13px">' +
            '<div style="margin-bottom:8px"><label>\u663E\u793A\u540D\u79F0</label><br>' +
            '<input id="editSiteDisplay" value="' + escHtml(s.display_name || '') + '" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u5206\u7C7B</label><br>' +
            '<select id="editSiteCategory" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)">' +
            '<option value="custom"' + (s.category==='custom'?' selected':'') + '>\u81EA\u5B9A\u4E49</option>' +
            '<option value="email"' + (s.category==='email'?' selected':'') + '>\u90AE\u7BB1\u670D\u52A1</option>' +
            '<option value="social"' + (s.category==='social'?' selected':'') + '>\u793E\u4EA4\u5A92\u4F53</option>' +
            '<option value="cloud"' + (s.category==='cloud'?' selected':'') + '>\u4E91\u670D\u52A1</option>' +
            '<option value="ai_chat"' + (s.category==='ai_chat'?' selected':'') + '>AI \u5BF9\u8BDD</option>' +
            '<option value="dev"' + (s.category==='dev'?' selected':'') + '>\u5F00\u53D1\u5DE5\u5177</option>' +
            '<option value="content"' + (s.category==='content'?' selected':'') + '>\u5185\u5BB9\u5E73\u53F0</option></select></div>' +
            '<div style="margin-bottom:8px"><label>\u767B\u5F55\u9875 URL</label><br>' +
            '<input id="editSiteLoginUrl" value="' + escHtml(s.login_url || '') + '" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u9996\u9875 URL</label><br>' +
            '<input id="editSiteHomeUrl" value="' + escHtml(s.home_url || '') + '" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-bottom:8px"><label>\u767B\u5F55\u68C0\u6D4B URL</label><br>' +
            '<input id="editSiteDetectUrl" value="' + escHtml(s.detect_url || '') + '" style="width:100%;padding:6px;margin-top:4px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"></div>' +
            '<div style="margin-top:12px"><button class="btn-primary" data-action="doUpdateSite">\u4FDD\u5B58</button></div></div>';
        document.getElementById('site-modal-title').textContent = '\u7F16\u8F91\u7F51\u7AD9: ' + (s.display_name || s.name);
        document.getElementById('site-modal-body').innerHTML = html;
        document.getElementById('site-modal').classList.remove('hidden');
    }

    async function doUpdateSite() {
        const name = _editingSiteName;
        if (!name) { showToast('\u7F51\u7AD9\u540D\u79F0\u4E22\u5931', 'danger'); return; }
        const data = {
            display_name: document.getElementById('editSiteDisplay')?.value || '',
            category: document.getElementById('editSiteCategory')?.value || 'custom',
            login_url: document.getElementById('editSiteLoginUrl')?.value || '',
            home_url: document.getElementById('editSiteHomeUrl')?.value || '',
            detect_url: document.getElementById('editSiteDetectUrl')?.value || '',
        };
        const r = await api('/api/sites/' + encodeURIComponent(name), { method: 'PUT', body: JSON.stringify(data) });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('\u5DF2\u66F4\u65B0\u7F51\u7AD9: ' + name, 'success');
        document.getElementById('site-modal').classList.add('hidden');
        showSites();
    }

    async function removeSite(name) {
        if (!confirm('\u786E\u5B9A\u8981\u5220\u9664\u7F51\u7AD9 "' + name + '" \u5417\uFF1F\u5C06\u540C\u65F6\u5220\u9664\u6D4F\u89C8\u5668 Profile\u3002')) return;
        const r = await api('/api/sites/' + encodeURIComponent(name), { method: 'DELETE' });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('\u5DF2\u5220\u9664: ' + name, 'success');
        showSites();
    }

    async function initSiteProfile(name) {
        const r = await api('/api/sites/' + encodeURIComponent(name) + '/init-profile', { method: 'POST' });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('Profile \u5DF2\u521D\u59CB\u5316: ' + name, 'success');
        showSites();
    }

    async function initAllProfiles() {
        showToast('\u6B63\u5728\u521D\u59CB\u5316\u6240\u6709 Profile...', 'info');
        const r = await api('/api/sites');
        if (r.error) { showToast(r.error, 'danger'); return; }
        const names = (r.sites || []).map(s => s.name);
        let count = 0;
        for (const n of names) {
            try { const pr = await api('/api/sites/' + encodeURIComponent(n) + '/init-profile', { method: 'POST' }); if (pr.ok) count++; } catch(e) {}
        }
        showToast('\u5DF2\u521D\u59CB\u5316 ' + count + ' \u4E2A Profile', 'success');
        showSites();
    }

    async function deleteSiteProfile(name) {
        if (!confirm('\u786E\u5B9A\u8981\u5220\u9664 "' + name + '" \u7684\u6D4F\u89C8\u5668 Profile \u5417\uFF1F\u8FD9\u5C06\u6E05\u9664\u767B\u5F55\u72B6\u6001\u548C Cookie\u3002')) return;
        const r = await api('/api/sites/profiles/' + encodeURIComponent(name), { method: 'DELETE' });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('Profile \u5DF2\u5220\u9664: ' + name, 'success');
        showSites();
    }

    async function openSiteBrowser(name) {
        showToast('\u6B63\u5728\u542F\u52A8\u6D4F\u89C8\u5668...', 'info');
        const r = await api('/api/sites/' + encodeURIComponent(name) + '/open-browser', { method: 'POST' });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast(r.message || '\u6D4F\u89C8\u5668\u5DF2\u6253\u5F00', 'success');
        setTimeout(function() { showSites(); }, 2000);
    }

    async function closeSiteBrowser(name) {
        const r = await api('/api/sites/' + encodeURIComponent(name) + '/close-browser', { method: 'POST' });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED', 'success');
        setTimeout(function() { showSites(); }, 1000);
    }

    // ─── Files (Workdir) Management ─────────────────────────────────
    let _workdirCurPath = '';

    async function showFiles() {
        if (showPanelOrDialog('files', 'files-dialog')) return;
        finishPanelOrDialog();
        const wd = await api('/api/workdir').catch(() => ({}));
        const wdPath = wd && wd.path ? wd.path : '';
        let html = '<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">' +
            '\u{1F4C1} \u5DE5\u4F5C\u76EE\u5F55: <strong>' + escHtml(wdPath) + '</strong></div>' +
            '<div id="workdirContent">\u52A0\u8F7D\u4E2D...</div>';
        document.getElementById('files-body').innerHTML = html;
        _workdirCurPath = '';
        loadWorkdirContent('');
    }

    async function loadWorkdirContent(subPath) {
        _workdirCurPath = subPath || '';
        const params = new URLSearchParams();
        if (subPath) params.set('path', subPath);
        const files = await api('/api/workdir/files?' + params.toString());
        const el = document.getElementById('workdirContent');
        if (!el) return;
        let bcHtml = '';
        if (subPath) {
            const parts = subPath.split('/');
            const crumbs = ['<span style="cursor:pointer;color:var(--accent)" onclick="ChatManager.loadWorkdirContent(\'\')">\u6839\u76EE\u5F55</span>'];
            let acc = '';
            for (let i = 0; i < parts.length; i++) {
                acc += (acc ? '/' : '') + parts[i];
                const p = acc;
                crumbs.push(' / <span style="cursor:pointer;color:var(--accent)" onclick="ChatManager.loadWorkdirContent(\'' + escHtml(p) + '\')">' + escHtml(parts[i]) + '</span>');
            }
            bcHtml = crumbs.join('');
        } else {
            bcHtml = '<span style="color:var(--text-secondary)">\u6839\u76EE\u5F55</span>';
        }
        let html = '<div style="font-size:12px;margin-bottom:8px">' + bcHtml + '</div>';
        html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
        html += '<tr style="border-bottom:2px solid var(--border)"><th style="text-align:left;padding:4px 8px">\u540D\u79F0</th><th style="text-align:left;padding:4px 8px;width:80px">\u5927\u5C0F</th><th style="text-align:right;padding:4px 8px;width:120px">\u64CD\u4F5C</th></tr>';
        if (!files || !files.length) {
            html += '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:20px">\u76EE\u5F55\u4E3A\u7A7A</td></tr>';
        } else {
            for (const f of files) {
                const fp = f.path || f.name;
                if (f.type === 'dir') {
                    html += '<tr style="border-bottom:1px solid var(--border)">' +
                        '<td style="padding:4px 8px;cursor:pointer" onclick="ChatManager.loadWorkdirContent(\'' + escHtml(fp) + '\')">' +
                        '\u{1F4C2} ' + escHtml(f.name) +
                        (f.modified ? '<div style="font-size:10px;color:var(--text-secondary)">\u4FEE\u6539: ' + escHtml(f.modified) + '</div>' : '') +
                        '</td><td style="padding:4px 8px">-</td>' +
                        '<td style="text-align:right;padding:4px 8px"><button style="padding:2px 8px;font-size:11px;background:#ef444420;color:#ef4444;border:1px solid #ef444440;border-radius:4px;cursor:pointer" onclick="event.stopPropagation();ChatManager.deleteWorkdirItem(\'' + escHtml(fp) + '\',true)">\u5220\u9664</button></td></tr>';
                } else {
                    let sizeStr = f.size > 1048576 ? (f.size/1048576).toFixed(1) + ' MB' : f.size > 1024 ? (f.size/1024).toFixed(1) + ' KB' : f.size + ' B';
                    html += '<tr style="border-bottom:1px solid var(--border)">' +
                        '<td style="padding:4px 8px">\u{1F4C4} ' + escHtml(f.name) +
                        (f.modified ? '<div style="font-size:10px;color:var(--text-secondary)">\u4FEE\u6539: ' + escHtml(f.modified) + '</div>' : '') +
                        '</td><td style="padding:4px 8px">' + sizeStr + '</td>' +
                        '<td style="text-align:right;padding:4px 8px;display:flex;gap:4px;justify-content:flex-end">' +
                        '<button class="btn-secondary" style="padding:2px 8px;font-size:11px" onclick="ChatManager.downloadWorkdirFile(\'' + escHtml(fp) + '\')">\u4E0B\u8F7D</button>' +
                        '<button style="padding:2px 8px;font-size:11px;background:#ef444420;color:#ef4444;border:1px solid #ef444440;border-radius:4px;cursor:pointer" onclick="ChatManager.deleteWorkdirItem(\'' + escHtml(fp) + '\',false)">\u5220\u9664</button>' +
                        '</td></tr>';
                }
            }
        }
        html += '</table>';
        el.innerHTML = html;
    }

    function downloadWorkdirFile(relPath) {
        const link = document.createElement('a');
        link.href = '/api/workdir/download/' + encodeURIComponent(relPath);
        link.download = '';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    async function changeWorkdir() {
        const p = prompt('\u65B0\u5DE5\u4F5C\u76EE\u5F55\u8DEF\u5F84:');
        if (!p) return;
        const r = await api('/api/workdir', { method: 'PUT', body: JSON.stringify({ path: p }) });
        if (r.error) { showToast(r.error, 'danger'); return; }
        showToast('\u5DF2\u66F4\u65B0\u5DE5\u4F5C\u76EE\u5F55', 'success');
        showFiles();
    }

    async function deleteWorkdirItem(relPath, isDir) {
        const itemType = isDir ? '\u6587\u4EF6\u5939' : '\u6587\u4EF6';
        if (!confirm('\u786E\u8BA4\u5220\u9664' + itemType + ' "' + relPath + '"\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002')) return;
        const r = await api('/api/workdir/delete/' + encodeURIComponent(relPath), { method: 'DELETE' });
        if (r.error) { showToast('\u5220\u9664\u5931\u8D25: ' + r.error, 'danger'); return; }
        showToast('\u5DF2\u5220\u9664', 'success');
        loadWorkdirContent(_workdirCurPath);
    }

    // ─── Event delegation for site/file actions ────────────────────────
    document.addEventListener('click', function(e) {
        const btn = e.target.closest('[data-action]');
        if (!btn) return;
        const action = btn.getAttribute('data-action');
        const name = btn.getAttribute('data-name') || '';
        if (btn.closest('#sites-body') || btn.closest('#sites-dialog')) {
            switch (action) {
                case 'viewSite': viewSiteDetail(name); break;
                case 'editSite': editSiteModal(name); break;
                case 'removeSite': removeSite(name); break;
                case 'openBrowser': openSiteBrowser(name); break;
                case 'closeBrowser': closeSiteBrowser(name); break;
                case 'deleteProfile': deleteSiteProfile(name); break;
                case 'initProfile': initSiteProfile(name); break;
            }
        }
        if (btn.closest('#site-modal')) {
            switch (action) {
                case 'doAddSite': doAddSite(); break;
                case 'doUpdateSite': doUpdateSite(); break;
                case 'editSiteModal':
                    document.getElementById('site-modal').classList.add('hidden');
                    if (name) editSiteModal(name);
                    break;
                case 'initProfileModal':
                    if (name) initSiteProfile(name);
                    document.getElementById('site-modal').classList.add('hidden');
                    break;
            }
        }
    });


    // Helper to switch skills tab from inline onclick handlers
    function switchSkillsTab(tab) {
        _skillsTab = tab;
        renderSkillsContent();
    }

    // Expose functions needed by inline onclick handlers to global scope
    window.viewSkillDetail = viewSkillDetail;
    window.toggleSkill = toggleSkill;
    window.doClawhubSearch = doClawhubSearch;
    window.doClawhubExplore = doClawhubExplore;
    window.uninstallClawhubSkill = uninstallClawhubSkill;
    window.updateClawhubSkill = updateClawhubSkill;
    window.updateAllClawhubSkills = updateAllClawhubSkills;
    window.installClawhubSkill = installClawhubSkill;
    window.viewClawhubDetail = viewClawhubDetail;
    window.switchSkillsTab = switchSkillsTab;

    return { init, removeModel, addModel, toggleTheme, setTheme, testModel, loadWorkdirContent, downloadWorkdirFile, deleteWorkdirItem, setLang, t };
})();

document.addEventListener('DOMContentLoaded', ChatManager.init);
