From c727ca34caa9dc96aa4f873fd47ddd0050b4dc49 Mon Sep 17 00:00:00 2001 From: dd3xp Date: Mon, 6 Jul 2026 10:53:40 -0400 Subject: [PATCH] fix(desktop): refine chat ui and persistence --- frontends/desktop/static/app.js | 715 ++++++++++--------------- frontends/desktop/static/fallback.html | 40 +- frontends/desktop/static/i18n.js | 468 ++++++++++++++++ frontends/desktop/static/index.html | 9 +- frontends/desktop/static/loading.html | 24 +- frontends/desktop/static/styles.css | 305 ++++++++--- frontends/desktop_bridge.py | 25 +- 7 files changed, 1021 insertions(+), 565 deletions(-) create mode 100644 frontends/desktop/static/i18n.js diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index 13f58959..7ffc842b 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -306,355 +306,8 @@ let bridgeUiOffline = false; })(); /* ═══════════════ i18n ═══════════════ */ -const I18N = { - zh: { - 'app.title': 'GenericAgent 桌面版', - 'brand.sub': '桌面终端', - 'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板', - 'nav.collab': '指挥家', 'nav.token': '用量', - 'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版', - 'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动', - 'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报', - 'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程', - 'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成', - 'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)', - 'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚', - 'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关', - 'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO', - 'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报', - 'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能', - 'composer.placeholder': 'GA 能帮你做些什么?', - 'search.placeholder': '搜索会话…', 'conv.new': '新对话', - 'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除', - 'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存', - 'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置', - 'modal.customPreset': '自定义预设', - 'modal.editCustomPreset': '编辑任务', - 'customPreset.titlePh': '标题,例如「写周报」', - 'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交', - 'customPreset.empty': '标题和 Prompt 不能为空', - 'customPreset.removeTitle': '删除', - 'customPreset.editTitle': '编辑', - 'builtinPreset.restoreBtn': '恢复默认预设', - 'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理', - 'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?', - 'appearance.light': '浅色', 'appearance.dark': '深色', - 'set.noModels': '暂无模型,点击下方添加', - 'lang.zh': '简体中文', 'lang.en': 'English', - 'model.name': '备注', 'model.namePh': '会显示在模型列表', - 'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变', - 'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages', - 'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)', - 'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式', - 'model.model': '模型', 'model.modelPh': 'model 参数名', - 'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致', - 'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)', - 'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除', - 'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起', - 'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼', - 'guide.step1': '点击下方链接,登录后创建并复制 API Key', - 'guide.step2': '把 Key 粘贴到下方「API Key」输入框', - 'guide.step3': '点击保存,即可在模型列表中选用', - 'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改', - 'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制', - 'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址', - 'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型', - 'confirm.modelDelete': '确定删除该模型配置?', - 'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个', - 'model.emptyMixin': '尚未加入模型', - 'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序', - 'err.mixinFailed': '操作失败', - 'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志', - 'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)', - 'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停', - 'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进', - 'collab.progressTitle': '分工进度', - 'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。', - 'collab.placeholder': '请对指挥家描述你想完成的目标', - 'collab.guideTitle': '把要完成的事告诉指挥家', - 'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。', - 'collab.guideStep1t': '描述目标', - 'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家', - 'collab.guideStep2t': '自动拆解', - 'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度', - 'collab.guideStep3t': '交付摘要', - 'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报', - 'collab.guideStep4t': '随时调整', - 'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理', - 'collab.chipProgress': '现在进展如何?', - 'collab.chipPause': '先暂停当前任务', - 'collab.chipSummary': '总结一下目前的结果', - 'collab.showProgressTitle': '查看分工进度', - 'collab.statRunning': '进行中', - 'collab.statDone': '已完成', - 'collab.plusMenu': '更多操作', - 'collab.switchMode': '切换模式', - 'collab.typing': '指挥家正在处理', - 'collab.offline': '无法连接指挥家服务,请确认后端已启动。', - 'collab.retry': '重试', - 'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。', - 'collab.reconnectIn': '{n} 秒后重试', - 'collab.stRunning': '执行中', 'collab.stReported': '已回报', 'collab.stPaused': '已暂停', - 'collab.stFailed': '遇到问题', 'collab.stTerminated': '已终止', - 'collab.summaryRunning': '正在处理中…', 'collab.summaryWait': '等待回报', - 'collab.taskFallback': '任务 {n}', - 'collab.timeJust': '刚刚', - 'collab.timeSec': '{n} 秒前', - 'collab.timeMin': '{n} 分钟前', - 'collab.timeHr': '{n} 小时前', - 'collab.timeDay': '{n} 天前', - 'page.token.title': '用量', 'page.token.sub': '每会话与累计用量及缓存率', - 'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中', - 'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命', - 'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话', - 'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败', - 'err.interruptTimeout': '等待上一轮停止超时,请稍后再试', - 'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息', - 'chat.interrupting': '正在停止上一轮…', - 'chat.sessionLoading': '正在加载会话…', - 'sys.stopRequested': '已请求停止', - 'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置', - 'slash.unknown': '未知命令', - 'upload.hint': '上传文件:选择 / 拖拽 / 粘贴', - 'upload.button': '上传文件', - 'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件', - 'upload.failed': '上传失败', - 'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n})', 'err.numMax': '不能超过 {n}', - 'file.openFailed': '无法打开文件', - 'file.kindGeneric': '文件', - 'file.kindDoc': '文档', - 'file.kindSheet': '表格', - 'file.kindSlide': '幻灯片', - 'file.kindCode': '代码', - 'file.kindArchive': '压缩包', - 'file.kindAudio': '音频', - 'file.kindVideo': '视频', - 'upload.removeTitle': '移除', - 'upload.dropHint': '松开以上传文件', - 'lightbox.closeTitle': '关闭', - 'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮', - 'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})', - 'plan.running': '计划执行中', 'plan.completeTitle': '计划完成', - 'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项', - 'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情', - 'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成', - 'timing.elapsed': '已运行 {t}', - 'model.auto': '自动选择', - 'model.menuLabel': '选择模型', - 'chip.plan': 'Plan', - 'chip.auto': 'Auto', - 'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉', - 'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord', - 'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本', - 'ch.logEmpty': '暂无日志', - 'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败', - 'err.mykeyImport': '导入模型配置失败', - 'err.mykeyExport': '导出模型配置失败', - 'err.channelNotConfigured': '请先在 mykey.py 中配置该平台', - 'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止', - 'modal.channelLogs': '进程日志', - 'modal.mykeyConfig': 'mykey.py 配置', - 'sys.configSaved': '配置已保存', - 'sys.mykeyImported': '模型配置已导入', - 'sys.mykeyExported': '模型配置已导出', - 'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常', - 'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出', - 'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送', - 'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家', - 'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲', - 'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单', - 'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。', - 'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本', - 'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除', - 'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算', - 'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $', - 'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $', - 'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。', - 'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。', - 'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。', - 'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。', - 'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。', - 'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。', - 'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。', - 'ask.banner': 'GA 等你回答', - 'ask.replyHint': '在下方输入框回复', - 'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)', - }, - en: { - 'app.title': 'GenericAgent Desktop', - 'brand.sub': 'Desktop terminal', - 'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status', - 'nav.collab': 'Conductor', 'nav.token': 'Usage', - 'foot.settings': 'Settings', 'foot.ver': 'GenericAgent · Desktop', - 'chat.startTitle': 'Start a conversation', 'chat.startSub': 'Type a message, or pick a preset', - 'preset.butler.t': 'Conductor', 'preset.butler.d': 'Auto-decompose complex tasks; just check progress and briefings', - 'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify', - 'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously', - 'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)', - 'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration', - 'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check', - 'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs', - 'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits', - 'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function', - 'composer.placeholder': 'What can GA do for you?', - 'search.placeholder': 'Search chats…', 'conv.new': 'New chat', - 'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete', - 'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save', - 'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings', - 'modal.customPreset': 'Custom preset', - 'modal.editCustomPreset': 'Edit task', - 'customPreset.titlePh': 'Title, e.g. "Weekly report"', - 'customPreset.promptPh': 'Prompt body — sent as the message when clicked', - 'customPreset.empty': 'Title and Prompt cannot be empty', - 'customPreset.removeTitle': 'Delete', - 'customPreset.editTitle': 'Edit', - 'builtinPreset.restoreBtn': 'Restore defaults', - 'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager', - 'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?', - 'appearance.light': 'Light', 'appearance.dark': 'Dark', - 'set.noModels': 'No models yet — add one below', - 'lang.zh': '简体中文', 'lang.en': 'English', - 'model.name': 'Note', 'model.namePh': 'Shown in the model list', - 'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key', - 'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages', - 'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)', - 'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream', - 'model.model': 'Model', 'model.modelPh': 'model parameter name', - 'model.modelHint': 'Must match the model field in your provider docs exactly', - 'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)', - 'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete', - 'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse', - 'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian', - 'guide.step1': 'Open the link, sign in, then create & copy your API key', - 'guide.step2': 'Paste the key into the “API Key” field below', - 'guide.step3': 'Click Save — then pick it from the model list', - 'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed', - 'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied', - 'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required', - 'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required', - 'confirm.modelDelete': 'Delete this model profile?', - 'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure', - 'model.emptyMixin': 'No models added yet', - 'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder', - 'err.mixinFailed': 'Operation failed', - 'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs', - 'page.channels.title': 'Channels', 'page.channels.sub': 'Background IM processes: list, start/stop, logs (hub.pyw style)', - 'page.status.title': 'Status', 'page.status.sub': 'Background processes/services managed by hub.pyw', - 'page.collab.title': 'Conductor', 'page.collab.sub': 'Describe a goal — split, delegate, and follow up', - 'collab.progressTitle': 'Progress', - 'collab.progressEmpty': 'No tasks running yet. After you describe a goal to Conductor, split tasks will appear here.', - 'collab.placeholder': 'Describe the goal you want to accomplish', - 'collab.guideTitle': 'Tell Conductor what you want done', - 'collab.guideWhen': 'Best for multi-step goals that take a while. For everyday chat and quick questions, use Chat in the sidebar.', - 'collab.guideStep1t': 'Describe your goal', - 'collab.guideStep1d': 'Write what you want done in the chat box and send it to Conductor', - 'collab.guideStep2t': 'Auto breakdown', - 'collab.guideStep2d': 'Conductor breaks down, assigns, monitors, and coordinates', - 'collab.guideStep3t': 'Summary', - 'collab.guideStep3d': 'Conductor delivers a briefing based on execution status', - 'collab.guideStep4t': 'Adjust anytime', - 'collab.guideStep4d': 'Add requirements or details anytime — Conductor handles them', - 'collab.chipProgress': 'How is it going?', - 'collab.chipPause': 'Pause current tasks', - 'collab.chipSummary': 'Summarize progress so far', - 'collab.showProgressTitle': 'View task progress', - 'collab.statRunning': 'Running', - 'collab.statDone': 'Done', - 'collab.plusMenu': 'More actions', - 'collab.switchMode': 'Switch mode', - 'collab.typing': 'Conductor is working', - 'collab.offline': 'Cannot reach the service. Make sure the backend is running.', - 'collab.retry': 'Retry', - 'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.', - 'collab.reconnectIn': 'Retry in {n}s', - 'collab.stRunning': 'Running', 'collab.stReported': 'Reported', 'collab.stPaused': 'Paused', - 'collab.stFailed': 'Issue', 'collab.stTerminated': 'Ended', - 'collab.summaryRunning': 'Working…', 'collab.summaryWait': 'Awaiting report', - 'collab.taskFallback': 'Task {n}', - 'collab.timeJust': 'just now', - 'collab.timeSec': '{n}s ago', - 'collab.timeMin': '{n}m ago', - 'collab.timeHr': '{n}h ago', - 'collab.timeDay': '{n}d ago', - 'page.token.title': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate', - 'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…', - 'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby', - 'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat', - 'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed', - 'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again', - 'sys.interruptPrev.hint': 'Previous reply stopped — processing new message', - 'chat.interrupting': 'Stopping previous reply…', - 'chat.sessionLoading': 'Loading conversation…', - 'sys.stopRequested': 'Stop requested', - 'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings', - 'slash.unknown': 'Unknown command', - 'upload.hint': 'Upload file: pick / drag / paste', - 'upload.button': 'Upload file', - 'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file', - 'upload.failed': 'Upload failed', - 'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}', - 'file.openFailed': 'Cannot open file', - 'file.kindGeneric': 'File', - 'file.kindDoc': 'Document', - 'file.kindSheet': 'Spreadsheet', - 'file.kindSlide': 'Slides', - 'file.kindCode': 'Code', - 'file.kindArchive': 'Archive', - 'file.kindAudio': 'Audio', - 'file.kindVideo': 'Video', - 'upload.removeTitle': 'Remove', - 'upload.dropHint': 'Drop to upload files', - 'lightbox.closeTitle': 'Close', - 'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}', - 'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})', - 'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete', - 'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more', - 'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details', - 'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done', - 'timing.elapsed': 'Elapsed {t}', - 'model.auto': 'Auto', - 'model.menuLabel': 'Select model', - 'chip.plan': 'Plan', - 'chip.auto': 'Auto', - 'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk', - 'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord', - 'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found', - 'ch.logEmpty': 'No log output yet', - 'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed', - 'err.mykeyImport': 'Failed to import model config', - 'err.mykeyExport': 'Failed to export model config', - 'err.channelNotConfigured': 'Configure this platform in mykey.py first', - 'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped', - 'modal.channelLogs': 'Process logs', - 'modal.mykeyConfig': 'mykey.py', - 'sys.configSaved': 'Configuration saved', - 'sys.mykeyImported': 'Model config imported', - 'sys.mykeyExported': 'Model config exported', - 'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error', - 'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit', - 'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send', - 'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor', - 'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle', - 'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks', - 'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.', - 'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost', - 'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted', - 'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults', - 'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $', - 'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $', - 'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.', - 'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.', - 'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.', - 'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.', - 'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.', - 'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.', - 'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.', - 'ask.banner': 'GA is waiting for your answer', - 'ask.replyHint': 'Reply in the input below', - 'ask.placeholderOpen': 'Type your answer here… (Enter to send)', - }, -}; -const LANGS = ['zh', 'en']; +const I18N = window.GA_I18N?.dict || { zh: {}, en: {} }; +const LANGS = window.GA_I18N?.languages || ['zh', 'en']; const STORE = { lang: 'ga_lang', theme: 'ga_theme', appearance: 'ga_appearance', plain: 'ga_plain', fontSize: 'ga_font_size' }; const APPEARANCE_IDS = ['light', 'dark']; const CHAT_FONT_MIN = 10; @@ -721,7 +374,14 @@ async function bridgeFetch(path, opts = {}) { if (!res.ok) throw new Error(data.error || data.message || res.statusText); return data; } -function t(key) { return (I18N[lang] && I18N[lang][key]) || (I18N.zh[key]) || key; } +function t(key, vars) { + if (window.GA_I18N?.t) return window.GA_I18N.t(lang, key, vars); + let text = (I18N[lang] && I18N[lang][key]) || (I18N.zh && I18N.zh[key]) || key; + if (vars) { + for (const [name, value] of Object.entries(vars)) text = text.replaceAll(`{${name}}`, String(value)); + } + return text; +} window.gaT = t; document.addEventListener('collab:running-count', e => { const b = document.getElementById('collab-badge'); @@ -734,6 +394,22 @@ function optionalPh(key) { const sep = (lang === 'en') ? ', ' : ','; return `${t('common.optional')}${sep}${t(key)}`; } +function setTooltip(el, text) { + if (!el) return; + const value = String(text || '').trim(); + el.removeAttribute('title'); + if (el.hasAttribute('data-no-tooltip')) { + delete el.dataset.tooltip; + if (value) el.setAttribute('aria-label', value); + return; + } + if (value) { + el.dataset.tooltip = value; + if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', value); + } else { + delete el.dataset.tooltip; + } +} function applyI18n() { document.documentElement.lang = (lang === 'en') ? 'en' : 'zh-CN'; document.title = t('app.title'); @@ -744,7 +420,8 @@ function applyI18n() { if (el.isContentEditable) el.setAttribute('data-ph', val); // contenteditable 用 :empty::before 显示 else el.setAttribute('placeholder', val); }); - document.querySelectorAll('[data-i18n-title]').forEach(el => { el.setAttribute('title', t(el.dataset.i18nTitle)); }); + document.querySelectorAll('[data-i18n-title]').forEach(el => { setTooltip(el, t(el.dataset.i18nTitle)); }); + migrateNativeTooltips(); renderLangList(); // 语言切换后重算激活模型 chip 文案;若当前会话已有渠道组运行态模型,保留运行态而非退回首选项 const _ap = (state.modelProfiles || []).find(p => (p.id ?? 0) === state.llmNo); @@ -1738,7 +1415,7 @@ const state = { }; function rt(sess) { let r = state.runtime.get(sess.id); - if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); } + if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planVisiblePayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); } return r; } const activeSess = () => state.sessions.get(state.activeId) || null; @@ -1786,7 +1463,7 @@ const MIN_MSG_LOADING_MS = 450; const HYDRATE_LOADING_TIMEOUT_MS = 10000; const POLL_MSG_LIMIT = 200; const PLAN_LOST_GRACE_MS = 1500; // tuiapp_v2._PLAN_LOST_GRACE_SEC -const PLAN_COMPLETE_GRACE_MS = 3000; // tuiapp_v2._PLAN_GRACE_SEC +const PLAN_DISMISS_STORE = 'ga_plan_dismissed_v1'; let _submitInFlight = false; const runToggle = document.getElementById('run-toggle'); @@ -1861,6 +1538,43 @@ function planTpl(tpl, v) { return String(tpl || '').replace(/\{(\w+)\}/g, (_, k) => (v[k] != null ? String(v[k]) : `{${k}}`)); } +function planItemsSignature(items) { + return (items || []) + .map(it => `${String(it.status || '')}\u001e${String(it.content || '')}`) + .join('\u001f'); +} + +function planDismissId(sess, plan) { + if (!sess || !plan) return ''; + const total = plan.total ?? (plan.items || []).length; + const done = plan.done ?? (plan.items || []).filter(it => it.status === 'done').length; + return `${sess.id || sess.bridgeSessionId || 'session'}\u001d${done}/${total}\u001d${planItemsSignature(plan.items || [])}`; +} + +function readPlanDismissed() { + try { + const raw = localStorage.getItem(PLAN_DISMISS_STORE); + const parsed = raw ? JSON.parse(raw) : {}; + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch (_) { + return {}; + } +} + +function isPlanDismissed(sess, plan) { + if (!plan?.complete) return false; + const id = planDismissId(sess, plan); + return !!(id && readPlanDismissed()[id]); +} + +function markPlanDismissed(sess, plan) { + const id = planDismissId(sess, plan); + if (!id) return; + const dismissed = readPlanDismissed(); + dismissed[id] = Date.now(); + try { localStorage.setItem(PLAN_DISMISS_STORE, JSON.stringify(dismissed)); } catch (_) {} +} + let planPollTimer; function syncPlanPollTimer() { const on = !!(activeSess()?.bridgeSessionId && state.bridgeReady); @@ -1881,21 +1595,12 @@ function clearPlanGrace(r) { r.planCompleteAt = r.planLostAt = null; r.planHoldItems = []; r.planLastPayload = null; + r.planVisiblePayload = null; r.planLastComplete = false; r.planDismissedComplete = false; if (r.planHideTimer) { clearTimeout(r.planHideTimer); r.planHideTimer = null; } } -function schedulePlanCompleteDismiss(sess) { - const r = rt(sess); - if (r.planHideTimer) clearTimeout(r.planHideTimer); - r.planHideTimer = setTimeout(() => { - r.planHideTimer = null; - r.planDismissedComplete = true; - if (isActive(sess)) refreshPlanBar(null); - }, PLAN_COMPLETE_GRACE_MS); -} - /** tuiapp_v2._refresh_planbar:用 runtime 里缓存的 items / placeholder 重绘 */ function refreshPlanBarFromRuntime(sess) { const r = rt(sess); @@ -1910,11 +1615,6 @@ function refreshPlanBarFromRuntime(sess) { refreshPlanBar(null); return; } - if (r.planCompleteAt != null && Date.now() - r.planCompleteAt >= PLAN_COMPLETE_GRACE_MS) { - r.planDismissedComplete = true; - refreshPlanBar(null); - return; - } if (!items.length) { if (lp?.active && lp.placeholder) { refreshPlanBar(lp); @@ -1975,7 +1675,9 @@ function applyPlanPayload(sess, raw) { const wasComplete = r.planLastComplete; if (nowComplete && !wasComplete) { r.planCompleteAt = now; - schedulePlanCompleteDismiss(sess); + r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems }); + } else if (nowComplete) { + r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems }); } else if (!nowComplete) { r.planCompleteAt = null; r.planDismissedComplete = false; @@ -2053,16 +1755,23 @@ function bindPlanCardUiOnce() { if (!sess) return; const r = rt(sess); const payload = r.planLastPayload; - if (!payload?.active) return; + const visiblePayload = r.planVisiblePayload || payload; + if (e.target.closest('[data-plan-dismiss]')) { + markPlanDismissed(sess, visiblePayload); + r.planDismissedComplete = true; + refreshPlanBar(null); + return; + } + if (!payload?.active && !visiblePayload?.active) return; if (e.target.closest('[data-plan-expand]')) { r.planCollapsed = false; - refreshPlanBar(payload); + refreshPlanBar(visiblePayload); } else if (e.target.closest('[data-plan-collapse]')) { r.planCollapsed = true; - refreshPlanBar(payload); + refreshPlanBar(visiblePayload); } else if (e.target.closest('[data-plan-details]')) { r.planShowAll = !r.planShowAll; - refreshPlanBar(payload); + refreshPlanBar(visiblePayload); } }); } @@ -2074,6 +1783,7 @@ function refreshPlanBar(plan) { planBarEl.hidden = true; planBarEl.replaceChildren(); planBarEl.className = 'plan-card'; + planBarEl._planRenderSig = ''; return; } const sess = activeSess(); @@ -2088,6 +1798,21 @@ function refreshPlanBar(plan) { plan.complete ? 'plan-card--complete' : '', plan.placeholder ? 'plan-card--placeholder' : '', ].filter(Boolean).join(' '); + const planItemsSig = planItemsSignature(plan.items || []); + if (sess && plan.complete && isPlanDismissed(sess, plan)) { + r.planDismissedComplete = true; + planBarEl.hidden = true; + planBarEl.replaceChildren(); + planBarEl.className = 'plan-card'; + planBarEl._planRenderSig = ''; + return; + } + r.planVisiblePayload = plan; + const renderSig = JSON.stringify([ + lang, mod, stepText, done, total, !!r.planShowAll, plan.pathHint || '', planItemsSig + ]); + if (!planBarEl.hidden && planBarEl.className === mod && planBarEl._planRenderSig === renderSig) return; + planBarEl._planRenderSig = renderSig; planBarEl.hidden = false; planBarEl.className = mod; @@ -2100,11 +1825,17 @@ function refreshPlanBar(plan) { btn.dataset.planExpand = '1'; const dot = document.createElement('span'); dot.className = 'plan-status-dot'; + const ico = document.createElement('span'); + ico.className = 'plan-capsule-ic'; + ico.innerHTML = GA_ICON('listChecks'); const txt = document.createElement('span'); txt.className = 'plan-capsule-text'; if (cap.step) txt.innerHTML = `${escapeHtml(cap.tag)} · ${escapeHtml(cap.step)}`; else txt.textContent = cap.tag; - btn.append(dot, txt); + const chev = document.createElement('span'); + chev.className = 'plan-capsule-chevron'; + chev.innerHTML = GA_ICON('caretRight'); + btn.append(dot, ico, txt, chev); planBarEl.append(btn); return; } @@ -2128,11 +1859,19 @@ function refreshPlanBar(plan) { } const actions = document.createElement('div'); actions.className = 'plan-head-actions'; + if (plan.complete) { + const dismissBtn = document.createElement('button'); + dismissBtn.type = 'button'; + dismissBtn.className = 'plan-btn'; + dismissBtn.dataset.planDismiss = '1'; + dismissBtn.innerHTML = `${GA_ICON('x', 'plan-btn-ic')}${escapeHtml(t('plan.dismiss'))}`; + actions.append(dismissBtn); + } const collapseBtn = document.createElement('button'); collapseBtn.type = 'button'; collapseBtn.className = 'plan-btn'; collapseBtn.dataset.planCollapse = '1'; - collapseBtn.textContent = t('plan.fold'); + collapseBtn.innerHTML = `${GA_ICON('caretDown', 'plan-btn-ic')}${escapeHtml(t('plan.fold'))}`; actions.append(collapseBtn); head.append(actions); frag.append(head); @@ -2192,7 +1931,7 @@ function refreshPlanBar(plan) { det.type = 'button'; det.className = 'plan-btn'; det.dataset.planDetails = '1'; - det.textContent = r.planShowAll ? t('plan.collapse') : t('plan.details'); + det.innerHTML = `${GA_ICON(r.planShowAll ? 'caretDown' : 'caretRight', 'plan-btn-ic')}${escapeHtml(r.planShowAll ? t('plan.collapse') : t('plan.details'))}`; foot.append(det); } if (foot.childNodes.length) frag.append(foot); @@ -2781,7 +2520,7 @@ function renderSessionList() { `
` + `
${pinSvg}${escapeHtml(displayTitle(sess))}
` + `
${busy ? t('status.running') : t('status.idle')}
` + - ``; + ``; convListEl.appendChild(item); } } @@ -2905,6 +2644,26 @@ async function closeSession(id) { const convMenu = document.getElementById('conv-menu'); let menuTargetId = null; +function hideConvMenu() { + if (!convMenu) return; + convMenu.hidden = true; + menuTargetId = null; +} +function positionConvMenu(anchor) { + if (!convMenu || !anchor) return; + convMenu.hidden = false; + const rect = anchor.getBoundingClientRect(); + const pad = 8; + const gap = 4; + const w = convMenu.offsetWidth; + const h = convMenu.offsetHeight; + const left = Math.max(pad, Math.min(window.innerWidth - w - pad, rect.right - w)); + const below = rect.bottom + gap; + const above = rect.top - h - gap; + const top = below + h <= window.innerHeight - pad ? below : Math.max(pad, above); + convMenu.style.left = `${left}px`; + convMenu.style.top = `${top}px`; +} function isSessionRowInteractiveTarget(target) { const el = target instanceof Element ? target : null; return !!el?.closest?.( @@ -2915,7 +2674,18 @@ convListEl.addEventListener('click', (e) => { const more = e.target.closest('.ci-more'); if (more) { e.stopPropagation(); - menuTargetId = more.closest('.conv-item').dataset.id; + hideTooltip(); + const item = more.closest('.conv-item'); + const nextTargetId = item?.dataset.id || null; + if (!nextTargetId) { + hideConvMenu(); + return; + } + if (!convMenu.hidden && menuTargetId === nextTargetId) { + hideConvMenu(); + return; + } + menuTargetId = nextTargetId; // 根据当前会话置顶状态切菜单文案:置顶 / 取消置顶 const tgt = state.sessions.get(menuTargetId); const pinSpan = convMenu.querySelector('[data-act="pin"] [data-i18n]'); @@ -2924,10 +2694,7 @@ convListEl.addEventListener('click', (e) => { pinSpan.setAttribute('data-i18n', k); pinSpan.textContent = t(k); } - convMenu.hidden = false; - const rect = more.getBoundingClientRect(); - convMenu.style.top = (rect.bottom + 4) + 'px'; - convMenu.style.left = (rect.right - convMenu.offsetWidth) + 'px'; + positionConvMenu(more); return; } if (isSessionRowInteractiveTarget(e.target)) return; @@ -2961,7 +2728,7 @@ convMenu.addEventListener('click', (e) => { patchSession(sess, { pinned: sess.pinned }); renderSessionList(); } else if (sess && act === 'rename') { - convMenu.hidden = true; + hideConvMenu(); const item = convListEl.querySelector(`.conv-item[data-id="${sess.id}"]`); if (!item) return; const titleEl = item.querySelector('.ci-title'); @@ -3003,9 +2770,9 @@ convMenu.addEventListener('click', (e) => { } else if (sess && act === 'del') { closeSession(sess.id); } - convMenu.hidden = true; + hideConvMenu(); }); -document.addEventListener('click', () => { convMenu.hidden = true; }); +document.addEventListener('click', hideConvMenu); newConvBtn.addEventListener('click', (e) => { e.preventDefault(); newSession(); }); /* ═══════════════ 轮询 + 流式 ═══════════════ */ @@ -3205,9 +2972,19 @@ async function pollSession(sess) { try { const result = await fetchSessionPoll(sess); consecutiveErrors = 0; - const busy = applyPollResult(sess, result); + let busy = applyPollResult(sess, result); if (busy) await new Promise(z => setTimeout(z, 500)); else { + const hasFinalAssistant = (result.messages || []).some(m => m && m.role === 'assistant'); + if (r.draftEl && !hasFinalAssistant) { + await new Promise(z => setTimeout(z, 150)); + const finalResult = await fetchSessionPoll(sess); + busy = applyPollResult(sess, finalResult); + if (busy) { + await new Promise(z => setTimeout(z, 500)); + continue; + } + } if (r.draftEl) { r.draftEl.remove(); r.draftEl = null; r.draftSegs = null; r.draftTurn = 0; } resetTypewriterState(r); break; @@ -3364,7 +3141,11 @@ async function sendPrompt(text) { if (res?.error) throw new Error(res.error.message || res.error); removeUsedPendingFiles(usedFiles); const uid = Number(res.userMessageId || res.result?.userMessageId || 0); - if (uid) { r.seen.add(uid); r.lastId = Math.max(r.lastId, uid); } + if (uid) { + userMsg.id = uid; + r.seen.add(uid); + r.lastId = Math.max(r.lastId, uid); + } planPoll(sess); pollSession(sess); return true; @@ -3392,9 +3173,10 @@ async function submitInput() { if (_submitInFlight) return; let text = composerText('chat'); if (!text.trim()) return; - if (text.trim().startsWith('/')) { + const slashName = slashCommandName(text); + if (slashName) { inputEl.innerHTML = ''; - handleSlash(text.trim()); + handleSlash(slashName); return; } if (text.length > 20000) { @@ -3443,8 +3225,103 @@ function showToast(text) { clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove('show'), 1800); } -async function handleSlash(cmd) { - const name = cmd.slice(1).split(/\s+/)[0]; + +let tooltipEl = null; +let tooltipTarget = null; +let tooltipTimer = null; +function ensureTooltipEl() { + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.className = 'ga-tooltip'; + tooltipEl.hidden = true; + document.body.appendChild(tooltipEl); + } + return tooltipEl; +} +function migrateNativeTooltips(root = document) { + const nodes = []; + if (root?.nodeType === Node.ELEMENT_NODE && root.hasAttribute?.('title')) nodes.push(root); + root?.querySelectorAll?.('[title]').forEach(el => nodes.push(el)); + nodes.forEach(el => { + const value = el.getAttribute('title') || ''; + setTooltip(el, value); + }); +} +function tooltipNodeFromEventTarget(target) { + const el = target?.closest?.('[data-tooltip],[title]'); + if (el?.hasAttribute('title')) { + const value = el.getAttribute('title') || ''; + setTooltip(el, value); + } + return el?.dataset?.tooltip ? el : null; +} +function positionTooltip() { + if (!tooltipEl || !tooltipTarget || tooltipEl.hidden) return; + const r = tooltipTarget.getBoundingClientRect(); + const pad = 8; + const tw = tooltipEl.offsetWidth; + const th = tooltipEl.offsetHeight; + const left = Math.max(pad, Math.min(window.innerWidth - tw - pad, r.left + r.width / 2 - tw / 2)); + const topAbove = r.top - th - 8; + const top = topAbove >= pad ? topAbove : Math.min(window.innerHeight - th - pad, r.bottom + 8); + tooltipEl.style.left = `${left}px`; + tooltipEl.style.top = `${Math.max(pad, top)}px`; +} +function showTooltip(target) { + const text = target?.dataset?.tooltip || ''; + if (!text) return; + tooltipTarget = target; + clearTimeout(tooltipTimer); + tooltipTimer = setTimeout(() => { + const el = ensureTooltipEl(); + el.textContent = text; + el.hidden = false; + el.classList.add('show'); + positionTooltip(); + }, 180); +} +function hideTooltip() { + clearTimeout(tooltipTimer); + tooltipTimer = null; + tooltipTarget = null; + if (tooltipEl) { + tooltipEl.classList.remove('show'); + tooltipEl.hidden = true; + } +} +function initCustomTooltips() { + migrateNativeTooltips(); + document.addEventListener('pointerover', (e) => { + const target = tooltipNodeFromEventTarget(e.target); + if (target) showTooltip(target); + }, true); + document.addEventListener('pointerout', (e) => { + if (!tooltipTarget) return; + const from = e.target?.closest?.('[data-tooltip]'); + if (from === tooltipTarget && !tooltipTarget.contains(e.relatedTarget)) hideTooltip(); + }, true); + document.addEventListener('focusin', (e) => { + const target = tooltipNodeFromEventTarget(e.target); + if (target) showTooltip(target); + }, true); + document.addEventListener('focusout', hideTooltip, true); + window.addEventListener('scroll', hideTooltip, true); + window.addEventListener('resize', hideTooltip); + new MutationObserver((records) => { + for (const rec of records) { + if (rec.type === 'attributes' && rec.attributeName === 'title') migrateNativeTooltips(rec.target); + rec.addedNodes?.forEach(node => migrateNativeTooltips(node)); + } + }).observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['title'] }); +} +const SLASH_COMMANDS = new Set(['help', 'new', 'clear', 'stop', 'settings']); +function slashCommandName(text) { + const trimmed = String(text || '').trim(); + if (!trimmed.startsWith('/')) return ''; + const token = trimmed.slice(1).split(/\s+/)[0]; + return SLASH_COMMANDS.has(token) ? token : ''; +} +async function handleSlash(name) { switch (name) { case 'help': showSystem(t('slash.help')); break; case 'new': await newSession(); break; @@ -3919,7 +3796,7 @@ function renderModelMenu(menuEl) { } function openModelMenu(chipEl, menuEl) { if (!chipEl || !menuEl) return; - if (typeof convMenu !== 'undefined' && convMenu) convMenu.hidden = true; + if (typeof hideConvMenu === 'function') hideConvMenu(); window.collabComposer?.closeMenu?.(); closeAllModelMenus(); renderModelMenu(menuEl); @@ -4462,7 +4339,10 @@ window.ga.onBridgeNotification((msg) => { if (msg && msg.type === 'session-state') { for (const sess of state.sessions.values()) { if (sess.bridgeSessionId === msg.sessionId) { - if (msg.status === 'running' || msg.state === 'running') pollSession(sess); + if (['running', 'idle', 'error', 'cancelled'].includes(msg.status) || + ['running', 'idle', 'error', 'cancelled'].includes(msg.state)) { + pollSession(sess); + } if (msg.state === 'idle' || msg.status === 'idle') tokPollBridge(); renderSessionList(); break; @@ -4994,49 +4874,13 @@ if (msgArea) { function uploadRawUrl(path, download) { return `${BRIDGE_ORIGIN}/upload/raw?path=${encodeURIComponent(path || '')}${download ? '&download=1' : ''}`; } -function bridgeIsLocal() { - return location.hostname === '127.0.0.1' || location.hostname === 'localhost'; -} async function openUploadFile(path, name) { - // 远程访问:浏览器无法调起 bridge 那台/本机的系统程序,降级为下载到本机 - if (!bridgeIsLocal()) { - const a = document.createElement('a'); - a.href = uploadRawUrl(path, true); - a.download = name || ''; - document.body.appendChild(a); a.click(); a.remove(); - return; - } - // 本地:bridge 与你同机,调系统默认程序打开 / 在文件夹显示 - const mode = isPreviewableByName(name || path) ? 'open' : 'reveal'; - try { - const res = await fetch(`${BRIDGE_ORIGIN}/path/open`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ kind: 'upload', path, mode }), - }); - const j = await res.json(); - if (!j.ok) throw new Error(j.error || 'open failed'); - } catch (e) { - showChanToast(t('file.openFailed'), e.message || String(e), 'err'); - } -} - -const PREVIEWABLE_EXTS = new Set([ - 'pdf', - 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'heic', 'tiff', - 'txt', 'md', 'log', 'json', 'yaml', 'yml', 'xml', 'csv', 'tsv', 'ini', 'toml', 'env', 'rtf', - 'py', 'js', 'ts', 'tsx', 'jsx', 'java', 'c', 'cpp', 'h', 'hpp', 'rs', 'go', 'rb', 'php', 'sh', 'bash', 'zsh', 'fish', 'lua', 'pl', 'r', 'scala', 'kt', 'swift', - 'html', 'htm', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', 'sql', - 'doc', 'docx', 'pages', 'odt', - 'xls', 'xlsx', 'numbers', 'ods', - 'ppt', 'pptx', 'key', 'odp', - 'mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', - 'mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', -]); -function isPreviewableByName(name) { - const m = String(name || '').match(/\.([^./\\]+)$/); - if (!m) return false; - return PREVIEWABLE_EXTS.has(m[1].toLowerCase()); + const a = document.createElement('a'); + a.href = uploadRawUrl(path, true); + a.download = name || ''; + document.body.appendChild(a); + a.click(); + a.remove(); } /* ═══════════════ 后台服务页 Tab(消息通道 / 状态面板) ═══════════════ */ @@ -5690,6 +5534,7 @@ initChatFontStepper(); applyChatFontSize(chatFontSize, { persist: false }); syncHljsTheme(); applyI18n(); +initCustomTooltips(); updateModelChip(); renderSessionList(); loadCustomPresets(); diff --git a/frontends/desktop/static/fallback.html b/frontends/desktop/static/fallback.html index c53f3c4e..c13808e8 100644 --- a/frontends/desktop/static/fallback.html +++ b/frontends/desktop/static/fallback.html @@ -2,7 +2,7 @@ -GenericAgent — Setup +GenericAgent +
-

⚙️ Setup Required

-

The backend could not start. Please configure the paths below.

-

后端启动失败,请配置以下路径。

+

+

- +
- +
-

- e.g. C:/Python312/python.exe / ~/miniconda3/envs/myenv/bin/python -

+

- +
- +
-

- The folder containing frontends/desktop_bridge.py -

+

- +
- + + diff --git a/frontends/desktop/static/loading.html b/frontends/desktop/static/loading.html index f02eddb3..e8fe9e59 100644 --- a/frontends/desktop/static/loading.html +++ b/frontends/desktop/static/loading.html @@ -17,21 +17,16 @@ html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:c #ga-bar.indet{width:35%;animation:indet 1.1s ease-in-out infinite} @keyframes indet{0%{margin-left:-35%}100%{margin-left:100%}} +
-
正在启动 GenericAgent…
+
diff --git a/frontends/desktop/static/styles.css b/frontends/desktop/static/styles.css index 687a3976..710ad424 100644 --- a/frontends/desktop/static/styles.css +++ b/frontends/desktop/static/styles.css @@ -92,8 +92,11 @@ --shadow-pop:0 8px 24px #14192829; --shadow-modal:0 18px 50px #14192847; - --divider-base:rgba(15, 74, 133, 0.22); + --divider-base:rgba(0,0,0,.12); --divider:var(--divider-base); + --tooltip-bg:var(--card); + --tooltip-fg:var(--txt); + --tooltip-border:var(--line-strong); --radius:4px; --radius-sm:2px; /* —— 折叠块 —— */ @@ -124,7 +127,7 @@ html[data-appearance="light"]:not([data-plain="1"])[data-theme]{ --bg:color-mix(in srgb, var(--accent) 7%, var(--bg-base)); --line:color-mix(in srgb, var(--accent) 16%, var(--line-base)); --line-soft:color-mix(in srgb, var(--accent) 10%, var(--line-soft-base)); - --divider:color-mix(in srgb, var(--accent) 22%, var(--divider-base)); + --divider:var(--divider-base); } /* —— 外观:深色 —— */ @@ -174,7 +177,7 @@ html[data-appearance="dark"]{ --fold-tool-bg:#16202b; --fold-result-border:#3f6647; --fold-result-bg:#15241a; - --divider-base:#569cd6; + --divider-base:rgba(255,255,255,.10); --divider:var(--divider-base); /* 发送键:深色用鲜亮深蓝 */ --ink-green:#2559d8; @@ -183,7 +186,7 @@ html[data-appearance="dark"]{ html[data-appearance="dark"][data-theme]{ --accent-bg:color-mix(in srgb, var(--accent) 22%, var(--panel)); --accent-soft:color-mix(in srgb, var(--accent) 12%, var(--panel)); - --divider:color-mix(in srgb, var(--accent) 18%, var(--divider-base)); + --divider:var(--divider-base); } html,body{ height:100%; } @@ -400,6 +403,17 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f transition:opacity .18s, transform .18s; z-index:9999; } .ga-toast.show{ opacity:1; transform:translateX(-50%) translateY(0); } +.ga-tooltip{ + position:fixed; z-index:10000; max-width:min(320px, calc(100vw - 16px)); + padding:6px 8px; border:1px solid var(--tooltip-border); border-radius:4px; + background:var(--tooltip-bg); + color:var(--tooltip-fg); font:12px/1.35 var(--font-sans); + box-shadow:0 8px 20px #00000026; + pointer-events:none; opacity:0; transform:translateY(2px); + transition:opacity .12s, transform .12s; + white-space:normal; overflow-wrap:anywhere; +} +.ga-tooltip.show{ opacity:1; transform:translateY(0); } .set-btn{ width:100%; display:flex; align-items:center; gap:8px; border:1px solid var(--line); background:var(--card); color:var(--txt-2); @@ -584,7 +598,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f background:var(--field-bg); cursor:pointer; text-align:left; transition:border-color .15s, background .15s; } -.pq-btn:hover{ background:var(--card); border-color:var(--accent); } +.pq-btn:hover{ background:var(--line-soft); border-color:var(--line-base); } .pq-logo{ flex:0 0 auto; width:26px; height:26px; border-radius:7px; display:flex; align-items:center; justify-content:center; @@ -594,7 +608,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f .pq-btn-name{ font-size:12px; font-weight:500; color:var(--txt); line-height:1.35; } .pq-btn-desc{ font-size:10px; color:var(--muted); line-height:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pq-arrow{ flex:0 0 auto; width:13px; height:13px; color:var(--muted2); transition:transform .15s, color .15s; } -.pq-btn:hover .pq-arrow{ color:var(--accent); transform:translateX(2px); } +.pq-btn:hover .pq-arrow{ color:var(--txt-2); transform:none; } /* ── 添加模型弹窗内的接入指引横幅 ── */ .model-guide{ @@ -721,9 +735,10 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f .run-state.busy .dot::before{ content:''; position:absolute; - left:50%; top:50%; + inset:0; + margin:auto; width:var(--breathe-core); height:var(--breathe-core); - transform:translate(-50%,-50%); + transform:none; border-radius:50%; background:var(--breathe-color); z-index:2; @@ -732,9 +747,11 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f .run-state.busy .dot::after{ content:''; position:absolute; - left:50%; top:50%; + inset:0; + margin:auto; width:var(--breathe-core); height:var(--breathe-core); - transform:translate(-50%,-50%); + transform:none; + transform-origin:center; border-radius:50%; border:1px solid color-mix(in srgb, var(--breathe-color) 52%, transparent); z-index:1; @@ -742,8 +759,8 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f animation:ga-status-breathe 1.75s ease-out infinite; } @keyframes ga-status-breathe{ - 0%{ transform:translate(-50%,-50%) scale(1); opacity:.55; } - 100%{ transform:translate(-50%,-50%) scale(2.35); opacity:0; } + 0%{ transform:scale(1); opacity:.55; } + 100%{ transform:scale(2.35); opacity:0; } } .link{ color:var(--accent); cursor:pointer; } .muted{ color:var(--muted); } @@ -1263,10 +1280,6 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } align-self:end; } .composer-inset.is-inline .composer-bot .spacer{ display:none; } -.composer-inset:focus-within{ - border-color:var(--accent); - box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); -} .composer-inset .input{ width:100%; min-height:38px; max-height:160px; overflow-y:auto; border:none; border-radius:0; background:transparent; @@ -1368,7 +1381,7 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } margin:0 -3px; } .rp-resize:hover, .rp-resize.dragging, -.sb-resize:hover, .sb-resize.dragging{ background:var(--accent); } +.sb-resize:hover, .sb-resize.dragging{ background:var(--line-strong); } .body.rp-collapsed .rp-resize, .body.sb-collapsed .sb-resize{ display:none; } @@ -1496,58 +1509,67 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } } .plan-card{ position:absolute; - left:10px; - bottom:100%; - margin-bottom:0; + left:12px; + bottom:calc(100% + 8px); z-index:13; - width:min(600px, 100%); + width:min(640px, calc(100% - 24px)); min-width:280px; max-width:100%; box-sizing:border-box; - font-size:12px; - line-height:1.35; + font-size:13px; + line-height:1.45; color:var(--txt); pointer-events:auto; } .plan-card[hidden]{ display:none !important; } .plan-card--expanded{ - padding:10px 12px 8px; - background:var(--card); + padding:14px 16px 12px 18px; + background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%); border:1px solid var(--line); - border-bottom:none; - border-left:3px solid var(--accent); - border-radius:var(--radius-sm) var(--radius-sm) 0 0; - box-shadow:0 -4px 16px #14192812, 0 -1px 0 var(--line) inset; + border-radius:8px; + box-shadow:0 12px 32px #14192818, 0 1px 2px #14192814; + overflow:hidden; +} +.plan-card--expanded::before{ + content:""; + position:absolute; + left:0; + top:0; + bottom:0; + width:4px; + background:var(--accent); } .plan-card--collapsed{ width:auto; min-width:0; max-width:min(520px, 70%); - margin-bottom:0; } -/* 吸附:卡片底与输入框顶共边,视觉上连成一体 */ +/* Keep the composer rounded while the plan card floats above it. */ .composer-slot:has(.plan-card:not([hidden])) .composer-inset{ - border-top-left-radius:0; - border-top-right-radius:0; + border-top-left-radius:var(--radius); + border-top-right-radius:var(--radius); } .composer-slot:has(.plan-card--collapsed:not([hidden])) .plan-capsule{ - border-radius:var(--radius-sm) var(--radius-sm) 0 0; - border-bottom:1px solid var(--line); - box-shadow:0 -4px 12px #14192810; + box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810; } .plan-card-head{ display:flex; align-items:center; - gap:8px; - margin-bottom:6px; + gap:10px; + margin-bottom:10px; + min-height:26px; } .plan-status-dot{ flex:0 0 auto; - width:7px; height:7px; + width:8px; height:8px; border-radius:50%; background:var(--accent); + box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent); +} +.plan-card--complete .plan-status-dot{ + background:var(--success); + box-shadow:0 0 0 4px var(--success-ring); } -.plan-card--complete .plan-status-dot{ background:var(--success); } .plan-card--placeholder .plan-status-dot{ animation:plan-dot-pulse 1.4s ease-in-out infinite; } @@ -1557,8 +1579,8 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } } .plan-title{ flex:1 1 auto; - font-weight:600; - font-size:12px; + font-weight:700; + font-size:14px; color:var(--txt-strong); min-width:0; white-space:nowrap; @@ -1567,104 +1589,198 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; } } .plan-progress{ flex:0 0 auto; - font-weight:600; + display:inline-flex; + align-items:center; + min-height:24px; + padding:0 8px; + border:1px solid color-mix(in srgb, var(--accent) 26%, var(--line)); + border-radius:999px; + background:color-mix(in srgb, var(--accent) 9%, var(--card)); + font-weight:700; font-size:12px; - color:var(--accent); + color:var(--txt-strong); font-family:var(--font-num); font-variant-numeric:var(--font-num-variant); } -.plan-card--complete .plan-progress{ color:var(--success); } +.plan-card--complete .plan-progress{ + color:var(--success); + border-color:color-mix(in srgb, var(--success) 28%, var(--line)); + background:var(--success-bg); +} .plan-head-actions{ flex:0 0 auto; display:flex; align-items:center; - gap:4px; + gap:6px; } .plan-btn{ - border:0; + display:inline-flex; + align-items:center; + justify-content:center; + gap:5px; + min-height:26px; + border:1px solid transparent; background:transparent; - padding:2px 6px; - font-size:11px; - font-weight:500; - color:var(--accent); + padding:0 8px; + font-size:12px; + font-weight:600; + color:var(--muted); cursor:pointer; - border-radius:var(--radius-sm); + border-radius:6px; font-family:var(--font-sans); + transition:background .15s, border-color .15s, color .15s; +} +.plan-btn:hover{ + background:var(--field-bg); + border-color:var(--line); + color:var(--txt-strong); +} +.plan-btn-ic{ + width:12px; + height:12px; + flex:0 0 auto; } -.plan-btn:hover{ background:var(--accent-soft); } .plan-current{ display:flex; - gap:4px; + gap:6px; align-items:flex-start; - margin-bottom:8px; - padding-left:15px; + margin:0 0 10px 0; + padding:8px 10px; + border:1px solid var(--line-soft); + border-radius:6px; + background:color-mix(in srgb, var(--field-bg) 82%, var(--accent) 18%); } .plan-current-label{ flex:0 0 auto; color:var(--muted); - font-size:11px; + font-size:12px; } .plan-current-text{ flex:1 1 auto; - color:var(--accent); - font-weight:500; + color:var(--txt-strong); + font-weight:600; word-break:break-word; } .plan-card--complete .plan-current-text{ color:var(--success); } .plan-wait{ color:var(--muted2); - padding:0 0 4px 15px; - font-size:11px; + padding:4px 0 2px; + font-size:12px; } .plan-items{ list-style:none; margin:0; - padding:0 0 4px 15px; + padding:0; } .plan-item{ display:flex; - gap:6px; + gap:10px; align-items:flex-start; - margin-top:3px; + padding:5px 0; + min-height:26px; } .plan-item-mark{ flex:0 0 auto; - width:1.1em; - text-align:center; - line-height:1.35; + position:relative; + display:inline-flex; + align-items:center; + justify-content:center; + width:16px; + height:16px; + margin-top:2px; + border:1px solid var(--line-strong); + border-radius:50%; + background:var(--card); + color:var(--muted); + font-size:0; } .plan-item-text{ flex:1 1 auto; word-break:break-word; + color:var(--txt-2); +} +.plan-item--done .plan-item-mark{ + color:var(--success); + border-color:color-mix(in srgb, var(--success) 40%, var(--line)); + background:var(--success-bg); +} +.plan-item--done .plan-item-mark::after{ + content:""; + width:7px; + height:4px; + border-left:2px solid currentColor; + border-bottom:2px solid currentColor; + transform:translateY(-1px) rotate(-45deg); } -.plan-item--done .plan-item-mark, .plan-item--done .plan-item-text{ color:var(--muted); - opacity:.75; + opacity:.78; } .plan-item--done .plan-item-text{ text-decoration:line-through; } -.plan-item--current .plan-item-mark{ color:var(--accent); } -.plan-item--current .plan-item-text{ color:var(--accent); font-weight:500; } -.plan-item--pending .plan-item-mark, +.plan-item--current .plan-item-mark{ + border-color:var(--accent); + background:var(--accent); + box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent); +} +.plan-item--current .plan-item-mark::after{ + content:""; + width:5px; + height:5px; + border-radius:50%; + background:var(--on-accent); +} +.plan-item--current .plan-item-text{ + color:var(--txt-strong); + font-weight:700; +} +.plan-item--pending .plan-item-mark::after{ + content:""; + width:4px; + height:4px; + border-radius:50%; + background:var(--line-strong); +} .plan-item--pending .plan-item-text{ color:var(--muted); } -.plan-item--error .plan-item-mark, +.plan-item--error .plan-item-mark{ + color:var(--danger); + border-color:color-mix(in srgb, var(--danger) 38%, var(--line)); + background:var(--danger-bg); +} +.plan-item--error .plan-item-mark::before, +.plan-item--error .plan-item-mark::after{ + content:""; + position:absolute; + width:8px; + height:2px; + border-radius:999px; + background:currentColor; +} +.plan-item--error .plan-item-mark::before{ transform:rotate(45deg); } +.plan-item--error .plan-item-mark::after{ transform:rotate(-45deg); } .plan-item--error .plan-item-text{ color:var(--danger); } -.plan-item--warn .plan-item-mark, -.plan-item--warn .plan-item-text{ color:#c67600; } +.plan-item--warn .plan-item-mark{ + color:#c67600; + border-color:color-mix(in srgb, #c67600 38%, var(--line)); + background:color-mix(in srgb, #c67600 12%, var(--card)); + font-size:10px; + font-weight:800; +} +.plan-item--warn .plan-item-mark::after{ content:"!"; } +.plan-item--warn .plan-item-text{ color:#9b6100; } html[data-appearance="dark"] .plan-item--warn .plan-item-mark, html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; } .plan-foot{ display:flex; justify-content:flex-end; gap:8px; - padding-top:4px; + padding-top:10px; border-top:1px solid var(--line-soft); - margin-top:4px; + margin-top:6px; } .plan-more-hint{ flex:1 1 auto; align-self:center; - font-size:11px; + font-size:12px; color:var(--muted2); text-align:left; } @@ -1673,19 +1789,36 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; } align-items:center; gap:8px; max-width:100%; - padding:6px 12px; + min-height:34px; + padding:0 12px 0 10px; border:1px solid var(--line); - border-left:3px solid var(--accent); border-radius:999px; - background:var(--card); + background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%); cursor:pointer; font-size:12px; - font-weight:500; + font-weight:600; color:var(--txt); - box-shadow:var(--shadow); + box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810; font-family:var(--font-sans); + transition:background .15s, border-color .15s, box-shadow .15s; +} +.plan-capsule:hover{ + border-color:var(--line-strong); + background:var(--card); + box-shadow:0 12px 28px #1419281f, 0 1px 2px #14192812; +} +.plan-capsule-ic{ + width:15px; + height:15px; + flex:0 0 auto; + color:var(--muted); +} +.plan-capsule-chevron{ + width:12px; + height:12px; + flex:0 0 auto; + color:var(--muted2); } -.plan-capsule:hover{ border-color:var(--line-strong); background:var(--field-bg); } .plan-capsule-text{ min-width:0; white-space:nowrap; @@ -1698,7 +1831,9 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; } color:var(--accent); font-weight:600; } -.plan-card--complete .plan-capsule{ border-left-color:var(--success); } +.plan-card--complete .plan-capsule{ + background:color-mix(in srgb, var(--card) 88%, var(--success) 12%); +} .plan-card--complete .plan-capsule-text em{ color:var(--success); } /* ─── chat composer: Plan/Auto toggle ─── */ diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index 472542ac..a42693b0 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -70,6 +70,24 @@ def strip_final_info_marker(text: Any) -> str: return _FINAL_INFO_RE.sub('', str(text or '')) +def normalize_final_turn_segs(full: str, outputs: Any) -> Optional[List[str]]: + if not outputs or not isinstance(outputs, (list, tuple)): + return None + segs = [strip_final_info_marker(s) for s in outputs] + full_text = strip_final_info_marker(full) + if not segs: + return None + joined = "".join(segs) + if full_text.strip() == joined.strip(): + return segs + if joined and full_text.startswith(joined): + suffix = full_text[len(joined):] + if suffix.strip(): + segs[-1] = segs[-1] + suffix + return segs + return None + + for _s in (sys.stdout, sys.stderr): with contextlib.suppress(Exception): _s.reconfigure(encoding="utf-8", errors="replace") @@ -664,9 +682,8 @@ class AgentManager: _segs[_idx - 1] = str(_outs[-2]) if "done" in item: full = strip_final_info_marker(item.get("done") or "") - done_outputs = item.get("outputs") # done时=turn_resps.copy()全量轮 + done_outputs = normalize_final_turn_segs(full, item.get("outputs")) # done时=turn_resps.copy()全量轮 if done_outputs: - done_outputs = [strip_final_info_marker(s) for s in done_outputs] with self.lock: if sess.partial is not None: sess.partial["content"] = full @@ -696,12 +713,10 @@ class AgentManager: with self.lock: sess.partial = None full = strip_final_info_marker(full) - if done_outputs: - done_outputs = [strip_final_info_marker(s) for s in done_outputs] import plan_state plan_state.sync_plan_path_from_text(sess, full, sess.cwd or self.ga_root) # 轨道2: 落库时带结构化全量轮(权威turn_segs),前端按轮渲染;content保留兜底 - _final_segs = [str(s) for s in done_outputs] if done_outputs else None + _final_segs = normalize_final_turn_segs(full, done_outputs) if _final_segs: self.add_message(sess, "assistant", full, turn_segs=_final_segs) else: