diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 6d0e719..960104e 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -2,12 +2,17 @@ ## [Unreleased] +- **[frontend]**: 修复持续日志输出时切换终端后的 viewport 恢复偏移问题,改为按距底部偏移恢复滚动位置,避免重新激活后无法继续向下滚到最底部 — by yinjianm + - 方案: [202604120705_terminal-scroll-viewport-restore-fix](archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/) + - 2026-03-25:初始化 `.helloagents/` 知识库骨架与首批模块文档,不代表源码功能变更。 - 2026-03-25:新增 GHCR Docker 发布 workflow,并将 `docker-compose.yml` 的三个业务镜像切换到 `ghcr.io/micah123321/*`。 - 2026-03-25:`/workspace` 默认布局改为“左侧 Workbench + 中央视终端 + 右侧状态监控”,并在状态监控中新增开机累计上下行流量展示。 - 2026-03-25:继续微调 `/workspace` Workbench,新增默认“快捷指令”标签、调整三栏宽度到更接近 xterminal 参考图,并修复终端区域鼠标悬停时指针异常消失的问题。 ### 修复 +- **[frontend]**: 为 SSH 服务器组头补充整组关闭按钮,并修正脚本模式对单/双引号包裹值的保存行为 — by yinjianm + - 方案: [202604120656_ssh-group-close-and-script-input-sanitize](archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/) - **[frontend]**: 将 `/workspace` Workbench 的导航从左侧竖排 icon rail 调整为 `Workbench` header 上方的横向纯图标栏,保留原有四面板切换逻辑与信息头部层级 — by yinjianm - 方案: [202603300206_workspace-workbench-top-tabs](archive/2026-03/202603300206_workspace-workbench-top-tabs/) - **[frontend]**: 将 `/workspace` 的 SSH 多终端展示从顶部组头胶囊改为“顶部只切服务器、终端面板内部切换同服务器多个终端”,修正服务器与终端的视觉层级 - by yinjianm @@ -38,6 +43,9 @@ - 方案: [202603250614_terminal-ansi-color-effects](archive/2026-03/202603250614_terminal-ansi-color-effects/) ### 快速修改 +- **[frontend]**: 修复右侧状态监控在窄侧栏下的内存/磁盘卡片字体重叠问题,改为基于卡片容器宽度自适应折列与缩字 — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/frontend/src/components/StatusMonitor.vue:446-452,572-600,697-707,744-802 - **[frontend]**: 将“添加新连接”弹窗的脚本模式开关上移到基本信息之前,并在脚本导入时自动忽略空格、空行与 Markdown 代码围栏行 — by yinjianm - 类型: 快速修改(无方案包) - 文件: packages/frontend/src/components/AddConnectionForm.vue, packages/frontend/src/composables/useAddConnectionForm.ts @@ -64,6 +72,10 @@ - 文件: packages/frontend/src/components/AddEditQuickCommandForm.vue:9,184-185,242-245 ### 新增 +- **[frontend]**: 在 `/workspace` 状态监控的 CPU 型号下方新增 CPU 核心数 badge,直接显示后端推送的服务器核数规格 — by yinjianm + - 方案: [202604120656_server-status-cpu-core-display](archive/2026-04/202604120656_server-status-cpu-core-display/) +- **[backend]**: 扩展 `StatusMonitorService` 的 CPU 规格采集链路,新增 `cpuCores` 字段并通过多级回退命令获取逻辑核心数 — by yinjianm + - 方案: [202604120656_server-status-cpu-core-display](archive/2026-04/202604120656_server-status-cpu-core-display/) - **[frontend]**: 为已登录页面新增 `Ctrl+Shift+F` 全局服务器快捷检索面板,支持模糊搜索并直接复用既有 SSH / RDP / VNC 连接链路 — by yinjianm - 方案: [202603300204_global-server-quick-search](archive/2026-03/202603300204_global-server-quick-search/) - **[frontend]**: 为文件管理器补齐“上传文件夹”入口,选择目录后会先在浏览器端打包为 zip,再上传并自动触发远端解压 — by yinjianm diff --git a/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/.status.json b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/.status.json new file mode 100644 index 0000000..4706d18 --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"done":4,"percent":100,"current":"Completed - server status CPU core display implemented and verified","updated_at":"2026-04-12 07:01:00"} diff --git a/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/proposal.md b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/proposal.md new file mode 100644 index 0000000..74020f8 --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/proposal.md @@ -0,0 +1,135 @@ +# 变更提案: server-status-cpu-core-display + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P2 +状态: 执行中 +创建: 2026-04-12 +``` + +--- + +## 1. 需求 + +### 背景 +当前工作区右侧状态监控已经展示 CPU 型号、CPU 使用率、内存、磁盘和网络信息,但无法直观看到服务器 CPU 是几核。用户希望在当前页面直接判断机器规格,无需再登录服务器执行 `nproc` 或 `lscpu`。 + +### 目标 +- 在服务器状态数据中补充 CPU 核心数字段。 +- 在 `StatusMonitor.vue` 中把 CPU 核心数放到 CPU 型号下方,直接显示为类似 `16 核` 的次级信息。 +- 保持现有状态监控布局和 WebSocket 链路不变,只做增量增强。 + +### 约束条件 +```yaml +时间约束: 当前轮次内完成实现与构建验证 +性能约束: 不额外引入持续高频采集逻辑,仅复用现有状态轮询时机 +兼容性约束: 状态数据新增字段必须保持向后兼容,字段缺失时前端优雅降级为 N/A +业务约束: 保持状态监控现有暗色仪表风格,不重构已有卡片和图表布局 +``` + +### 验收标准 +- [ ] 后端状态采集结果包含 `cpuCores`,能在 Debian 12 等常见 Linux 环境下稳定读取逻辑核心数。 +- [ ] 前端状态监控页在 CPU 型号下方展示 CPU 核心数,字段缺失时显示 `N/A`。 +- [ ] `packages/frontend` 与 `packages/backend` 构建通过。 + +--- + +## 2. 方案 + +### 技术方案 +后端在 `StatusMonitorService` 中复用现有 SSH 执行链路采集 CPU 核心数,优先使用 `nproc`,失败时回退到 `getconf _NPROCESSORS_ONLN` 与 `lscpu` 解析,最终将结果写入新增的 `cpuCores` 字段。前端扩展 `ServerStatus` 类型和状态监控多语言文案,在 `StatusMonitor.vue` 的 CPU 型号行内改为“主值 + 次级 badge”布局,使 CPU 核心数直接显示在 CPU 型号下方,并保持窄屏下可自然换行。 + +### 影响范围 +```yaml +涉及模块: + - backend: 扩展状态采集结果,新增 CPU 核心数采集逻辑 + - frontend: 扩展状态类型、状态监控文案和 CPU 信息展示 + - knowledge-base: 同步前后端模块说明与变更记录 +预计变更文件: 9 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 部分精简系统缺少 `nproc` 或 `lscpu` | 低 | 采用多级回退命令,无法获取时返回 `undefined` 由前端降级 | +| CPU 型号文案较长导致布局拥挤 | 低 | 维持现有 `truncate` 主行,核心数改为独立次级 badge 并允许换行 | + +--- + +## 3. 技术设计(可选) + +> 涉及架构变更、API设计、数据模型变更时填写 + +### 架构设计 +```mermaid +flowchart TD + A[SSH 状态采集] --> B[StatusMonitorService] + B --> C[status_update WebSocket payload] + C --> D[useStatusMonitor] + D --> E[StatusMonitor.vue] +``` + +### API设计 +#### WebSocket `status_update` +- **请求**: 由前端现有状态订阅链路触发,无新增请求参数 +- **响应**: `payload.status` 新增可选字段 `cpuCores?: number` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `cpuCores` | `number` | 服务器可用的逻辑 CPU 核心数,采集失败时缺省 | + +--- + +## 4. 核心场景 + +> 执行完成后同步到对应模块文档 + +### 场景: 工作区状态监控查看 CPU 规格 +**模块**: frontend / backend +**条件**: 用户已在工作区打开活动 SSH 会话,右侧状态监控正常接收服务器状态。 +**行为**: 后端通过现有 SSH 采集链路读取 CPU 核心数并随 `status_update` 推送;前端在 CPU 型号行下方显示如 `16 核` 的次级信息。 +**结果**: 用户无需登录服务器执行命令,即可在状态监控页直观看到当前服务器 CPU 核数。 + +--- + +## 5. 技术决策 + +> 本方案涉及的技术决策,归档后成为决策的唯一完整记录 + +### server-status-cpu-core-display#D001: 复用现有状态流增量展示 CPU 核心数 +**日期**: 2026-04-12 +**状态**: ✅采纳 +**背景**: 用户只要求在现有状态监控页直观看到 CPU 是几核,不需要新页面或独立接口,但现有状态采集结果没有此字段。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 在现有 `status_update` 中新增 `cpuCores` 并在 CPU 型号下方展示 | 改动链路短、向后兼容、最符合用户“直观看到”的诉求 | 需要同时修改前后端和多语言文案 | +| B: 新增独立接口或额外系统信息区展示 | 字段职责更分离 | 实现更重,增加状态来源和 UI 冗余 | +**决策**: 选择方案 A +**理由**: 当前状态监控已经承载 CPU 型号和使用率,把 CPU 核心数作为同一组信息的次级字段展示最直接,也不会引入新的请求或页面结构。 +**影响**: 影响 `packages/backend/src/services/status-monitor.service.ts`、`packages/frontend/src/types/server.types.ts`、`packages/frontend/src/components/StatusMonitor.vue` 及对应 locale 文案。 + +--- + +## 6. 成果设计 + +> 含视觉产出的任务由 DESIGN Phase2 填充。非视觉任务整节标注"N/A"。 + +### 设计方向 +- **美学基调**: 延续当前状态监控面板的暗色运行态仪表风格,只做密度增强,不改变现有层级和卡片语言。 +- **记忆点**: CPU 型号下方增加一个小尺寸规格 badge,让“型号 + 核数”形成一组可快速扫读的硬件信息。 +- **参考**: 参考当前 `StatusMonitor.vue` 的内存/磁盘卡片视觉语言和 CPU 型号文本层级。 + +### 视觉要素 +- **配色**: 延续现有状态行文本配色,badge 使用与监控卡片一致的低饱和边框和深色底。 +- **字体**: 继承当前页面现有字体体系,核心数值使用现有中号半粗文本层级,不引入新的字体依赖。 +- **布局**: CPU 型号主值保持单行截断,核心数作为其下方独立次级行展示,窄屏时允许自然折行。 +- **动效**: 无新增独立动效,保持现有状态监控的稳定刷新体验。 +- **氛围**: 保持现有暗色面板和轻微描边质感,不增加额外装饰性元素。 + +### 技术约束 +- **可访问性**: 核心数字段缺失时必须显示 `N/A`,避免空白状态;文本对比度沿用当前状态面板规范。 +- **响应式**: 在现有 `StatusMonitor.vue` 响应式规则下工作,不新增独立断点。 diff --git a/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/tasks.md b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/tasks.md new file mode 100644 index 0000000..7374a59 --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_server-status-cpu-core-display/tasks.md @@ -0,0 +1,52 @@ +# 任务清单: server-status-cpu-core-display + +> **@status:** completed | 2026-04-12 07:02 + +```yaml +@feature: server-status-cpu-core-display +@created: 2026-04-12 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 4 | 0 | 0 | 4 | + +--- + +## 任务列表 + +### 1. 后端状态采集扩展 + +- [√] 1.1 在 `packages/backend/src/services/status-monitor.service.ts` 中新增 `cpuCores` 字段采集与回退逻辑 | depends_on: [] + +### 2. 前端状态展示扩展 + +- [√] 2.1 在 `packages/frontend/src/types/server.types.ts` 与 locale 文件中补充 CPU 核心数字段和展示文案 | depends_on: [1.1] +- [√] 2.2 在 `packages/frontend/src/components/StatusMonitor.vue` 中将 CPU 核心数展示到 CPU 型号下方并处理缺省值 | depends_on: [2.1] + +### 3. 验证与知识库同步 + +- [√] 3.1 运行前后端构建验证,并同步 `.helloagents` 中的模块文档与变更记录 | depends_on: [2.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-12 06:56 | 方案设计 | 完成 | 确认采用“后端新增 `cpuCores` + 前端 CPU 型号下方展示”的增量方案 | +| 2026-04-12 06:59 | 1.1 | 完成 | 后端新增 `cpuCores` 字段,并实现 `nproc/getconf/procfs/lscpu` 多级回退采集 | +| 2026-04-12 07:00 | 2.1 / 2.2 | 完成 | 前端类型、locale 与状态监控 UI 已补齐 CPU 核数展示 | +| 2026-04-12 07:01 | 3.1 | 完成 | `packages/backend` 与 `packages/frontend` 构建通过,知识库与 CHANGELOG 已同步 | + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 + +- 当前仓库存在一个与本需求无关的遗留方案包 `202603252311_terminal-group-and-broadcast-dedupe`,本次不触碰其代码与任务状态。 diff --git a/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/.status.json b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/.status.json new file mode 100644 index 0000000..0b43dda --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":5,"failed":0,"pending":0,"total":5,"done":5,"percent":100,"current":"已完成 SSH 服务器组头整组关闭与脚本模式引号清洗,并通过前端构建验证","updated_at":"2026-04-12 07:03:00"} diff --git a/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/proposal.md b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/proposal.md new file mode 100644 index 0000000..6184b17 --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/proposal.md @@ -0,0 +1,116 @@ +# 变更提案: ssh-group-close-and-script-input-sanitize + +## 元信息 +```yaml +类型: 功能增强 +方案类型: implementation +优先级: P1 +状态: 实施中 +创建: 2026-04-12 +``` + +--- + +## 1. 需求 + +### 背景 +当前工作区顶部终端标签栏已经把 SSH 会话聚合为“服务器组头”,但组头本身只能切换,不能像单个终端标签那样直接关闭。用户希望在服务器组头上也能出现一个关闭入口,并且点击后一次关闭该服务器下的全部终端,避免逐个点子标签关闭。同时,“添加新连接”的脚本模式虽然已经支持空格、空行和 Markdown 围栏清洗,但脚本值仍只会去掉双引号,像 `-p '$Moka1998A'` 这样的单引号包裹密码会被原样保存,导致实际密码多出 `'`。 + +### 目标 +- 在 `TerminalTabBar.vue` 的 SSH 服务器组头中加入一个独立的关闭按钮,点击后关闭该服务器下全部终端。 +- 保持现有“组头切服务器、组内终端在次级标签条管理”的交互模式,不改后端协议和会话模型。 +- 修正脚本模式参数解析,对单引号和双引号包裹值统一去壳,让密码、代理名、标签、备注等字段都落成真实值。 + +### 约束条件 +```yaml +范围约束: 仅改前端工作区终端标签栏、脚本模式解析链路和相关文案,不改后端 API +交互约束: 服务器组头的主点击行为仍然是激活该服务器,不让新增关闭按钮抢占原有点击语义 +兼容性约束: 非 SSH 的 RDP/VNC 顶层标签保持现状;脚本模式已有的空格、空行和 Markdown 围栏清洗继续保留 +数据约束: 仅移除成对包裹值的外层引号,不修改值内部合法字符 +``` + +### 验收标准 +- [ ] SSH 服务器组头出现可见的关闭按钮,点击后能关闭该服务器下全部终端 +- [ ] 组头关闭按钮不会误触发组头本身的激活切换,单个终端标签关闭行为保持不变 +- [ ] 脚本模式导入 `-p '$Moka1998A'` 时,实际保存的密码是 `$Moka1998A` 而不是带 `'` +- [ ] `packages/frontend` 的构建校验通过 + +--- + +## 2. 方案 + +### 技术方案 +沿用现有 `TerminalTabBar.vue` 对 SSH 会话按 `connectionId` 聚合的结果,在服务器组头按钮内部增加一个次级 `X` 按钮。该按钮通过本地分组会话列表拿到当前服务器的所有 `sessionId`,逐个复用已有的 `session:close` 事件链路关闭,避免引入新的后端或 store 协议。视觉上保持现有“服务器组头胶囊 + 终端子标签”的样式语言,只在 hover 和激活态补充关闭入口。 + +脚本模式解析继续保留在 `useAddConnectionForm.ts` 中处理,新增统一的“去掉成对包裹引号”辅助逻辑,并将参数切分从“只识别双引号”扩展为“同时识别单引号和双引号”。这样 `-p '$Moka1998A'`、`-proxy 'ssh 1'`、`-note 'foo bar'` 等值都能保留真实内容而不带外层引号。 + +### 影响范围 +```yaml +涉及模块: + - frontend: TerminalTabBar.vue 的 SSH 组头按钮与关闭交互 + - frontend: useAddConnectionForm.ts 的脚本模式参数切分与值清洗 + - frontend: locales 文案(组头关闭提示) +预计变更文件: 5-6 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 组头关闭按钮触发冒泡,误切到服务器而不是关闭整组 | 中 | 在关闭按钮处理器中显式 `stopPropagation()` / `preventDefault()` | +| 批量关闭同组终端时,本地分组列表在关闭过程中变化导致遗漏 | 低 | 先基于当前分组生成 `sessionId` 快照,再逐个发送关闭事件 | +| 单引号解析过宽,误删值内部字符 | 低 | 只去掉首尾成对且同类型的包裹引号,不处理内部字符 | + +--- + +## 3. 核心场景 + +### 场景: 关闭某台服务器下的全部终端 +**模块**: frontend / TerminalTabBar +**条件**: 当前连接类型为 SSH,且同一 `connectionId` 下存在 1 个或以上终端 +**行为**: 用户点击服务器组头右侧的关闭按钮,组件基于当前分组会话快照逐个发出既有 `session:close` 事件 +**结果**: 该服务器下的全部终端标签被关闭,其他服务器标签保持不变 + +### 场景: 脚本模式导入带单引号的密码 +**模块**: frontend / useAddConnectionForm +**条件**: 用户在脚本模式中输入 `-p '$Moka1998A'` 这类带单引号包裹的参数值 +**行为**: 解析阶段按单引号/双引号感知切分参数,并在取值时移除成对外层引号 +**结果**: 提交给后端的密码值为 `$Moka1998A`,不会附带 `'` + +--- + +## 4. 技术决策 + +> 本方案涉及的技术决策,归档后成为决策的唯一完整记录 + +### ssh-group-close-and-script-input-sanitize#D001: 组头关闭复用现有单会话关闭链路,脚本值统一做外层引号去壳 +**日期**: 2026-04-12 +**状态**: ✅采纳 +**背景**: 本次需求是现有前端交互增强,不需要改变后端会话协议,但要确保“整组关闭”和“脚本值去壳”都尽量复用既有链路,避免引入新的状态分叉。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 组头关闭复用现有 `session:close` 事件,脚本值统一做外层引号去壳 | 改动边界小,直接复用现有关闭和提交流程,风险低 | 组头关闭是逐个关闭,不是单事件批量关闭 | +| B: 新增“按 connectionId 批量关闭”专用事件,并重写脚本解析器 | 语义更集中,理论上更便于后续扩展 | 需要扩展事件协议,改动面更大,超出这次需求边界 | +**决策**: 选择方案 A +**理由**: 当前需求聚焦前端交互与输入清洗,复用既有关闭链路与表单提交链路更符合“保守修改”和最小影响原则。 +**影响**: 影响 `TerminalTabBar.vue`、`useAddConnectionForm.ts` 及相关 locale 文案。 + +--- + +## 5. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有终端标签栏的黑绿运维胶囊风格,不新增跳脱视觉体系的新按钮样式 +- **记忆点**: 服务器组头在保持整块高亮的同时,hover 后能露出一个紧贴右侧的整组关闭入口 +- **参考**: 现有 `TerminalTabBar.vue` 的服务器组头与子标签胶囊风格 + +### 视觉要素 +- **配色**: 沿用组头当前 `border-primary/60 + bg-primary/10` 激活色系,关闭按钮 hover 时使用 `bg-header` / `text-foreground` +- **字体**: 延续当前标签栏字号体系,不引入额外字体变化 +- **布局**: 关闭按钮嵌在服务器组头右侧,避免破坏左侧 server icon + 名称 + 计数的既有结构 +- **动效**: 延续现有 150ms 渐隐渐显,关闭按钮默认隐藏,hover 组头时显现 +- **氛围**: 保持当前运维面板风格,不追加新纹理或装饰 + +### 技术约束 +- **可访问性**: 关闭按钮保留独立 `title`,避免只靠图标表达语义 +- **响应式**: 继续兼容现有移动端/桌面端标签栏高度,不新增超出标签高度的控件 diff --git a/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/tasks.md b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/tasks.md new file mode 100644 index 0000000..8ee89f8 --- /dev/null +++ b/.helloagents/archive/2026-04/202604120656_ssh-group-close-and-script-input-sanitize/tasks.md @@ -0,0 +1,57 @@ +# 任务清单: ssh-group-close-and-script-input-sanitize + +> **@status:** completed | 2026-04-12 07:09 + +```yaml +@feature: ssh-group-close-and-script-input-sanitize +@created: 2026-04-12 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 5 | 0 | 0 | 5 | + +--- + +## 任务列表 + +### 1. 方案与边界确认 + +- [√] 1.1 创建“SSH 组头整组关闭 + 脚本值引号清洗”实施方案包,并确认沿用现有前端关闭链路与解析链路 | depends_on: [] + +### 2. 终端组头整组关闭 + +- [√] 2.1 在 `packages/frontend/src/components/TerminalTabBar.vue` 中为 SSH 服务器组头加入关闭整组终端的 `X` 按钮,并处理 hover/冒泡细节 | depends_on: [1.1] +- [√] 2.2 补充组头关闭入口所需的 locale 文案,确保 tooltip 语义明确 | depends_on: [2.1] + +### 3. 脚本模式值清洗 + +- [√] 3.1 在 `packages/frontend/src/composables/useAddConnectionForm.ts` 中扩展脚本参数切分与值解析,支持单引号包裹值并统一去掉外层成对引号 | depends_on: [1.1] + +### 4. 验证与同步 + +- [√] 4.1 执行 `packages/frontend` 构建验证,并同步 `.helloagents` 知识库与 CHANGELOG 记录本次落地结果 | depends_on: [2.2, 3.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-12 06:56 | 1.1 | 完成 | 已创建 implementation 方案包,并确认“组头关闭复用现有 session:close 链路 + 脚本值统一去外层引号”方向 | +| 2026-04-12 07:01 | 2.1 / 2.2 | 完成 | 已为 SSH 服务器组头补充整组关闭按钮,并同步中英日 tooltip 文案 | +| 2026-04-12 07:02 | 3.1 | 完成 | 脚本模式参数切分已支持单引号/双引号包裹值,`'$Moka1998A'` 可解析为 `$Moka1998A` | +| 2026-04-12 07:03 | 4.1 | 完成 | `packages/frontend` 构建通过,并完成登录页浏览器冒烟;组头 X 的完整交互仍需已登录且存在 SSH 会话的运行态确认 | + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 + +- 本轮只做前端交互与解析层增强,不改后端接口。 +- 服务器组头关闭按钮按用户选择采用“直接关闭,不再二次确认”。 diff --git a/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/proposal.md b/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/proposal.md new file mode 100644 index 0000000..b8b7e5f --- /dev/null +++ b/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/proposal.md @@ -0,0 +1,112 @@ +# 变更提案: terminal-scroll-viewport-restore-fix + +## 元信息 +```yaml +类型: 缺陷修复 +方案类型: implementation +优先级: P1 +状态: 实施中 +创建: 2026-04-12 +``` + +--- + +## 1. 需求 +### 背景 +当前工作区会为每个 SSH 会话保留一个独立的 `xterm` 终端实例,并在标签切换、尺寸变化和重新激活时恢复 viewport。现有实现使用“绝对行号”保存滚动位置:当终端持续输出日志、用户又切换到其他服务器后,隐藏期间 `buffer.baseY` 会继续增长,但保存的绝对行号不会随之更新。重新激活该终端时,组件会把 viewport 恢复到一个已经过时的绝对位置,导致用户向下滚动时很难再追到底部,表现为“鼠标滚轮先向上滚过一次,后面向下滚也到不了最底部”。 + +### 目标 +- 修复终端在持续输出日志时切换服务器后出现的滚动恢复异常。 +- 保持现有“在底部时继续贴底、离底部阅读时不强制自动跟随”的总体交互策略不变。 +- 将影响范围控制在前端终端组件,不修改后端会话、WebSocket 协议或工作区布局逻辑。 + +### 约束条件 +```yaml +范围约束: 仅修改 packages/frontend 中与 xterm viewport 跟踪和恢复有关的代码与文档 +行为约束: 不新增切换服务器后自动跳底的强制行为,不改变用户主动离开底部阅读时的现有语义 +兼容性约束: 兼容 keep-alive + v-show 的多终端实例结构,兼容现有 ResizeObserver 和 fit() 调整流程 +验证约束: 以 packages/frontend 的 TypeScript 构建通过作为静态验收基线 +``` + +### 验收标准 +- [ ] 终端处于持续输出日志状态时,切换到其他服务器再切回,若此前贴底则仍能恢复到最底部。 +- [ ] 终端离开底部后切换服务器再切回,viewport 恢复应基于“距离底部的偏移”而不是过时的绝对行号,用户继续向下滚动能够重新到达底部。 +- [ ] `packages/frontend` 构建校验通过。 + +--- + +## 2. 方案 + +### 技术方案 +将 `Terminal.vue` 中的 viewport 快照从“绝对 viewport 行号”改为“距底部的偏移量 + 是否贴底”: +- 采集快照时记录 `distanceFromBottom = baseY - viewportY`,而不是仅记录 `viewportY`。 +- 恢复快照时,如果此前处于贴底状态则继续调用 `scrollToBottom()`;如果此前离底部,则按当前最新 `baseY - distanceFromBottom` 计算目标行并恢复。 +- 保留现有 `onScroll`、`ResizeObserver`、`fitAndEmitResizeNow()` 和激活切换逻辑,仅修正快照语义,避免把过期的绝对行号反复应用到不断增长的缓冲区上。 + +### 影响范围 +```yaml +涉及模块: + - frontend: Terminal.vue 的 viewport 跟踪与恢复逻辑 + - frontend: frontend 模块知识库文档与 CHANGELOG 记录 +预计变更文件: 4-5 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 恢复公式错误导致离底部阅读位置跳动 | 中 | 仅替换快照字段语义,复用现有 `scrollToBottom`/`scrollToLine` 恢复链路 | +| 修改后影响普通贴底场景 | 低 | 保留 `shouldStickToBottom` 判断,贴底路径不变 | +| 构建期间暴露出与本次改动无关的旧错误 | 低 | 先执行前端构建验证,若失败则区分是本次回归还是仓库既有问题 | + +--- + +## 3. 核心场景 + +### 场景: 持续日志输出时切换会话后恢复终端 +**模块**: frontend / Terminal.vue +**条件**: 某个 SSH 终端持续输出日志,用户切换到其他服务器后再切回该终端 +**行为**: 组件在重新激活与 `fit()` 后恢复 viewport +**结果**: 若此前贴底则保持贴底;若此前离底部则按距离底部的相对偏移恢复,不会因为隐藏期间日志追加而把终端固定在越来越早的历史位置 + +### 场景: 用户手动滚离底部后再向下滚动 +**模块**: frontend / Terminal.vue +**条件**: 用户曾向上滚动查看历史内容,终端仍在持续输出 +**行为**: 用户向下滚动尝试回到最新输出 +**结果**: viewport 不会被过期快照拖回旧位置,用户可以继续下滚直到重新抵达底部 + +--- + +## 4. 技术决策 + +### terminal-scroll-viewport-restore-fix#D001: viewport 快照改为记录距底部偏移而非绝对行号 +**日期**: 2026-04-12 +**状态**: 已采纳 +**背景**: 当前问题本质是日志持续输出时 `baseY` 持续增长,而组件记录的 `viewportLine` 是静态绝对值。重新激活后按旧绝对值恢复,会让 viewport 相对最新输出越来越靠前。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 保留现有恢复链路,只把快照字段改为“距底部偏移” | 改动小,符合现有交互语义,能直接修复会话切换后的滚动异常 | 仍需依赖现有 `fit()`/`ResizeObserver` 链路的稳定性 | +| B: 切换会话后统一强制 `scrollToBottom()` | 实现最简单,用户总能看到最新输出 | 会改变现有交互语义,破坏用户离底部阅读历史内容的能力 | +**决策**: 选择方案 A +**理由**: 用户已明确要求“只修复当前异常,不额外改动自动跟随策略”。使用相对底部偏移恢复既能消除过时绝对行号带来的问题,又不改变贴底/离底部两种状态的既有语义。 +**影响**: 影响 `packages/frontend/src/components/Terminal.vue` 的 viewport 快照结构和恢复计算方式,并同步 frontend 模块知识库与变更日志。 + +--- + +## 5. 成果设计 + +### 设计方向 +- **美学基调**: N/A,本次为交互缺陷修复,不涉及视觉样式调整 +- **记忆点**: N/A +- **参考**: 现有终端交互保持不变 + +### 视觉要素 +- **配色**: N/A +- **字体**: N/A +- **布局**: N/A +- **动效**: N/A +- **氛围**: N/A + +### 技术约束 +- **可访问性**: 不改动现有 DOM 结构与键鼠交互入口 +- **响应式**: 保持桌面端与移动端现有 `fit()`/`ResizeObserver` 适配流程 diff --git a/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/tasks.md b/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/tasks.md new file mode 100644 index 0000000..7029bac --- /dev/null +++ b/.helloagents/archive/2026-04/202604120705_terminal-scroll-viewport-restore-fix/tasks.md @@ -0,0 +1,52 @@ +# 任务清单: terminal-scroll-viewport-restore-fix + +> **@status:** completed | 2026-04-12 07:18 + +```yaml +@feature: terminal-scroll-viewport-restore-fix +@created: 2026-04-12 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 5 | 0 | 0 | 5 | + +--- + +## 任务列表 + +### 1. 方案与问题定位 +- [√] 1.1 创建“终端滚动恢复修复”方案包,并确认问题根因是 `Terminal.vue` 使用绝对 viewport 行号恢复滚动位置 | depends_on: [] + +### 2. 终端滚动修复 +- [√] 2.1 在 `packages/frontend/src/components/Terminal.vue` 中把 viewport 快照改为记录“距底部偏移 + 是否贴底”,修复会话切换后的滚动恢复异常 | depends_on: [1.1] +- [√] 2.2 复核激活切换、`fit()` 与 `ResizeObserver` 路径,确保修复不改变现有贴底策略 | depends_on: [2.1] + +### 3. 验证与知识库同步 +- [√] 3.1 执行 `packages/frontend` 构建校验,确认本次修改未引入 TypeScript / Vite 构建错误 | depends_on: [2.2] +- [√] 3.2 同步 frontend 模块文档与 `.helloagents/CHANGELOG.md`,记录本次终端滚动修复结果 | depends_on: [3.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-12 07:05 | 1.1 | 完成 | 已创建 implementation 方案包,并确认问题集中在 `Terminal.vue` 的 viewport 绝对行号恢复逻辑 | +| 2026-04-12 07:07 | 2.1 | 完成 | 已将 viewport 快照改为记录距底部偏移,并同步更新激活恢复逻辑 | +| 2026-04-12 07:08 | 2.2 | 完成 | 已复核 `fit()`、`ResizeObserver` 与标签激活路径,确认贴底语义未改动 | +| 2026-04-12 07:09 | 3.1 | 完成 | `packages/frontend` 执行 `npm run build` 通过,仅存在既有 dynamic import 与 chunk size 警告 | +| 2026-04-12 07:10 | 3.2 | 完成 | 已同步 frontend 模块文档与 `.helloagents/CHANGELOG.md` | + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 + +- 本轮仅修复终端切换后的滚动恢复异常,不额外新增“切换服务器后强制跳底”的行为。 +- 当前静态验收为 `packages/frontend` 构建通过;运行态仍建议按“持续输出日志 -> 切换服务器 -> 切回后滚轮上/下验证”做一次手工确认。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index e0b1c3c..07e66ea 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,9 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202604120705 | terminal-scroll-viewport-restore-fix | - | - | - | ✅完成 | +| 202604120656 | ssh-group-close-and-script-input-sanitize | implementation | frontend | ssh-group-close-and-script-input-sanitize#D001 | ✅完成 | +| 202604120656 | server-status-cpu-core-display | - | - | - | ✅完成 | | 202603300204 | global-server-quick-search | - | - | - | ✅完成 | | 202603300206 | workspace-workbench-top-tabs | implementation | frontend | workspace-workbench-top-tabs#D001 | ✅完成 | | 202603292139 | terminal-server-internal-tabs | - | - | - | ✅完成 | @@ -44,6 +47,9 @@ ## 按月归档 +### 2026-04 +- [202604120656_ssh-group-close-and-script-input-sanitize](./2026-04/202604120656_ssh-group-close-and-script-input-sanitize/) - 为 SSH 服务器组头补充整组关闭按钮,并修正脚本模式对单/双引号包裹值的保存行为 + ### 2026-03 - [202603300206_workspace-workbench-top-tabs](./2026-03/202603300206_workspace-workbench-top-tabs/) - 将 Workbench 的导航从左侧竖排 icon rail 调整为 `Workbench` header 上方的横向纯图标栏 - [202603292300_terminal-tab-close-all](./2026-03/202603292300_terminal-tab-close-all/) - 为终端标签右键菜单补充“关闭全部”,并复用现有工作区会话清理链路 diff --git a/.helloagents/modules/backend.md b/.helloagents/modules/backend.md index 0de87a9..7b39788 100644 --- a/.helloagents/modules/backend.md +++ b/.helloagents/modules/backend.md @@ -73,5 +73,5 @@ ### 状态监控字段扩展 **条件**: `StatusMonitorService` 为前端工作区持续轮询服务器状态。 -**行为**: 当前状态采集链路除 `free`、`df`、`/proc/stat` 与 `/proc/net/dev` 外,还会补充解析 `memFree`、`memCached`、`diskAvailable`、`diskMountPoint`、`diskFsType`、`diskDevice`,并基于 `/proc/diskstats` 计算根设备的磁盘读写速率;设备名会尽量从分区名规整到块设备名,无法获取的字段则按 `undefined` 降级。 -**结果**: 前端状态监控可以直接展示参考图风格的内存/磁盘卡片,而不需要再自行推导缓存、空闲和磁盘元信息。 +**行为**: 当前状态采集链路除 `free`、`df`、`/proc/stat` 与 `/proc/net/dev` 外,还会补充解析 `memFree`、`memCached`、`diskAvailable`、`diskMountPoint`、`diskFsType`、`diskDevice`,并基于 `/proc/diskstats` 计算根设备的磁盘读写速率;CPU 规格信息则会先读取 CPU 型号,再通过 `nproc`、`getconf _NPROCESSORS_ONLN`、`grep -c '^processor' /proc/cpuinfo` 与 `lscpu` 多级回退获取 `cpuCores`;无法获取的字段均按 `undefined` 降级。 +**结果**: 前端状态监控可以直接展示参考图风格的内存/磁盘卡片,并额外展示 CPU 核心数,而不需要再自行推导缓存、空闲、磁盘元信息或服务器 CPU 规格。 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index 25e5517..7f5bf26 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -36,7 +36,7 @@ ### 工作区交互 **条件**: 用户进入 `/workspace` 或相关管理页面。 -**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 继续整合快捷指令、命令历史、文件管理和编辑器四个面板,导航入口保持为纯图标按钮,但已调整为位于 `Workbench` 标题区上方的横向 icon rail,四个入口自左向右排列、默认仅显示图标并通过 tooltip 暴露名称,默认激活快捷指令。`CommandInputBar.vue` 当前已将底部命令框升级为支持会话级草稿保留的多行 `textarea`:普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前命令,输入框会按内容自动增高至约 6 行,超出后在输入框内部滚动,并继续兼容快捷指令/命令历史同步与选中发送逻辑。应用根组件 `App.vue` 现在还新增了全局服务器快捷检索:已登录页面按下 `Ctrl+Shift+F` 会打开 `GlobalConnectionQuickSearch.vue`,通过 `utils/connectionSearch.ts` 对连接名称、主机、用户名和类型做本地模糊排序,并直接复用 `sessionStore.handleConnectRequest()` 触发 SSH 工作区跳转或 RDP / VNC 弹窗连接。快捷指令相关能力目前由 `AddEditQuickCommandForm.vue`、`QuickCommandsView.vue` 与新增的 `utils/quickCommandTemplate.ts` 协同实现:编辑弹窗左侧既可维护自定义 `${变量名}`,也提供 `${{date}}`、`${{time}}`、`${{timestamp}}`、`${{week}}`、`${{uuid}}`、`${{random:8}}`、`${{clipboard}}`、`${{password}}` 等动态变量的一键插入;实际执行时会统一走共享解析器,覆盖编辑弹窗执行、列表直接执行、粘贴到命令输入框和发送到全部服务器等链路,并对未定义变量、无法读取的剪贴板或不可用密码给出非阻断告警。`QuickCommandsView.vue` 内的新增按钮、空状态按钮和列表操作按钮统一复用 `bg-button`、`text-button-text`、`hover:bg-button-hover`、`hover:bg-border` 等主题变量类,避免写死黑白 hover 色值;该视图当前还支持命令项右键菜单,并已修正为实底卡片式上下文菜单,提供立即执行、粘贴到命令输入框(不自动发送)、复制命令、发送到全部服务器、编辑和删除等动作。`Terminal.vue` 会跟踪 xterm 的视口行号与贴底状态,在终端标签切换、重新激活和 `fit()` 后按原滚动意图恢复,并在渲染层为带 `xterm-fg-*` class 或内联 `style.color` 的显式前景色字符打标记,让终端文字描边/阴影仅作用于默认前景文本,不覆盖 ANSI 彩色输出;`session.store` 当前会为同一 SSH 连接下的新终端分配递增的 `terminalIndex`。当前顶部 `TerminalTabBar.vue` 已改为服务器级入口:SSH 项只负责在不同服务器之间切换,全局 `+` 继续负责选择其他服务器;同一服务器下的多个终端则下沉到 `LayoutRenderer.vue` 的终端面板内部,以次级标签条承载切换、关闭和新增,从而让“进入服务器后再管理该服务器的多个终端”成为主要交互模型。当前终端标签右键菜单继续复用 `WorkspaceView.vue` 中转的会话关闭链路,除关闭当前、关闭其他、关闭左右侧外,也支持直接触发“关闭全部”来清空当前工作区中的全部终端标签。`ConnectionsView.vue` 已升级为“左侧范围树 + 顶部搜索工具条 + 右侧结果列表”的双栏管理台,当前左侧进一步支持基于标签名路径分隔符推导的多级标签树、树节点展开状态持久化、分组 scope 恢复,以及树工具栏中的展开全部、收起全部和重置范围控制;近期又补上了独立的左侧树搜索、命中节点及祖先路径过滤、命中链路自动展开、节点计数高亮,以及更接近资源管理器的树头部布局;本轮继续为树节点加入 hover 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);`FileManager.vue` 当前已进一步收敛为固定 `/` 根节点的单栏资源管理器树,组件加载时会优先拉取 `/` 目录,树中按“目录在前、文件在后”同时显示目录和文件节点,点击目录只展开与聚焦,点击文件则沿用现有工作区文件打开链路;文件右键菜单链路则已补齐图标化菜单结构、危险态删除项、终端子菜单(执行 `cd` 命令到终端 / 新建终端到当前目录)、复制文件名与复制绝对路径等动作,并继续复用现有下载、权限、新建、上传和删除逻辑,同时又新增了独立“上传文件夹”入口:前端会先将本地目录打包为 zip,再复用现有 `sftp:upload` 链路上传,并在上传成功后自动调用远端解压、尝试清理临时压缩包;外部拖拽文件或目录上传时,则会按鼠标当前悬停的目录作为目标路径,其中目录同样走“先压缩再上传”的路径,从而显著降低小文件很多时的扫描与上传耗时;本轮又补上了拖拽上传前的目标路径确认、桌面端右键子菜单点击展开,以及目录删除时“仅删空目录 / 强制递归删除”的显式二选一;当前右键菜单的关闭职责已经收敛到 `FileManagerContextMenu.vue` 组件层处理,`useFileManagerContextMenu.ts` 不再额外注册捕获阶段的全局点击关闭监听,以避免“终端 / 上传 / 压缩”等带子菜单项在展开或点击前被提前关闭;同时 `useSftpActions.ts` 会在删除目录后自动回退当前或待加载的失效路径,避免文件树持续对已删除目录刷出 `No such file`。样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 +**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 继续整合快捷指令、命令历史、文件管理和编辑器四个面板,导航入口保持为纯图标按钮,但已调整为位于 `Workbench` 标题区上方的横向 icon rail,四个入口自左向右排列、默认仅显示图标并通过 tooltip 暴露名称,默认激活快捷指令。`CommandInputBar.vue` 当前已将底部命令框升级为支持会话级草稿保留的多行 `textarea`:普通 `Enter` 插入换行,`Ctrl+Shift+Enter` 发送当前命令,输入框会按内容自动增高至约 6 行,超出后在输入框内部滚动,并继续兼容快捷指令/命令历史同步与选中发送逻辑。应用根组件 `App.vue` 现在还新增了全局服务器快捷检索:已登录页面按下 `Ctrl+Shift+F` 会打开 `GlobalConnectionQuickSearch.vue`,通过 `utils/connectionSearch.ts` 对连接名称、主机、用户名和类型做本地模糊排序,并直接复用 `sessionStore.handleConnectRequest()` 触发 SSH 工作区跳转或 RDP / VNC 弹窗连接。快捷指令相关能力目前由 `AddEditQuickCommandForm.vue`、`QuickCommandsView.vue` 与新增的 `utils/quickCommandTemplate.ts` 协同实现:编辑弹窗左侧既可维护自定义 `${变量名}`,也提供 `${{date}}`、`${{time}}`、`${{timestamp}}`、`${{week}}`、`${{uuid}}`、`${{random:8}}`、`${{clipboard}}`、`${{password}}` 等动态变量的一键插入;实际执行时会统一走共享解析器,覆盖编辑弹窗执行、列表直接执行、粘贴到命令输入框和发送到全部服务器等链路,并对未定义变量、无法读取的剪贴板或不可用密码给出非阻断告警。`QuickCommandsView.vue` 内的新增按钮、空状态按钮和列表操作按钮统一复用 `bg-button`、`text-button-text`、`hover:bg-button-hover`、`hover:bg-border` 等主题变量类,避免写死黑白 hover 色值;该视图当前还支持命令项右键菜单,并已修正为实底卡片式上下文菜单,提供立即执行、粘贴到命令输入框(不自动发送)、复制命令、发送到全部服务器、编辑和删除等动作。`Terminal.vue` 现在会跟踪 xterm 相对底部的视口偏移与贴底状态,在终端标签切换、重新激活和 `fit()` 后按原滚动意图恢复;当隐藏标签在后台持续追加日志时,重新激活会基于“距底部偏移”而不是过期的绝对行号恢复 viewport,避免用户继续向下滚动时无法回到底部。组件同时继续在渲染层为带 `xterm-fg-*` class 或内联 `style.color` 的显式前景色字符打标记,让终端文字描边/阴影仅作用于默认前景文本,不覆盖 ANSI 彩色输出;`session.store` 当前会为同一 SSH 连接下的新终端分配递增的 `terminalIndex`。当前顶部 `TerminalTabBar.vue` 已改为服务器级入口:SSH 项只负责在不同服务器之间切换,全局 `+` 继续负责选择其他服务器;同一服务器下的多个终端则下沉到 `LayoutRenderer.vue` 的终端面板内部,以次级标签条承载切换、关闭和新增,从而让“进入服务器后再管理该服务器的多个终端”成为主要交互模型。服务器组头现在除主点击切换外,还额外提供了一个 hover 后出现的 `X` 按钮,点击后会复用既有 `session:close` 事件逐个关闭该 `connectionId` 下的全部终端。当前终端标签右键菜单继续复用 `WorkspaceView.vue` 中转的会话关闭链路,除关闭当前、关闭其他、关闭左右侧外,也支持直接触发“关闭全部”来清空当前工作区中的全部终端标签。连接新增弹窗中的脚本模式则继续由 `useAddConnectionForm.ts` 统一清洗输入:会先剔除空行、Markdown 代码围栏行,再按单引号/双引号感知切分参数,并去掉成对包裹值的外层引号,避免像 `-p '$Moka1998A'` 这样的输入把 `'` 一并保存。`ConnectionsView.vue` 已升级为“左侧范围树 + 顶部搜索工具条 + 右侧结果列表”的双栏管理台,当前左侧进一步支持基于标签名路径分隔符推导的多级标签树、树节点展开状态持久化、分组 scope 恢复,以及树工具栏中的展开全部、收起全部和重置范围控制;近期又补上了独立的左侧树搜索、命中节点及祖先路径过滤、命中链路自动展开、节点计数高亮,以及更接近资源管理器的树头部布局;本轮继续为树节点加入 hover 工具按钮、资源管理器式分隔标题行与拖拽重排占位反馈;右侧结果列表则同时支持顶部排序控件、列头点击排序,并将行内操作整理为“连接”主按钮加“更多”菜单(编辑/测试/克隆/删除);`FileManager.vue` 当前已进一步收敛为固定 `/` 根节点的单栏资源管理器树,组件加载时会优先拉取 `/` 目录,树中按“目录在前、文件在后”同时显示目录和文件节点,点击目录只展开与聚焦,点击文件则沿用现有工作区文件打开链路;文件右键菜单链路则已补齐图标化菜单结构、危险态删除项、终端子菜单(执行 `cd` 命令到终端 / 新建终端到当前目录)、复制文件名与复制绝对路径等动作,并继续复用现有下载、权限、新建、上传和删除逻辑,同时又新增了独立“上传文件夹”入口:前端会先将本地目录打包为 zip,再复用现有 `sftp:upload` 链路上传,并在上传成功后自动调用远端解压、尝试清理临时压缩包;外部拖拽文件或目录上传时,则会按鼠标当前悬停的目录作为目标路径,其中目录同样走“先压缩再上传”的路径,从而显著降低小文件很多时的扫描与上传耗时;本轮又补上了拖拽上传前的目标路径确认、桌面端右键子菜单点击展开,以及目录删除时“仅删空目录 / 强制递归删除”的显式二选一;当前右键菜单的关闭职责已经收敛到 `FileManagerContextMenu.vue` 组件层处理,`useFileManagerContextMenu.ts` 不再额外注册捕获阶段的全局点击关闭监听,以避免“终端 / 上传 / 压缩”等带子菜单项在展开或点击前被提前关闭;同时 `useSftpActions.ts` 会在删除目录后自动回退当前或待加载的失效路径,避免文件树持续对已删除目录刷出 `No such file`。样式编辑器中的终端文字描边/阴影默认开关也已与新的黑绿终端风格保持默认开启。 **结果**: 页面逻辑分散在 `views/`、`components/`、`stores/` 与 `composables/`,其中工作区终端行为和标签交互优先落在 `session.store.ts`、`session/actions/sessionActions.ts`、`session/getters.ts`、`TerminalTabBar.vue`、`WorkspaceView.vue`、`Terminal.vue` 与相关 locale 文件。 ### 仪表盘总览 @@ -58,6 +58,6 @@ ### 状态监控卡片 **条件**: 用户在 `/workspace` 右侧状态监控面板查看服务器资源状态。 -**行为**: `StatusMonitor.vue` 当前将内存与磁盘区域升级为卡片化监控视图:内存卡片展示总量、已用、缓存、空闲和环形占比,磁盘卡片展示设备名、文件系统类型、读写速率以及挂载点/大小/可用/已用率表格;CPU、Swap、网络速率和 `StatusCharts.vue` 的 CPU / 网络曲线继续保留。 -**结果**: 状态监控从“简单进度行”升级为“高信息密度卡片”,并直接承接后端新增的内存细分字段与磁盘元数据。 +**行为**: `StatusMonitor.vue` 当前将内存与磁盘区域升级为卡片化监控视图:内存卡片展示总量、已用、缓存、空闲和环形占比,磁盘卡片展示设备名、文件系统类型、读写速率以及挂载点/大小/可用/已用率表格;CPU、Swap、网络速率和 `StatusCharts.vue` 的 CPU / 网络曲线继续保留,其中 CPU 型号行会在主型号文案下方追加一个次级 badge,直接显示后端推送的 `cpuCores`(如 `16 核`)。 +**结果**: 状态监控从“简单进度行”升级为“高信息密度卡片”,并直接承接后端新增的内存细分字段、磁盘元数据和 CPU 核心数,无需用户再登录服务器手动查询机器规格。 diff --git a/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/.status.json b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/.status.json new file mode 100644 index 0000000..45f9432 --- /dev/null +++ b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/.status.json @@ -0,0 +1 @@ +{"status":"in_progress","completed":0,"failed":0,"pending":4,"total":4,"done":0,"percent":0,"current":"正在修改快捷命令列表交互并准备构建验证","updated_at":"2026-04-12 07:10:00"} diff --git a/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/proposal.md b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/proposal.md new file mode 100644 index 0000000..8c202fd --- /dev/null +++ b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/proposal.md @@ -0,0 +1,139 @@ +# 变更提案: quickcommands-double-click-tooltip + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P1 +状态: 已完成 +创建: 2026-04-12 +完成: 2026-04-12 +``` + +--- + +## 1. 需求 + +### 背景 +当前工作台中的快捷命令列表在鼠标单击时会立即执行命令。这个交互对频繁浏览和筛选命令的场景过于敏感,容易在仅想选中或查看命令时误触执行。与此同时,列表中的命令文本在部分模式下会被截断,用户无法通过 hover 直接看到完整命令内容。 + +### 目标 +- 将快捷命令列表的鼠标主交互改为“单击选中、双击执行”。 +- 保留键盘 `Enter` 执行和右键菜单“立即执行”能力,避免回退已有高效入口。 +- 在鼠标悬停快捷命令项时显示完整命令,便于长命令核对。 + +### 约束条件 +```yaml +时间约束: 本轮内完成前端交互改造与基础构建验证 +性能约束: 不新增依赖,不引入额外全局状态 +兼容性约束: 保持现有快捷命令键盘导航、右键菜单动作和动态变量解析链路不回退 +业务约束: 仅收紧鼠标列表项执行方式;用户确认保留键盘 Enter 与右键“立即执行” +``` + +### 验收标准 +- [ ] 快捷命令列表项鼠标单击不再直接执行,而是只更新当前选中态 +- [ ] 快捷命令列表项鼠标双击后仍可向当前活动 SSH 会话执行处理后的命令 +- [ ] 键盘 `Enter` 执行与右键菜单“立即执行”能力保持可用 +- [ ] 鼠标悬停任意快捷命令项时可看到完整命令内容 +- [ ] `packages/frontend` 的构建验证通过 + +--- + +## 2. 方案 + +### 技术方案 +继续在 `QuickCommandsView.vue` 内做最小改动,不拆分新组件。将列表项绑定从单击执行调整为单击设置选中项、双击触发原有 `executeCommand()`;新增一个轻量选择函数,根据当前 `flatVisibleCommands` 反查并写入 `selectedIndex`,确保键盘 `Enter` 仍复用既有选中执行逻辑。完整命令展示直接通过列表项 `title` 属性承载,沿用浏览器原生 tooltip,不新增额外浮层状态。 + +### 影响范围 +```yaml +涉及模块: + - frontend: `QuickCommandsView.vue` 的列表项点击行为与 tooltip 展示 + - frontend: 快捷命令选中态与键盘执行的联动验证 +预计变更文件: 1-3 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 双击执行后,单击选中态与现有键盘导航索引不一致 | 中 | 统一通过 `selectedIndex` 维护选中态,单击先显式写入对应索引 | +| 列表项新增 tooltip 后与按钮自带 `title` 提示冲突 | 低 | 仅在行容器挂载完整命令 title,按钮级 title 保持局部动作提示 | +| 改动鼠标执行手势后影响右键菜单和动态变量执行链路 | 低 | 不改 `executeCommand()` 与菜单动作实现,仅调整触发入口 | + +--- + +## 3. 技术设计(可选) + +### 架构设计 +```mermaid +flowchart LR + A[QuickCommands row click] --> B[selectCommand] + A2[QuickCommands row dblclick] --> C[executeCommand] + B --> D[selectedIndex] + D --> E[keyboard Enter execute] + C --> F[resolveProcessedCommand] +``` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `selectedIndex` | `number` | 当前快捷命令在 `flatVisibleCommands` 中的选中索引 | +| `cmd.command` | `string` | 列表项 hover 时展示的完整命令内容 | + +--- + +## 4. 核心场景 + +### 场景: 单击快捷命令仅选中 +**模块**: frontend +**条件**: 用户在工作台快捷命令列表中单击某条命令。 +**行为**: 组件仅更新当前选中项高亮,不立即向活动会话发送命令。 +**结果**: 用户可以先浏览、对比或配合键盘 `Enter` 再决定是否执行。 + +### 场景: 双击快捷命令立即执行 +**模块**: frontend +**条件**: 用户在工作台快捷命令列表中双击某条命令,且当前存在活动 SSH 会话。 +**行为**: 组件沿用现有命令处理链路,解析动态变量后向当前活动会话执行命令。 +**结果**: 鼠标执行操作改为更明确的双击确认动作。 + +### 场景: hover 查看完整命令 +**模块**: frontend +**条件**: 快捷命令名称或命令文本过长,列表中出现截断显示。 +**行为**: 鼠标移动到命令项上时,浏览器原生 tooltip 展示完整命令字符串。 +**结果**: 用户无需编辑或复制,即可直接核对完整命令内容。 + +--- + +## 5. 技术决策 + +### quickcommands-double-click-tooltip#D001: 使用“单击选中 + 双击执行 + 原生 title tooltip”,而不是引入自定义气泡组件 +**日期**: 2026-04-12 +**状态**: ✅采纳 +**背景**: 用户要求避免快捷命令误触执行,并在 hover 时直接看到完整命令;现有列表已经有选中态和键盘执行能力。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 单击选中、双击执行,完整命令走原生 `title` | 改动最小,兼容现有选中态和键盘执行,无需新增依赖或状态 | tooltip 样式受浏览器控制,可定制度低 | +| B: 保持单击执行,额外增加确认弹层或自定义 tooltip | 视觉上更可控 | 误触问题没有真正消除,交互和实现都更重 | +**决策**: 选择方案A +**理由**: 该需求本质是降低误触风险并补齐信息可见性,现有列表已经具备选中高亮与键盘执行语义,直接复用最稳妥。 +**影响**: frontend + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有工作台深色工具型列表,不引入新的视觉层级,仅修正交互手势 +- **记忆点**: 命令项保持现有高亮风格,但 hover 时能直接看到完整命令 +- **参考**: 当前 `QuickCommandsView.vue` 列表样式与系统原生 tooltip + +### 视觉要素 +- **配色**: 沿用现有主题变量和选中高亮,不新增色彩体系 +- **字体**: 沿用当前命令列表字体体系,命令文本继续使用 monospace 呈现 +- **布局**: 保持现有分组与扁平列表布局不变 +- **动效**: 继续沿用现有 hover/selected 过渡,不新增动画 +- **氛围**: 保持深色工作台的克制工具感,以交互调整替代视觉重绘 + +### 技术约束 +- **可访问性**: 单击选中后需保持现有高亮反馈,便于键盘 `Enter` 执行路径延续 +- **响应式**: 继续兼容紧凑模式与普通模式下的截断显示 diff --git a/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/tasks.md b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/tasks.md new file mode 100644 index 0000000..869993c --- /dev/null +++ b/.helloagents/plan/202604120709_quickcommands-double-click-tooltip/tasks.md @@ -0,0 +1,45 @@ +# 任务清单: quickcommands-double-click-tooltip + +```yaml +@feature: quickcommands-double-click-tooltip +@created: 2026-04-12 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 4 | 0 | 0 | 4 | + +--- + +## 任务列表 + +### 1. 快捷命令交互改造 + +- [√] 1.1 在 `packages/frontend/src/views/QuickCommandsView.vue` 中将快捷命令列表项交互改为单击选中、双击执行 | depends_on: [] +- [√] 1.2 在 `packages/frontend/src/views/QuickCommandsView.vue` 中为命令项补充完整命令 hover 标签,并确保分组/扁平列表两种渲染路径一致 | depends_on: [1.1] + +### 2. 联动验证 + +- [√] 2.1 检查键盘 `Enter` 与右键菜单“立即执行”链路,确保仍复用原有执行逻辑 | depends_on: [1.2] +- [√] 2.2 执行 `npm run build --workspace @nexus-terminal/frontend`,确认类型检查与构建通过 | depends_on: [2.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-12 07:09 | DESIGN | completed | 已确认只收紧鼠标列表项执行方式,保留键盘 Enter 与右键“立即执行” | +| 2026-04-12 07:10 | 1.1 / 1.2 | 完成 | `QuickCommandsView.vue` 已改为单击选中、双击执行,并为命令项增加完整命令 title | +| 2026-04-12 07:11 | 2.1 | 完成 | 代码检查确认 `executeCommand()`、键盘 Enter 与右键菜单执行链路未改动,仅更换鼠标触发入口 | +| 2026-04-12 07:12 | 2.2 | 完成 | `npm run build --workspace @nexus-terminal/frontend` 通过 | + +--- + +## 执行备注 + +> 本轮未新增 locale 文案,完整命令提示直接复用浏览器原生 `title` tooltip。该方案满足“hover 查看完整命令”需求,同时避免引入额外浮层状态和主题样式维护成本。 diff --git a/.playwright-mcp/page-2026-04-11T23-14-58-247Z.yml b/.playwright-mcp/page-2026-04-11T23-14-58-247Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/services/status-monitor.service.ts b/packages/backend/src/services/status-monitor.service.ts index a21249f..0062a98 100644 --- a/packages/backend/src/services/status-monitor.service.ts +++ b/packages/backend/src/services/status-monitor.service.ts @@ -5,6 +5,7 @@ import { settingsService } from '../settings/settings.service'; interface ServerStatus { cpuPercent?: number; + cpuCores?: number; memPercent?: number; memUsed?: number; // MB memTotal?: number; // MB @@ -120,21 +121,7 @@ export class StatusMonitorService { status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown'); } catch (err) { /* noop */ } - try { - let cpuModelOutput = ''; - try { - cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1"); - status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim(); - } catch (procErr) { - cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); - status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim(); - } - if (!status.cpuModel) { - status.cpuModel = 'Unknown'; - } - } catch (err) { - status.cpuModel = 'Unknown'; - } + await this.collectCpuStatus(sshClient, status); await this.collectMemoryStatus(sshClient, status); await this.collectDiskStatus(sshClient, sessionId, timestamp, status); @@ -183,6 +170,62 @@ export class StatusMonitorService { return status as ServerStatus; } + private async collectCpuStatus(sshClient: Client, status: Partial): Promise { + try { + let cpuModelOutput = ''; + try { + cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1"); + status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim(); + } catch (procErr) { + cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); + status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim(); + } + } catch (err) { + status.cpuModel = undefined; + } + + if (!status.cpuModel) { + status.cpuModel = 'Unknown'; + } + + status.cpuCores = await this.resolveCpuCoreCount(sshClient); + } + + private async resolveCpuCoreCount(sshClient: Client): Promise { + const parseCpuCount = (raw?: string): number | undefined => { + if (!raw) { + return undefined; + } + + const match = raw.match(/(\d+)/); + if (!match) { + return undefined; + } + + const value = parseInt(match[1], 10); + return Number.isInteger(value) && value > 0 ? value : undefined; + }; + + const commands = [ + 'nproc', + 'getconf _NPROCESSORS_ONLN', + "grep -c '^processor' /proc/cpuinfo", + "lscpu | grep '^CPU(s):'", + ]; + + for (const command of commands) { + try { + const output = await this.executeSshCommand(sshClient, command); + const cpuCount = parseCpuCount(output); + if (cpuCount !== undefined) { + return cpuCount; + } + } catch (err) { /* noop */ } + } + + return undefined; + } + private async collectMemoryStatus(sshClient: Client, status: Partial): Promise { try { let freeCommand = 'free -m'; diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index 8c8389c..4726b63 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -36,7 +36,10 @@
- {{ displayCpuModel }} +
+ {{ displayCpuModel }} + {{ displayCpuCores }} +
@@ -245,6 +248,7 @@ const displaySwapPercent = computed(() => currentServerStatus.value?.swapPercent const currentStatusError = computed(() => currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null); const cachedCpuModel = ref(null); +const cachedCpuCores = ref(null); const cachedOsName = ref(null); watch(currentServerStatus, newData => { @@ -252,6 +256,9 @@ watch(currentServerStatus, newData => { if (newData.cpuModel) { cachedCpuModel.value = newData.cpuModel; } + if (typeof newData.cpuCores === 'number' && Number.isFinite(newData.cpuCores)) { + cachedCpuCores.value = newData.cpuCores; + } if (newData.osName) { cachedOsName.value = newData.osName; } @@ -266,6 +273,14 @@ watch(() => props.activeSessionId, async (newId, oldId) => { }); const displayCpuModel = computed(() => (currentServerStatus.value?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable')); +const displayCpuCores = computed(() => { + const cpuCores = currentServerStatus.value?.cpuCores ?? cachedCpuCores.value; + if (typeof cpuCores !== 'number' || !Number.isFinite(cpuCores)) { + return t('statusMonitor.notAvailable'); + } + + return t('statusMonitor.cpuCoresValue', { count: Math.round(cpuCores) }); +}); const displayOsName = computed(() => (currentServerStatus.value?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable')); const formatBytesPerSecond = (bytes?: number): string => { @@ -434,6 +449,36 @@ const copyIpToClipboard = async (ipAddress: string | null) => { border-radius: 14px; padding: 14px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); + container-type: inline-size; +} + +.cpu-spec-block { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + min-width: 0; +} + +.cpu-model-value { + display: block; + width: 100%; +} + +.cpu-core-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid rgba(96, 165, 250, 0.26); + background: rgba(37, 99, 235, 0.14); + color: #dbeafe; + font-size: 12px; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; } .status-card__header { @@ -530,6 +575,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { border: 1px solid rgba(255, 255, 255, 0.04); border-radius: 10px; padding: 8px 10px; + min-width: 0; } .memory-stat__label, @@ -537,6 +583,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { display: inline-flex; align-items: center; gap: 6px; + flex-wrap: wrap; font-size: 12px; color: var(--text-secondary-color, #9ca3af); } @@ -550,6 +597,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { font-weight: 700; color: #f8fafc; line-height: 1.15; + overflow-wrap: anywhere; } .memory-stat__dot { @@ -654,6 +702,11 @@ const copyIpToClipboard = async (ipAddress: string | null) => { align-items: center; } +.disk-table__header > span, +.disk-table__row > span { + min-width: 0; +} + .disk-table__header { color: var(--text-secondary-color, #9ca3af); font-size: 12px; @@ -688,6 +741,67 @@ const copyIpToClipboard = async (ipAddress: string | null) => { border: 1px solid rgba(34, 197, 94, 0.18); } +@container (max-width: 320px) { + .status-card__header { + flex-wrap: wrap; + } + + .memory-card__content, + .disk-card__body { + grid-template-columns: 1fr; + } + + .memory-ring, + .disk-usage-tube { + justify-self: center; + } + + .memory-stats-grid, + .disk-rate-grid { + grid-template-columns: 1fr; + } + + .memory-stat__value, + .disk-rate-card__value { + font-size: 18px; + } + + .disk-meta-row { + flex-direction: column; + align-items: flex-start; + } + + .disk-table__header, + .disk-table__row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container (max-width: 250px) { + .status-card { + padding: 12px; + } + + .status-card__badge { + white-space: normal; + } + + .memory-stat__value, + .disk-rate-card__value { + font-size: 16px; + } + + .disk-table__header, + .disk-table__row { + grid-template-columns: 1fr; + } + + .disk-mount-pill, + .disk-percent-pill { + width: 100%; + } +} + @media (max-width: 640px) { .memory-card__content, .disk-card__body { diff --git a/packages/frontend/src/components/Terminal.vue b/packages/frontend/src/components/Terminal.vue index 3b2aa0e..b3579a9 100644 --- a/packages/frontend/src/components/Terminal.vue +++ b/packages/frontend/src/components/Terminal.vue @@ -42,7 +42,7 @@ let lastResizeObserverWidth = 0; let lastResizeObserverHeight = 0; const RESIZE_THRESHOLD = 0.5; // px const BOTTOM_STICK_THRESHOLD = 2; -let lastKnownViewportLine = 0; +let lastKnownDistanceFromBottom = 0; let lastKnownShouldStickToBottom = true; @@ -95,7 +95,7 @@ const debounce = (func: Function, delay: number) => { }; type TerminalViewportSnapshot = { - viewportLine: number; + distanceFromBottom: number; shouldStickToBottom: boolean; }; @@ -103,30 +103,38 @@ const getViewportSnapshot = (term: Terminal): TerminalViewportSnapshot => { const buffer = term.buffer.active; const maxScrollLine = Math.max(0, buffer.baseY); const viewportLine = Math.max(0, Math.min(buffer.viewportY, maxScrollLine)); + // Keep a relative offset from the live bottom so hidden terminals can recover + // the same reading position even if logs keep appending while inactive. + const distanceFromBottom = Math.max(0, maxScrollLine - viewportLine); return { - viewportLine, - shouldStickToBottom: maxScrollLine - viewportLine <= BOTTOM_STICK_THRESHOLD, + distanceFromBottom, + shouldStickToBottom: distanceFromBottom <= BOTTOM_STICK_THRESHOLD, }; }; const syncViewportTracking = (term: Terminal): TerminalViewportSnapshot => { const snapshot = getViewportSnapshot(term); - lastKnownViewportLine = snapshot.viewportLine; + lastKnownDistanceFromBottom = snapshot.distanceFromBottom; lastKnownShouldStickToBottom = snapshot.shouldStickToBottom; return snapshot; }; const restoreViewportSnapshot = (term: Terminal, snapshot?: TerminalViewportSnapshot) => { const effectiveSnapshot = snapshot ?? { - viewportLine: lastKnownViewportLine, + distanceFromBottom: lastKnownDistanceFromBottom, shouldStickToBottom: lastKnownShouldStickToBottom, }; + const maxScrollLine = Math.max(0, term.buffer.active.baseY); if (effectiveSnapshot.shouldStickToBottom) { term.scrollToBottom(); } else { - const targetLine = Math.min(effectiveSnapshot.viewportLine, Math.max(0, term.buffer.active.baseY)); + const targetDistanceFromBottom = Math.min( + Math.max(0, effectiveSnapshot.distanceFromBottom), + maxScrollLine, + ); + const targetLine = Math.max(0, maxScrollLine - targetDistanceFromBottom); term.scrollToLine(targetLine); } @@ -418,7 +426,7 @@ onMounted(() => { // --- Become Active --- console.log(`[Terminal ${props.sessionId}] Becoming active. Observing element and fitting.`); const activationViewportSnapshot = { - viewportLine: lastKnownViewportLine, + distanceFromBottom: lastKnownDistanceFromBottom, shouldStickToBottom: lastKnownShouldStickToBottom, }; // Start observing diff --git a/packages/frontend/src/components/TerminalTabBar.vue b/packages/frontend/src/components/TerminalTabBar.vue index 0be7317..4380546 100644 --- a/packages/frontend/src/components/TerminalTabBar.vue +++ b/packages/frontend/src/components/TerminalTabBar.vue @@ -58,6 +58,16 @@ const closeSession = (event: MouseEvent, sessionId: string) => { emitWorkspaceEvent('session:close', { sessionId }); }; +const closeConnectionGroup = (event: MouseEvent, connectionId: string) => { + event.preventDefault(); + event.stopPropagation(); + + const sessionIds = getConnectionSessions(connectionId).map((session) => session.sessionId); + sessionIds.forEach((sessionId) => { + emitWorkspaceEvent('session:close', { sessionId }); + }); +}; + // --- 本地状态 --- const sessionStore = useSessionStore(); // Session store 保持不变 const showConnectionListPopup = ref(false); // 连接列表弹出状态 @@ -534,34 +544,49 @@ onBeforeUnmount(() => { :class="['flex h-full flex-shrink-0 items-stretch py-1', isGroupStart(index) ? 'pl-1' : 'pl-0']" @dragstart="handleDragStart" > - + + + {{ session.connectionName }} + + + {{ getConnectionSessionCount(session.connectionId) }} + + + +
{ line = line.trim(); if (!line) { @@ -309,8 +320,8 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon let note: string | null = null; // 4. Parse optionsString - // Regex to split by space, respecting quotes - const args = optionsString.match(/(?:[^\s"]+|"[^"]*")+/g) || []; + // Regex to split by space, respecting both single and double quotes + const args = optionsString.match(/"[^"]*"|'[^']*'|[^\s]+/g) || []; let i = 0; while (i < args.length) { const arg = args[i]; @@ -322,7 +333,7 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon // Handle -tags, which can be followed by zero or more tags tags = []; while (i < args.length && !args[i].startsWith('-')) { - tags.push(args[i].replace(/^"|"$/g, '')); // Remove surrounding quotes + tags.push(stripWrappedQuotes(args[i])); i++; } // No need to i++ here, the next loop iteration or outer loop handles it @@ -333,14 +344,14 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon noteParts.push(args[i]); i++; } - note = noteParts.join(' ').replace(/^"|"$/g, ''); // Join parts and remove quotes + note = stripWrappedQuotes(noteParts.join(' ')); break; // Exit the outer loop as note consumes the rest } else if (i >= args.length) { // All other options require a value return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingValueForKey', { key: arg }) }; } else { // Handle options that require a single value - const value = args[i].replace(/^"|"$/g, ''); // Remove surrounding quotes + const value = stripWrappedQuotes(args[i]); switch (key) { case 'type': const typeValue = value.toUpperCase(); diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 3922e18..41943a6 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -626,6 +626,7 @@ "errorPrefix": "Error:", "loading": "Waiting for data...", "cpuModelLabel": "CPU Model:", + "cpuCoresValue": "{count} cores", "osLabel": "OS:", "cpuLabel": "CPU:", "memoryLabel": "Memory:", @@ -1627,6 +1628,7 @@ "selectServerTitle": "Select server to connect", "showTransferProgressTooltip": "Show/Hide Transfer Progress", "newTerminalTooltip": "Open another terminal for the current server", + "closeConnectionGroupTooltip": "Close all terminals for {name} ({count})", "openConnectionPickerTooltip": "Choose another server", "terminalBadge": "Terminal {index}", "serverEntryTitle": "{name} · {count} terminals", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index bab5136..5a5bc1a 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -1365,6 +1365,7 @@ "bytesPerSecond": "B/秒", "cpuLabel": "CPU:", "cpuModelLabel": "CPU モデル:", + "cpuCoresValue": "{count} コア", "diskLabel": "ディスク:", "errorPrefix": "エラー:", "gigaBytes": "GB", @@ -1587,6 +1588,7 @@ "selectServerTitle": "接続するサーバーを選択", "showTransferProgressTooltip": "転送進捗の表示/非表示", "newTerminalTooltip": "現在のサーバーに新しいターミナルを追加", + "closeConnectionGroupTooltip": "{name} の端末をすべて閉じる ({count} 件)", "openConnectionPickerTooltip": "別のサーバーを選択", "terminalBadge": "端末 {index}" }, diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 7c5681e..e8d9d99 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -626,6 +626,7 @@ "errorPrefix": "错误:", "loading": "等待数据...", "cpuModelLabel": "CPU 型号:", + "cpuCoresValue": "{count} 核", "osLabel": "系统:", "cpuLabel": "CPU:", "memoryLabel": "内存:", @@ -1631,6 +1632,7 @@ "selectServerTitle": "选择要连接的服务器", "showTransferProgressTooltip": "显示/隐藏传输进度", "newTerminalTooltip": "为当前服务器新增终端", + "closeConnectionGroupTooltip": "关闭 {name} 的全部终端({count} 个)", "openConnectionPickerTooltip": "选择其他服务器", "terminalBadge": "终端 {index}", "serverEntryTitle": "{name} · {count} 个终端", diff --git a/packages/frontend/src/types/server.types.ts b/packages/frontend/src/types/server.types.ts index 4961553..3ee7607 100644 --- a/packages/frontend/src/types/server.types.ts +++ b/packages/frontend/src/types/server.types.ts @@ -1,6 +1,7 @@ // 类型定义:用于服务器状态监控数据 (从 useStatusMonitor 迁移) export interface ServerStatus { cpuPercent?: number; + cpuCores?: number; memPercent?: number; memUsed?: number; // MB memTotal?: number; // MB diff --git a/packages/frontend/src/views/QuickCommandsView.vue b/packages/frontend/src/views/QuickCommandsView.vue index 327b499..2a50c07 100644 --- a/packages/frontend/src/views/QuickCommandsView.vue +++ b/packages/frontend/src/views/QuickCommandsView.vue @@ -111,10 +111,12 @@ v-for="(cmd) in groupData.commands" :key="cmd.id" :data-command-id="cmd.id" + :title="cmd.command" class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150" :style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }" :class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }" - @click="executeCommand(cmd)" + @click="selectCommand(cmd.id)" + @dblclick="executeCommand(cmd)" @contextmenu.prevent="showQuickCommandContextMenu($event, cmd)" > @@ -157,10 +159,12 @@ v-for="(cmd) in flatFilteredCommands" :key="cmd.id" :data-command-id="cmd.id" + :title="cmd.command" class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150" :style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }" :class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }" - @click="executeCommand(cmd)" + @click="selectCommand(cmd.id)" + @dblclick="executeCommand(cmd)" @contextmenu.prevent="showQuickCommandContextMenu($event, cmd)" > @@ -392,6 +396,10 @@ const isCommandSelected = (commandId: number): boolean => { return flatVisibleCommands.value[storeSelectedIndex.value].id === commandId; }; +const selectCommand = (commandId: number) => { + storeSelectedIndex.value = flatVisibleCommands.value.findIndex((cmd) => cmd.id === commandId); +}; + // --- 生命周期钩子 ---