From dd0636db49df52d3a353e20989a97933195d9f7e Mon Sep 17 00:00:00 2001 From: cfn Date: Thu, 5 Nov 2026 00:13:15 +0800 Subject: [PATCH] feat: add MCP workflows and refresh desktop experience Add model configuration, MCP management, persistent conversations, attachments, reasoning and Markdown rendering. Refresh workspace, settings and logs; bundle private runtimes and exclude generated local artifacts. --- .gitignore | 27 +- CODEMAP.md | 21 +- README.md | 67 ++- agent/README.md | 3 + build/darwin/Taskfile.yml | 19 +- build/windows/Taskfile.yml | 20 +- build/windows/nsis/project.nsi | 23 +- frontend/package-lock.json | 106 ++++ frontend/package.json | 2 + frontend/src/App.vue | 141 +++-- frontend/src/components/UiIcon.vue | 15 + .../src/components/chat/AttachmentList.vue | 27 + frontend/src/components/chat/ChatPane.vue | 417 +++++++++++---- .../src/components/chat/ConversationList.vue | 214 ++++++++ .../src/components/chat/MarkdownContent.vue | 37 ++ .../src/components/chat/ToolImageGallery.vue | 85 +++ frontend/src/components/device/DeviceBar.vue | 12 +- frontend/src/components/logs/LogPane.vue | 174 ++++-- .../src/components/screen/CapturePane.vue | 104 +++- .../src/components/settings/MCPSettings.vue | 290 ++++++++++ .../src/components/settings/ModelSettings.vue | 245 +++++++++ .../src/components/settings/SettingsPane.vue | 362 ++++--------- frontend/src/lib/markdown.ts | 25 + frontend/src/lib/platform.ts | 5 + frontend/src/lib/prefs.ts | 4 + frontend/src/lib/useConversations.ts | 315 +++++++++++ frontend/src/lib/useLogs.ts | 71 +++ frontend/src/main.ts | 2 + frontend/src/styles/app.css | 16 + frontend/src/types/index.ts | 62 +++ frontend/tests/conversations.test.mjs | 276 ++++++++++ frontend/tests/logs.test.mjs | 83 +++ frontend/tests/markdown.test.mjs | 40 ++ frontend/tests/prefs.test.mjs | 21 + go.mod | 21 +- go.sum | 40 +- internal/agent/attachment_extract.go | 277 ++++++++++ internal/agent/attachments.go | 328 ++++++++++++ internal/agent/attachments_test.go | 194 +++++++ internal/agent/config.go | 239 +++++++++ internal/agent/conversation_locks.go | 38 ++ internal/agent/conversations.go | 376 +++++++++++++ internal/agent/conversations_test.go | 221 ++++++++ internal/agent/memory.go | 247 +++++++++ internal/agent/memory_test.go | 247 +++++++++ internal/agent/model.go | 205 ++++++++ internal/agent/model_test.go | 250 +++++++++ internal/agent/reasoning_test.go | 90 ++++ internal/agent/screenshots.go | 135 +++++ internal/agent/screenshots_test.go | 122 +++++ internal/agent/service.go | 265 +++++++--- internal/agent/stream.go | 198 +++++++ internal/agent/stream_test.go | 148 ++++++ internal/agent/tools.go | 310 +++++++++++ internal/agent/tools_test.go | 200 +++++++ internal/applog/service.go | 178 +++++++ internal/applog/service_test.go | 129 +++++ internal/device/adb.go | 31 +- internal/device/process_other.go | 7 + internal/device/process_windows.go | 13 + internal/device/runtime_test.go | 49 ++ internal/device/stream.go | 85 +-- internal/device/stream_integration_test.go | 36 +- internal/device/touch.go | 34 +- internal/device/touch_test.go | 46 ++ internal/mcpclient/agent.go | 241 +++++++++ internal/mcpclient/agent_test.go | 70 +++ internal/mcpclient/filesystem.go | 195 +++++++ internal/mcpclient/filesystem_test.go | 94 ++++ internal/mcpclient/process_other.go | 7 + internal/mcpclient/process_windows.go | 12 + internal/mcpclient/service.go | 497 ++++++++++++++++++ internal/mcpclient/service_test.go | 260 +++++++++ internal/mcpclient/transport.go | 92 ++++ internal/runtimeassets/paths.go | 37 ++ internal/runtimeassets/paths_test.go | 25 + internal/runtimeassets/webview_other.go | 5 + internal/runtimeassets/webview_windows.go | 30 ++ main.go | 22 +- runtime/filesystem/FILESYSTEM-LICENSE.txt | 216 ++++++++ runtime/filesystem/package-lock.json | 171 ++++++ runtime/filesystem/package.json | 16 + scripts/bundle-macos-libs.py | 68 +++ scripts/package-windows-portable.ps1 | 6 + scripts/stage-filesystem-runtime.mjs | 52 ++ scripts/stage-macos-runtime.sh | 30 ++ scripts/sync-macos-runtime.sh | 24 + scripts/sync-windows-runtime.ps1 | 94 ++++ third_party/README.md | 7 + third_party/RUNTIME-NOTICES.txt | 125 +++++ 90 files changed, 9815 insertions(+), 671 deletions(-) create mode 100644 frontend/src/components/UiIcon.vue create mode 100644 frontend/src/components/chat/AttachmentList.vue create mode 100644 frontend/src/components/chat/ConversationList.vue create mode 100644 frontend/src/components/chat/MarkdownContent.vue create mode 100644 frontend/src/components/chat/ToolImageGallery.vue create mode 100644 frontend/src/components/settings/MCPSettings.vue create mode 100644 frontend/src/components/settings/ModelSettings.vue create mode 100644 frontend/src/lib/markdown.ts create mode 100644 frontend/src/lib/platform.ts create mode 100644 frontend/src/lib/useConversations.ts create mode 100644 frontend/src/lib/useLogs.ts create mode 100644 frontend/tests/conversations.test.mjs create mode 100644 frontend/tests/logs.test.mjs create mode 100644 frontend/tests/markdown.test.mjs create mode 100644 frontend/tests/prefs.test.mjs create mode 100644 internal/agent/attachment_extract.go create mode 100644 internal/agent/attachments.go create mode 100644 internal/agent/attachments_test.go create mode 100644 internal/agent/config.go create mode 100644 internal/agent/conversation_locks.go create mode 100644 internal/agent/conversations.go create mode 100644 internal/agent/conversations_test.go create mode 100644 internal/agent/memory.go create mode 100644 internal/agent/memory_test.go create mode 100644 internal/agent/model.go create mode 100644 internal/agent/model_test.go create mode 100644 internal/agent/reasoning_test.go create mode 100644 internal/agent/screenshots.go create mode 100644 internal/agent/screenshots_test.go create mode 100644 internal/agent/stream.go create mode 100644 internal/agent/stream_test.go create mode 100644 internal/agent/tools.go create mode 100644 internal/agent/tools_test.go create mode 100644 internal/applog/service.go create mode 100644 internal/applog/service_test.go create mode 100644 internal/device/process_other.go create mode 100644 internal/device/process_windows.go create mode 100644 internal/device/runtime_test.go create mode 100644 internal/mcpclient/agent.go create mode 100644 internal/mcpclient/agent_test.go create mode 100644 internal/mcpclient/filesystem.go create mode 100644 internal/mcpclient/filesystem_test.go create mode 100644 internal/mcpclient/process_other.go create mode 100644 internal/mcpclient/process_windows.go create mode 100644 internal/mcpclient/service.go create mode 100644 internal/mcpclient/service_test.go create mode 100644 internal/mcpclient/transport.go create mode 100644 internal/runtimeassets/paths.go create mode 100644 internal/runtimeassets/paths_test.go create mode 100644 internal/runtimeassets/webview_other.go create mode 100644 internal/runtimeassets/webview_windows.go create mode 100644 runtime/filesystem/FILESYSTEM-LICENSE.txt create mode 100644 runtime/filesystem/package-lock.json create mode 100644 runtime/filesystem/package.json create mode 100644 scripts/bundle-macos-libs.py create mode 100644 scripts/package-windows-portable.ps1 create mode 100644 scripts/stage-filesystem-runtime.mjs create mode 100644 scripts/stage-macos-runtime.sh create mode 100644 scripts/sync-macos-runtime.sh create mode 100644 scripts/sync-windows-runtime.ps1 create mode 100644 third_party/RUNTIME-NOTICES.txt diff --git a/.gitignore b/.gitignore index 412bfa7..a221ac7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,27 @@ /.task/ /frontend/dist/ /frontend/bindings/ -/frontend/node_modules/ -/frontend/*.tsbuildinfo +/frontend/.bindings-tmp-*/ +node_modules/ +.pnpm-store/ +.npm-cache/ +.vite/ +*.tsbuildinfo +/coverage/ +/coverage.out +/frontend/coverage/ + +# Generated Go / Wails binaries and packaging artifacts +/*.syso +/*.exe +/*.test +/*.test.exe +/ituoke +/iTuoke +/*.dmg +/*.zip +/*.msix +*.dSYM/ # Local Python environments and caches .venv/ @@ -37,6 +56,10 @@ __pycache__/ # Bundled SDK binaries are synchronized before packaging /third_party/platform-tools/ /third_party/scrcpy/ +/third_party/ffmpeg/ +/third_party/webview2/ +/third_party/mcp-filesystem/ +/third_party/runtime-manifest.json /build/linux/appimage/build/ /build/windows/nsis/MicrosoftEdgeWebview2Setup.exe diff --git a/CODEMAP.md b/CODEMAP.md index 1392e4b..f4f9417 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -10,7 +10,24 @@ | 内置 adb 同步到安装包 | `scripts/sync-platform-tools.sh` → `third_party/platform-tools/` | | 设备相关数据结构 | `internal/device/types.go` | | 无线配对/连接面板 | `frontend/src/components/device/ConnectPanel.vue` | -| 对话 / 启动 Agent / 协议版本 | `internal/agent/service.go` | +| 模型对话 / 内存上下文 / 模型状态 | `internal/agent/service.go` | +| 模型配置与系统凭据存储 | `internal/agent/config.go` | +| 模型 HTTP 请求 / 连接测试 / 模型列表 | `internal/agent/model.go` | +| Agent 多轮工具调用 / 结果与图片输入适配 / 执行事件 | `internal/agent/tools.go` | +| 简单截图快捷执行 / MCP 图片本地保存 | `internal/agent/screenshots.go` | +| 会话图片缩略图 / 放大预览 / 按需加载 | `frontend/src/components/chat/ToolImageGallery.vue` | +| MCP 会话生命周期 / 参数校验 / 操作序列排队 | `internal/mcpclient/agent.go` | +| SSE 流式解析 / 完成标记 / 中断检测 | `internal/agent/stream.go` | +| 会话文件存储 / 重启恢复 / 上下文筛选 | `internal/agent/conversations.go` | +| 自动摘要 / 增量记忆 / 原文校验与上下文长度预算 | `internal/agent/memory.go` | +| 对话附件保存 / 会话隔离 / 模型输入与删除清理 | `internal/agent/attachments.go` | +| PDF / DOCX / XLSX / 中文文本解析 | `internal/agent/attachment_extract.go` | +| 待发送附件 / 消息附件卡片 | `frontend/src/components/chat/AttachmentList.vue` | +| 会话列表 / 新建 / 重命名 / 删除确认 | `frontend/src/components/chat/ConversationList.vue` | +| 按会话管理流式生成 / 草稿 / 自动保存队列 | `frontend/src/lib/useConversations.ts` | +| 模型配置表单 | `frontend/src/components/settings/ModelSettings.vue` | +| Filesystem MCP 预设 / 临时文件读写测试 / 目录范围 | `internal/mcpclient/filesystem.go` | +| Filesystem MCP 固定版本及依赖锁 / 内置运行资源准备 | `runtime/filesystem/package.json` → `scripts/stage-filesystem-runtime.mjs` | | Go → Python HTTP 调用 | `internal/agent/client.go` | | JSON-RPC 版本与消息字段 | `internal/protocol/*.go` | | Python 模型与 method 实现 | `agent/main.py` | @@ -20,6 +37,8 @@ | 顶部设备栏 | `frontend/src/components/device/DeviceBar.vue` | | 设置页 | `frontend/src/components/settings/SettingsPane.vue` | | 日志页 | `frontend/src/components/logs/LogPane.vue` | +| 日志持久化 / 2 MiB 分卷 / 历史读取与清空 | `internal/applog/service.go` | +| 日志恢复 / 顺序保存 / 失败重试 | `frontend/src/lib/useLogs.ts` | | 全局配色 / 字体 | `frontend/src/styles/app.css` | 改了 Go 的导出方法后执行: diff --git a/README.md b/README.md index 53ef842..ebb0fa6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ frontend/ UI:左画面 | 右对话 | 设置 | 日志 main.go 桌面壳入口(窗口 + 注册服务) internal/device/ ADB 连接 / 视频预览 / 实时触摸控制(Go) -internal/agent/ 对接 Python Agent 的桥 +internal/agent/ 模型配置 / 系统凭据存储 / Go 直连模型 / 对话上下文 +internal/mcpclient/ MCP 服务管理 / 系统凭据存储 / stdio 与 HTTP 工具发现 internal/protocol/ 版本化 JSON-RPC 契约 agent/ Python Agent 进程(可后接云端模型) ``` @@ -17,19 +18,50 @@ agent/ Python Agent 进程(可后接云端模型) ## 开发 -需要 Go 1.26、Node.js 22.18+(测试直接运行 TypeScript)、npm 和 Wails v3。macOS 实时预览还需安装 `scrcpy 5.0.1` 与 `ffmpeg`(当前控制协议对应 scrcpy 5.0.1)。首次运行先安装前端依赖,并同步内置 ADB: +构建机需要 Go 1.26+、Node.js 22.18+(测试直接运行 TypeScript)、npm 和 Wails v3。**客户机无需安装 Go、Node、Python、ADB、scrcpy、FFmpeg 或 WebView2**。工作台对话由 Go 直接调用配置的模型服务,不启动 Python。 + +先安装前端依赖,再按构建平台准备运行资源: ```bash npm --prefix frontend install -./scripts/sync-platform-tools.sh ``` +Windows 10/11 x64(PowerShell,在项目根目录): + +```powershell +./scripts/sync-windows-runtime.ps1 +wails3 dev +# 正式构建:自动准备并复制依赖到 bin/ +wails3 build +# 免安装包:整个目录解压后运行 iTuoke.exe,不要单独拷贝 exe +wails3 task windows:package:portable +# 安装包(构建机需有 NSIS / makensis) +wails3 package +``` + +Windows 内置固定版本 scrcpy server 5.0.1、FFmpeg 8.1.2、ADB 和私有 WebView2 156.0.4314.58,下载缓存及二进制不提交 Git。ZIP/安装包都包含运行资源,无客户侧下载。当前 Windows 交付目标为 amd64;ARM64 和当前只复制主 exe 的 Wails MSIX 打包路径会明确拒绝,避免交付缺少资源的包。Windows 的后台工具不弹控制台窗口,macOS 专用网络权限按钮不会显示。 + +macOS(在 Mac 构建机上): + +```bash +# 构建机准备 ffmpeg;也可通过 ITUOKE_FFMPEG_SOURCE 指向已有二进制 +brew install ffmpeg +bash scripts/sync-macos-runtime.sh +wails3 dev +wails3 package +# 对外分发仍需自己的 Developer ID 签名和公证 +wails3 task darwin:sign:notarize +``` + +macOS 准备脚本下载 Mac 版 ADB 和相同的 scrcpy server,收集 FFmpeg 的非系统 dylib 依赖并重写为包内相对路径,不依赖客户的 Homebrew。打包和开发 `.app` 均将工具放进 `Contents/Resources/{platform-tools,scrcpy,ffmpeg}` 后再签名。缺资源、错平台或缺目标架构会直接失败。Apple Silicon 和 Intel 分别构建相应版本;universal 包要求 FFmpeg 和所有 dylib 同时包含 arm64、x86_64,不能使用单架构 Homebrew FFmpeg 冒充 universal。 + +两个平台均优先按主程序位置查找资源,因此支持从快捷方式或不同工作目录启动。准备步骤只在构建机执行;离线重建可使用已经准备好的资源,Windows 通过 `-StageOnly` 校验和复制。升级依赖后需要重新验证投屏协议、签名以及第三方许可和对应源码分发要求,参见 `third_party/RUNTIME-NOTICES.txt`。 ```bash # 1) 生成前端绑定(改 Go 导出方法后重跑) wails3 generate bindings -ts -i ./... -# 2) 跑桌面应用(热重载) +# 2) 跑桌面应用(热重载,先完成对应平台运行资源准备) wails3 dev # 3) 可选:单独起 Python Agent @@ -38,14 +70,14 @@ cd agent && python3 main.py macOS 开发运行会把应用包同步到 `~/Applications/iTuoke.app` 后启动,避免从外置磁盘运行时反复触发「可移除宗卷」文件授权。ADB 使用应用专用的本地后台服务,不复用其他工具的默认服务;配对记录继续保留。 -## 内置 ADB(客户无需安装) +## 内置运行资源(客户无需安装) ```bash -./scripts/sync-platform-tools.sh # 同步 platform-tools 到 third_party/ -wails3 build # macOS 打包时自动拷进 .app/Contents/Resources/ +bash scripts/sync-macos-runtime.sh # Mac 构建机:完整投屏运行资源 +wails3 package # macOS 打进 .app/Contents/Resources/ ``` -运行时优先使用应用内置 `adb`,不依赖客户电脑是否安装过 Android SDK。 +Windows 使用上面的 PowerShell 脚本和 Windows 打包任务;不要在 Windows 上运行旧的 `sync-platform-tools.sh`。`scripts/sync-platform-tools.sh` 仅作为历史 ADB 同步工具保留,不能代替完整运行资源准备。 ## 当前能力 @@ -53,7 +85,24 @@ wails3 build # macOS 打包时自动拷进 .app/Contents/R - scrcpy 视频预览,支持适应窗口及 50%–200% 缩放。 - 预览中点按、长按及连续拖动:按下、移动、松开实时发送到 scrcpy 控制通道。 - 深色、浅色和跟随系统主题,连接配置与缩放偏好持久化。 -- 设置与日志页面;Go/Python Agent 对话目前仍为 stub,尚未接入模型。 +- 设置与日志页面;模型配置支持 OpenAI、DeepSeek、通义千问、已有 Ollama 及自定义 Chat Completions 兼容服务。 +- 运行日志自动保存到系统用户配置目录的 `iTuoke/logs/`,Windows 与 macOS 均支持。单文件最多 2 MiB,写入下一条会超出时按时间另建历史分卷,旧文件保留。重启后恢复最近 2000 条供界面查看;清空会删除当前和历史日志文件,保存失败可重试。 +- API 基础地址、模型名称、系统提示词、可选 Temperature、输出上限、上下文轮数与请求超时可配置。获取模型列表及连接测试使用当前表单,保存后用于工作台真实对话。 +- API Key 由 Go 写入 Windows 凭据管理器 / macOS 钥匙串,按服务地址隔离;普通配置保存到系统用户配置目录下的 `iTuoke/model.json`,不包含密钥。 +- 对话支持新建、切换、搜索、重命名和删除会话。消息及输入草稿自动保存到用户配置目录的 `iTuoke/conversations/`,每个会话一个 JSON 文件,使用原子替换写入。保存失败时提示并可重试;重启后恢复上次会话。 +- 对话顶栏在同一行显示会话列表图标、会话名称、模型名称和新会话图标。会话管理从窗口左侧抽屉展开,支持遮罩、关闭按钮和 Esc 收起;界面图标统一使用 Lucide Vue 线性图标。 +- 各会话拥有独立模型上下文,可并行生成,切换会话不会串消息;重启后从已保存的摘要与近期问答恢复上下文。中断回复仍可查看,重启时标为已停止,不作为完整问答直接发送;摘要只按实际状态记录结果。更换模型配置不会删除会话记录。 +- 会话支持自动摘要记忆:历史问答超出设置的近期轮数或正文长度预算时,使用当前模型把较早内容增量整理为摘要,后续发送“摘要 + 近期问答 + 当前问题”。摘要包含需求、限制、决定、关键参数、工具结果及待办,原始消息保留。摘要与覆盖范围按会话保存到同一 JSON 文件,重启后恢复;前端旧快照不能覆盖后端新记忆,历史原文被修改时会失效并重新整理。记忆及整理过程在后台处理,不在页面展示;停止生成仍可取消整理。摘要失败或取消不会丢弃原记录,也不会推进覆盖范围。摘要会额外调用当前模型,上下文轮数为 0 时不使用或生成摘要。 +- 对话支持文件选择、拖放和粘贴图片。附件可在发送前移除,也可仅发送附件;待发送附件随草稿保存,已发送附件显示在消息中,按会话隔离,重启后可继续引用。原文件与提取正文保存在用户配置目录的 `iTuoke/attachments/{会话ID}/{附件ID}/`,删除会话时同步清理。文件名只作为元数据,文件路径由程序生成,客户端无需安装额外解析工具。 +- 支持 UTF-8、带 BOM 的 UTF-16、GB18030 文本与代码文件,以及文字型 PDF、DOCX、XLSX 和 PNG/JPEG/WebP。PDF 不做扫描件 OCR;Excel 使用单元格缓存值,不执行公式。每条最多 5 个附件,单个最多 20 MB、合计最多 30 MB;单文件提取正文最多 128 KB,超出会标记“部分正文”,每条附件正文合计最多 256 KB。文本内容用于对话和自动摘要,图片作为当前模型的图片输入,模型需支持识图。 +- Agent 支持 Chat Completions 工具调用:将已启用 MCP 的工具提供给模型,执行后回传结果并继续流式回复;支持手机操作及文件工具。投屏本身不上传模型;截图等工具返回的图片会作为当前轮图片输入发送给所配置模型(需要支持图片),同时保存到本机会话附件,记录中只存引用。图片卡片直接显示在聊天中,可点击放大,切换会话和重启后恢复。Ollama 选项连接已有服务,不随应用打包本地推理引擎或模型权重。 +- “截图给我看”等单纯截图请求使用本地快捷路径:只识别唯一在线手机并调用截图工具,跳过模型规划、历史摘要、页面与目录查询,不再另存重试。包含导航、分析、指定保存路径等额外要求时仍由模型执行。普通任务优先使用已确定的设备和批量顺序动作,减少逐步播报和重复核验。Qwen3.8-Max 工具任务默认设置 `enable_thinking=false`,可在模型高级设置中开启“工具执行使用深度思考”。 +- 「设置 → MCP 管理」支持添加、编辑、启停和删除多个 MCP 服务。支持本地 stdio 进程和远程 Streamable HTTP,连接测试真实发现工具(含分页),可展开查看工具说明和输入参数。测试使用当前表单,不自动保存或执行工具,完成后关闭连接。Agent 每次对话读取启用服务,保留连接直至本轮结束;停用、删除或修改目标会阻止旧会话继续调用该服务。 +- 工具名称按服务隔离,调用前校验 JSON Schema,执行结果(含错误)回传模型。工具进度、参数和结果摘要显示在聊天中并随会话保存;停止生成会取消请求和后续调用。每次任务最多 16 轮、40 次调用、10 分钟;同一应用中的 MCP 操作序列按任务排队,避免多个会话交错操作同一手机,纯文字对话仍可并行。 +- MCP 普通配置保存在系统用户配置目录下的 `iTuoke/mcp.json`;环境变量与认证请求头保存到系统凭据库,不返回前端。编辑时可保留、替换或清除凭据,修改连接目标须重新填写或清除凭据。本地服务的 Node/Python 等运行环境需自行准备;命令与 JSON 启动参数分开填写,不经 shell 执行。远程服务目前支持自定义认证请求头,不包含 OAuth 登录流程或旧版 HTTP+SSE 传输。 +- 「添加 Filesystem」使用官方 `@modelcontextprotocol/server-filesystem` 2.1.0,默认只开放系统用户配置目录的 `iTuoke/files/`。编辑中可指定多个已有目录;配置不保存随安装位置变化的启动路径。普通连接测试只发现工具,「测试文件读写」会实际创建临时目录、写入及读取中文文件、搜索、重命名并验证目录外读写被拒绝,随后清理测试文件。启用后可由对话 Agent 调用。 +- Filesystem 的 Node 与锁定依赖随 Windows/macOS 包交付,客户端无需安装或联网下载。构建机需 Node >= 24,首次执行 `npm --prefix runtime/filesystem run stage`;Windows/macOS 运行资源准备脚本也会执行该步骤。`runtime/filesystem/package-lock.json` 固定依赖,目录搜索依赖覆盖为修复版本。Mac 使用官方自包含 Node,检查目标架构及系统 dylib;universal 构建需通过 `ITUOKE_NODE_UNIVERSAL_SOURCE` 提供同时含 arm64/x86_64 的 Node,并用 `ITUOKE_NODE_LICENSE` 指定其许可证(若不在当前 Node 相邻目录)。 +- 工作台使用 SSE 流式输出并逐段显示,支持停止生成。停止或连接中断后保留已收到的文字并显示状态;未完成的回答不加入模型上下文。达到输出上限时显示不完整提示。模型服务需支持 Chat Completions 的 `stream=true`。 `frontend/bindings/`、依赖目录、构建产物、录屏和本机配置不提交;前端绑定由上述生成命令或 Wails 构建任务重建。图标、原始图标素材及各平台打包模板保留在仓库。 diff --git a/agent/README.md b/agent/README.md index b617ad7..53ba634 100644 --- a/agent/README.md +++ b/agent/README.md @@ -1,5 +1,8 @@ # Python Agent(独立进程) +此目录目前为独立实验骨架。桌面应用的模型配置和真实对话已经由 +`internal/agent/` 中的 Go 服务实现,不需要启动本 Python 进程。 + > **[关键]** 模型理解、下一步规划、工具调用、审计日志都放这里。 > Go 通过 `internal/agent/client.go` 以 JSON-RPC(`POST /rpc`)调用本进程。 diff --git a/build/darwin/Taskfile.yml b/build/darwin/Taskfile.yml index 49e3c4c..e3b22ba 100644 --- a/build/darwin/Taskfile.yml +++ b/build/darwin/Taskfile.yml @@ -127,6 +127,8 @@ tasks: - task: build:universal cmds: - task: create:app:bundle + vars: + BUNDLE_ARCH: universal package:dmg: summary: Packages the application into a styled `.dmg` @@ -167,15 +169,8 @@ tasks: - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" # 故意不拷贝旧 Assets.car,避免覆盖 icons.icns 图标 - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" - # [内置 adb] 打进 Resources,客户无需安装 platform-tools - - | - if [ -x third_party/platform-tools/adb ]; then - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources/platform-tools" - rsync -a third_party/platform-tools/ "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources/platform-tools/" - echo "bundled platform-tools into app Resources" - else - echo "WARNING: third_party/platform-tools/adb missing — run ./scripts/sync-platform-tools.sh" - fi + # All runtime tools live in Resources and are verified before signing. + - bash scripts/stage-macos-runtime.sh "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" "{{.BUNDLE_ARCH | default .ARCH | default ARCH}}" - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' @@ -202,11 +197,7 @@ tasks: - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources/icons.icns" - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" - - | - if [ -x third_party/platform-tools/adb ]; then - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources/platform-tools" - rsync -a third_party/platform-tools/ "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources/platform-tools/" - fi + - bash scripts/stage-macos-runtime.sh "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" "{{.ARCH | default ARCH}}" - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Info.plist" - xattr -cr "{{.BIN_DIR}}/{{.APP_NAME}}.app" - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml index c2f0b5a..74c2722 100644 --- a/build/windows/Taskfile.yml +++ b/build/windows/Taskfile.yml @@ -34,6 +34,7 @@ tasks: summary: Builds the application using native Go cross-compilation internal: true deps: + - task: prepare:runtime - task: common:go:mod:tidy - task: common:build:frontend vars: @@ -52,6 +53,7 @@ tasks: vars: ARCH: '{{.ARCH}}' - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"' + - powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sync-windows-runtime.ps1 -StageOnly -Arch {{.ARCH | default ARCH}} -Destination "{{.BIN_DIR}}" - cmd: powershell Remove-item *.syso platforms: [windows] - cmd: rm -f *.syso @@ -73,6 +75,8 @@ tasks: ref: .OBFUSCATED - task: common:generate:icons preconditions: + - sh: 'false' + msg: 'Self-contained Windows packages must be built on Windows with scripts/sync-windows-runtime.ps1.' - sh: docker info > /dev/null 2>&1 msg: "Docker is required for CGO cross-compilation. Please install Docker." - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 @@ -104,6 +108,19 @@ tasks: FORMAT: '{{.FORMAT | default "nsis"}}' INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' + prepare:runtime: + summary: Download and verify private Windows runtime on the build machine + run: once + cmds: + - powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sync-windows-runtime.ps1 -Arch {{.ARCH | default ARCH}} + + package:portable: + summary: Creates a self-contained Windows ZIP (extract the whole directory) + deps: + - task: build + cmds: + - powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-windows-portable.ps1 -BinDir "{{.BIN_DIR}}" -AppName "{{.APP_NAME}}" + generate:syso: summary: Generates Windows `.syso` file dir: build @@ -121,8 +138,6 @@ tasks: - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]' msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'" cmds: - # Create the Microsoft WebView2 bootstrapper if it doesn't exist - - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" - | {{if eq OS "windows"}} makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi @@ -139,6 +154,7 @@ tasks: deps: - task: build cmds: + - powershell -NoProfile -Command "throw 'MSIX runtime resource staging is not supported. Use Windows NSIS or package:portable for self-contained delivery.'" - |- wails3 tool msix \ --config "{{.ROOT_DIR}}/build/config.yml" \ diff --git a/build/windows/nsis/project.nsi b/build/windows/nsis/project.nsi index 38a5164..303b364 100644 --- a/build/windows/nsis/project.nsi +++ b/build/windows/nsis/project.nsi @@ -33,6 +33,7 @@ Unicode true #### ## Include the wails tools #### +!define PRODUCT_EXECUTABLE "iTuoke.exe" !include "wails_tools.nsh" # The version information for this two must consist of 4 parts @@ -87,12 +88,30 @@ FunctionEnd Section !insertmacro wails.setShellContext - !insertmacro wails.webview2runtime - SetOutPath $INSTDIR !insertmacro wails.files + ; Runtime files are installed privately; never download or install client dependencies. + SetOutPath "$INSTDIR\platform-tools" + File /r "..\..\..\bin\platform-tools\*" + SetOutPath "$INSTDIR\scrcpy" + File /r "..\..\..\bin\scrcpy\*" + SetOutPath "$INSTDIR\ffmpeg" + File /r "..\..\..\bin\ffmpeg\*" + SetOutPath "$INSTDIR\webview2" + File /r "..\..\..\bin\webview2\*" + SetOutPath "$INSTDIR\mcp-filesystem" + File /r "..\..\..\bin\mcp-filesystem\*" + SetOutPath $INSTDIR + File "..\..\..\bin\runtime-manifest.json" + File "..\..\..\bin\RUNTIME-NOTICES.txt" + ; Required for Fixed Version >= 120 on Windows 10 (AppContainer renderer). + nsExec::ExecToLog '"$SYSDIR\icacls.exe" "$INSTDIR\webview2" /grant "*S-1-15-2-2:(OI)(CI)(RX)"' + Pop $0 + nsExec::ExecToLog '"$SYSDIR\icacls.exe" "$INSTDIR\webview2" /grant "*S-1-15-2-1:(OI)(CI)(RX)"' + Pop $0 + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d358cc..55f0eb8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@lucide/vue": "1.68.0", "@wailsio/runtime": "3.0.0", + "markdown-it": "15.0.3", "vue": "^3.2.45" }, "devDependencies": { @@ -70,6 +72,15 @@ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, + "node_modules/@lucide/vue": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.68.0.tgz", + "integrity": "sha512-ywFT0JLQC89yjvOD2XXwwT0s+T9ydharRZBgFEIAbk6YyvrNv5MWjkRfzW+hlcxWJJujyzXqofeLjyJC4au4tQ==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/@oxc-project/types": { "version": "0.156.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", @@ -563,6 +574,22 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -903,6 +930,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -912,6 +958,51 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -991,6 +1082,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rolldown": { "version": "1.2.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", @@ -1065,6 +1165,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "license": "MIT" + }, "node_modules/vite": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3c4bfd7..f8e355b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@lucide/vue": "1.68.0", "@wailsio/runtime": "3.0.0", + "markdown-it": "15.0.3", "vue": "^3.2.45" }, "devDependencies": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d6d29bc..5a4ee63 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,6 +4,8 @@ * 顶栏菜单 + 可拖拽左右分栏;连接配置在「设置」并持久化 */ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import { useConversations } from './lib/useConversations' +import { useLogs } from './lib/useLogs' import { TouchQueue, type TouchEvent } from './lib/touch' import DeviceBar from './components/device/DeviceBar.vue' import CapturePane from './components/screen/CapturePane.vue' @@ -13,7 +15,6 @@ import SettingsPane from './components/settings/SettingsPane.vue' import type { ActionResult, AdbInfo, - ChatMessage, DeviceInfo, MainTab, } from './types' @@ -41,20 +42,16 @@ const connectBusy = ref(false) const connectionNotice = ref('') const connectionRequest = ref(0) const live = ref(false) +const manualControl = ref(false) const streamUrl = ref('') const controlError = ref('') const frameError = ref('') const adbInfo = ref(null) -const messages = ref([ - { - id: 'welcome', - role: 'system', - content: '在「设置」里完成配对与连接。连接参数会自动记住。工作台可拖拽中间分隔条调整左右宽度。', - createdAt: Date.now(), - }, -]) -const sending = ref(false) -const logs = ref([`[${ts()}] app boot`]) +const logStore = useLogs() +const { lines: logs, log } = logStore +log('app boot') +const conversations = useConversations(log) +const { messages, sending } = conversations const protocolVersion = ref('') const agentStatus = ref | null>(null) @@ -62,14 +59,6 @@ const workspaceRef = ref(null) let dragging = false let liveTimer: ReturnType | null = null -function ts() { - return new Date().toLocaleTimeString() -} - -function log(line: string) { - logs.value = [...logs.value.slice(-400), `[${ts()}] ${line}`] -} - const activeDevice = computed( () => devices.value.find((d) => d.id === activeId.value) ?? null, ) @@ -82,7 +71,7 @@ const adbHint = computed(() => { }) const splitStyle = computed(() => ({ - gridTemplateColumns: `${splitRatio.value}fr 6px ${1 - splitRatio.value}fr`, + gridTemplateColumns: `${splitRatio.value}fr 14px ${1 - splitRatio.value}fr`, })) function setTab(t: MainTab) { @@ -159,16 +148,38 @@ async function selectDevice(id: string) { } } -const touchQueue = new TouchQueue((event) => DeviceService.Touch(event), (error) => { +const touchQueue = new TouchQueue((event) => { + if (event.action !== 3 && (!manualControl.value || !live.value || activeId.value !== event.deviceId)) { + return Promise.resolve() + } + return DeviceService.Touch(event) +}, (error) => { controlError.value = `触摸控制失败:${String(error)}` log(`input touch: ${String(error)}`) }) function onPhoneTouch(event: TouchEvent) { - if (event.action !== 3 && (!live.value || activeId.value !== event.deviceId)) return + if (event.action !== 3 && (!manualControl.value || !live.value || activeId.value !== event.deviceId)) return if (event.action === 0) controlError.value = '' touchQueue.push(event) } -watch([activeId, live], () => { controlError.value = '' }) +watch([activeId, live, tab], () => { + manualControl.value = false + controlError.value = '' +}, { flush: 'sync' }) + +let screenPowerUpdate: Promise = Promise.resolve() +watch(() => prefs.value.screenOff, (screenOff) => { + if (!live.value || !activeId.value) return + const deviceId = activeId.value + screenPowerUpdate = screenPowerUpdate.then(async () => { + if (!live.value || activeId.value !== deviceId) return + await DeviceService.SetScreenOff(deviceId, screenOff) + log(screenOff ? '手机已切换为息屏预览' : '手机已恢复亮屏') + }).catch((error) => { + connectionNotice.value = `调整手机屏幕失败,设置已保存,下次连接会重试:${String(error)}` + log(connectionNotice.value) + }) +}) async function startStream() { if (!activeDevice.value?.online) { @@ -176,9 +187,10 @@ async function startStream() { return } previewStarting.value = true + manualControl.value = false frameError.value = '' try { - const res = (await DeviceService.StartPreviewStream(activeId.value)) as ActionResult + const res = (await DeviceService.StartPreviewStream(activeId.value, prefs.value.screenOff)) as ActionResult if (!res.ok) { live.value = false streamUrl.value = '' @@ -242,7 +254,7 @@ async function onPair(hostPort: string, code: string) { const res = (await DeviceService.PairWireless({ hostPort, code })) as ActionResult log(`pair: ${res.message}`) messages.value.push({ - id: `sys-${Date.now()}`, + id: crypto.randomUUID(), role: 'system', content: res.ok ? `配对成功:${res.message}` : `配对失败:${res.message}`, createdAt: Date.now(), @@ -273,7 +285,7 @@ async function onConnect(hostPort: string) { const res = (await DeviceService.ConnectWireless({ hostPort })) as ActionResult log(`connect: ${res.message}`) messages.value.push({ - id: `sys-${Date.now()}`, + id: crypto.randomUUID(), role: 'system', content: res.ok ? `连接结果:${res.message}` : `连接失败:${res.message}`, createdAt: Date.now(), @@ -326,36 +338,6 @@ async function onDisconnect(id: string) { } } -async function sendChat(text: string) { - messages.value.push({ - id: `u-${Date.now()}`, - role: 'user', - content: text, - createdAt: Date.now(), - }) - sending.value = true - try { - const res = (await AgentService.Chat(activeId.value || 'default', text)) as { - content?: string - } - messages.value.push({ - id: `a-${Date.now()}`, - role: 'assistant', - content: res?.content ?? JSON.stringify(res), - createdAt: Date.now(), - }) - } catch (e) { - messages.value.push({ - id: `e-${Date.now()}`, - role: 'system', - content: `发送失败:${String(e)}`, - createdAt: Date.now(), - }) - } finally { - sending.value = false - } -} - async function refreshAgentMeta() { try { protocolVersion.value = await AgentService.ProtocolVersion() @@ -366,17 +348,17 @@ async function refreshAgentMeta() { } onMounted(async () => { + await logStore.load() + await conversations.load() await refreshAdb() await refreshDevices() await refreshAgentMeta() // 若已保存连接地址且当前无设备,可提示去设置;不自动连以免打扰 if (prefs.value.connectHost && !devices.value.length) { - messages.value.push({ - id: `tip-${Date.now()}`, - role: 'system', - content: `已记住连接地址 ${prefs.value.connectHost},可在「设置」一键连接。`, - createdAt: Date.now(), - }) + const content = `已记住连接地址 ${prefs.value.connectHost},可在「设置」一键连接。` + if (!messages.value.some(message => message.role === 'system' && message.content === content)) { + messages.value.push({ id: crypto.randomUUID(), role: 'system', content, createdAt: Date.now() }) + } } }) @@ -410,32 +392,41 @@ onUnmounted(() => { :stream-url="streamUrl" :loading="previewStarting" :live="live" + :manual-control="manualControl" :error="frameError" :control-error="controlError" @touch="onPhoneTouch" @toggle-live="toggleLive" + @toggle-control="manualControl = !manualControl" />
- +
{
- +
@@ -457,6 +448,7 @@ onUnmounted(() => { } .workspace { + margin: 16px; flex: 1; min-height: 0; display: grid; @@ -467,7 +459,7 @@ onUnmounted(() => { .splitter { cursor: col-resize; - background: var(--border); + background: transparent; position: relative; z-index: 2; } @@ -478,13 +470,20 @@ onUnmounted(() => { inset: 0 -3px; } +.splitter::before { content: ''; position: absolute; top: 50%; left: 50%; width: 3px; height: 40px; border-radius: 3px; background: var(--border); transform: translate(-50%, -50%); } +.splitter:hover::before, .splitter:active::before { background: var(--accent); } + .splitter:hover, .splitter:active { - background: var(--accent); + background: var(--accent-soft); } .page { flex: 1; min-height: 0; } +@media (max-width: 760px) { + .workspace { margin: 10px; grid-template-columns: minmax(0, 1fr) !important; grid-template-rows: minmax(300px, .9fr) minmax(380px, 1.1fr); gap: 12px; overflow: auto; } + .splitter { display: none; } +} diff --git a/frontend/src/components/UiIcon.vue b/frontend/src/components/UiIcon.vue new file mode 100644 index 0000000..6d10199 --- /dev/null +++ b/frontend/src/components/UiIcon.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/frontend/src/components/chat/AttachmentList.vue b/frontend/src/components/chat/AttachmentList.vue new file mode 100644 index 0000000..3a49cff --- /dev/null +++ b/frontend/src/components/chat/AttachmentList.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/components/chat/ChatPane.vue b/frontend/src/components/chat/ChatPane.vue index 523367b..44d1aa7 100644 --- a/frontend/src/components/chat/ChatPane.vue +++ b/frontend/src/components/chat/ChatPane.vue @@ -1,28 +1,180 @@