docs/plan/2026-06-08-terminal-design.md
Rama main · réplica de lectura del repositorio original.
# 内嵌终端模块技术设计文档
> 版本:v1.0 | 日期:2026-06-05 | 状态:设计中
## 1. 需求概述
### 1.1 功能目标
在 Electron 桌面客户端中内置终端模块,替代现有的"打开系统终端"功能,提供类似 VSCode Terminal 的使用体验。
### 1.2 核心需求
| 项目 | 描述 |
|------|------|
| 位置 | 底部面板,挤压上方内容区域 |
| 多终端 | 支持多 Tab,创建、切换、关闭 |
| 默认 cwd | LOCAL 模式跟随任务工作区,CLOUD 模式打开本地 home 目录 |
| Shell | 读取用户 `$SHELL` 环境变量(macOS 默认 zsh,Linux 默认 bash) |
| 审批 | 无审批机制,用户直接操作 |
| 权限 | 所有登录用户可用 |
| 持久化 | 轻量方案,纯内存,关闭/重启后不恢复 |
| 面板高度 | 可拖拽调整,默认收起 |
| 快捷键 | `` Ctrl+` `` 展开/收起终端面板 |
### 1.3 V1 范围
**包含:**
- 底部面板 + 可拖拽高度
- 多终端 Tab(创建、切换、关闭)
- 真实 PTY + xterm.js 渲染
- ANSI 颜色、基本键盘交互
- 自动检测 `$SHELL`
- 默认 cwd 跟随工作区
- 快捷键展开/收起
**不含(V2):**
- 终端内搜索
- 链接可点击
- 分屏
- 终端字体/主题配置
- cwd 与任务同步
- 终端输出持久化
## 2. 架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────┐
│ Renderer (Vue 3) │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ TaskView │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Chat Area (messages + input) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ TerminalPanel (可拖拽高度) │ │ │
│ │ │ ┌─────┬─────┬─────┬─────┐ │ │ │
│ │ │ │Tab 1│Tab 2│Tab 3│ + │ ← TerminalTabs │ │ │
│ │ │ └─────┴─────┴─────┴─────┘ │ │ │
│ │ │ ┌───────────────────────────────────────┐ │ │ │
│ │ │ │ xterm.js Terminal Instance │ │ │ │
│ │ │ │ (每个 Tab 对应一个 xterm 实例) │ │ │ │
│ │ │ └───────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │ window.electronAPI.terminal.* │
│ ▼ IPC │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Preload Bridge (contextBridge) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Main Process │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ TerminalManager │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ terminalSessions: Map<id, TerminalSession> │ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │
│ │ │ │ Session 1 │ │ Session 2 │ ... │ │ │
│ │ │ │ pty (node-pty)│ │ pty │ │ │ │
│ │ │ │ shell: zsh │ │ shell: bash │ │ │ │
│ │ │ │ cwd: /workspace│ │ cwd: ~ │ │ │ │
│ │ │ └──────────────┘ └──────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 2.2 与现有架构的关系
终端模块是**完全独立的新通道**,不与现有 Agent 工具执行体系耦合:
- **不复用** `shellSessions`(现有 marker 模式 shell 会话)
- **不复用** `handleShellFromWebSocket`(现有工具执行逻辑)
- **不经过** WebSocket 连接后端服务
- **不经过** 工具审批流
IPC 通道完全独立,使用新的 `terminal:*` 命名空间。
## 3. 详细设计
### 3.1 新增依赖
```jsonc
// package.json 新增
{
"dependencies": {
"@xterm/xterm": "^5.5.0", // 终端模拟器 UI
"@xterm/addon-fit": "^0.10.0", // 自适应尺寸
"@xterm/addon-web-links": "^0.11.0" // 链接检测(V2 启用)
},
"devDependencies": {
"node-pty": "^1.0.0" // 伪终端(原生模块)
}
}
```
**node-pty 编译配置:**
在 `package.json` 的 `electron-builder` 配置中增加原生模块重建:
```jsonc
{
"build": {
"npmRebuild": true,
"nodeGypRebuild": false,
// ...
}
}
```
在 `scripts` 中增加重建命令:
```jsonc
{
"scripts": {
"postinstall": "electron-rebuild -f -w node-pty"
}
}
```
需要额外安装 `electron-rebuild` 作为 devDependency。
### 3.2 Main 进程 — TerminalManager
#### 文件:`desktop/electron/terminalManager.cjs`
独立模块,导出 TerminalManager 类,由 `main.cjs` 引入并初始化。
```javascript
// terminalManager.cjs 核心结构
const pty = require('node-pty');
const os = require('os');
const path = require('path');
class TerminalManager {
constructor() {
this.sessions = new Map(); // id -> TerminalSession
this.nextId = 1;
}
/**
* 创建新的终端会话
* @param {object} options
* @param {string} options.cwd - 初始工作目录
* @param {number} options.cols - 列数
* @param {number} options.rows - 行数
* @param {string} [options.shell] - 指定 shell 路径,默认读取 $SHELL
* @returns {{ id: string }}
*/
create(options) {
const id = `term_${this.nextId++}`;
const shell = options.shell || this._detectShell();
const cwd = options.cwd || os.homedir();
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: options.cols || 80,
rows: options.rows || 24,
cwd: cwd,
env: {
...process.env,
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
},
});
const session = {
id,
pty: ptyProcess,
shell,
cwd,
createdAt: Date.now(),
};
this.sessions.set(id, session);
return { id };
}
/**
* 向终端写入数据(键盘输入)
*/
write(id, data) {
const session = this.sessions.get(id);
if (session) {
session.pty.write(data);
}
}
/**
* 调整终端尺寸
*/
resize(id, cols, rows) {
const session = this.sessions.get(id);
if (session) {
session.pty.resize(cols, rows);
}
}
/**
* 关闭终端会话
*/
kill(id) {
const session = this.sessions.get(id);
if (session) {
session.pty.kill();
this.sessions.delete(id);
}
}
/**
* 关闭所有终端会话(应用退出时调用)
*/
killAll() {
for (const [id, session] of this.sessions) {
session.pty.kill();
}
this.sessions.clear();
}
/**
* 检测用户默认 shell
*/
_detectShell() {
if (process.env.SHELL) {
return process.env.SHELL;
}
// macOS 默认 zsh,其他平台默认 bash
return process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash';
}
/**
* 获取终端会话列表
*/
list() {
return Array.from(this.sessions.values()).map(s => ({
id: s.id,
shell: s.shell,
cwd: s.cwd,
createdAt: s.createdAt,
}));
}
}
module.exports = { TerminalManager };
```
#### main.cjs 修改点
```javascript
// 引入 TerminalManager
const { TerminalManager } = require('./terminalManager.cjs');
const terminalManager = new TerminalManager();
// 注册 IPC handlers
ipcMain.handle('terminal:create', (event, options) => {
const result = terminalManager.create(options);
// 监听 pty 输出,推送到渲染端
const session = terminalManager.sessions.get(result.id);
session.pty.onData((data) => {
mainWindow.webContents.send('terminal:data', {
id: result.id,
data,
});
});
// 监听 pty 退出
session.pty.onExit(({ exitCode }) => {
mainWindow.webContents.send('terminal:exit', {
id: result.id,
exitCode,
});
terminalManager.sessions.delete(result.id);
});
return result;
});
ipcMain.on('terminal:data', (event, { id, data }) => {
terminalManager.write(id, data);
});
ipcMain.on('terminal:resize', (event, { id, cols, rows }) => {
terminalManager.resize(id, cols, rows);
});
ipcMain.handle('terminal:kill', (event, { id }) => {
terminalManager.kill(id);
});
ipcMain.handle('terminal:list', () => {
return terminalManager.list();
});
// 应用退出时清理
app.on('before-quit', () => {
terminalManager.killAll();
});
```
**IPC 命名规范说明:**
- `terminal:data`(渲染端 → Main)使用 `ipcMain.on`(fire-and-forget,高频键盘输入不需要 Promise)
- `terminal:create`、`terminal:kill`、`terminal:list` 使用 `ipcMain.handle`(需要返回值)
- `terminal:data`、`terminal:exit`(Main → 渲染端)使用 `mainWindow.webContents.send`
### 3.3 Preload Bridge
#### preload.cjs 新增
```javascript
// 在 contextBridge.exposeInMainWorld 的 electronAPI 对象中增加 terminal 命名空间
terminal: {
// 创建终端,返回 { id }
create: (options) => ipcRenderer.invoke('terminal:create', options),
// 向终端写入数据(键盘输入),高频调用用 send 而非 invoke
write: (id, data) => ipcRenderer.send('terminal:data', { id, data }),
// 调整终端尺寸
resize: (id, cols, rows) => ipcRenderer.send('terminal:resize', { id, cols, rows }),
// 关闭终端
kill: (id) => ipcRenderer.invoke('terminal:kill', { id }),
// 获取终端列表
list: () => ipcRenderer.invoke('terminal:list'),
// 监听终端输出(Main → Renderer)
onData: (callback) => {
const handler = (event, payload) => callback(payload);
ipcRenderer.on('terminal:data', handler);
return () => ipcRenderer.removeListener('terminal:data', handler);
},
// 监听终端退出(Main → Renderer)
onExit: (callback) => {
const handler = (event, payload) => callback(payload);
ipcRenderer.on('terminal:exit', handler);
return () => ipcRenderer.removeListener('terminal:exit', handler);
},
},
```
**类型声明:** 在 `src/types/electron.d.ts` 中补充 `window.electronAPI.terminal` 的 TypeScript 类型。
### 3.4 Renderer — 组件设计
#### 3.4.1 组件结构
```
src/components/terminal/
├── TerminalPanel.vue # 底部面板容器(高度拖拽 + 展开/收起)
├── TerminalTabs.vue # Tab 栏(创建、切换、关闭)
├── TerminalView.vue # 单个终端实例(xterm.js 封装)
└── useTerminal.ts # Composable(终端状态管理 + IPC 通信)
```
#### 3.4.2 useTerminal.ts — Composable
```typescript
// src/composables/useTerminal.ts
import { ref, shallowRef, onUnmounted } from 'vue';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
export interface TerminalTab {
id: string; // 与 Main 进程 session id 对应
title: string; // 显示标题(如 "zsh", "bash")
cwd: string; // 当前工作目录
}
// 模块级单例状态
const tabs = ref<TerminalTab[]>([]);
const activeTabId = ref<string | null>(null);
const isOpen = ref(false);
// 终端实例缓存:id -> { terminal, fitAddon, disposers }
const instances = new Map<string, {
terminal: Terminal;
fitAddon: FitAddon;
disposers: Array<() => void>;
}>();
// IPC 事件清理函数
let removeDataListener: (() => void) | null = null;
let removeExitListener: (() => void) | null = null;
export function useTerminal() {
// 初始化 IPC 监听(仅执行一次)
function initListeners() {
if (removeDataListener) return;
removeDataListener = window.electronAPI.terminal.onData(
({ id, data }: { id: string; data: string }) => {
const inst = instances.get(id);
if (inst) {
inst.terminal.write(data);
}
}
);
removeExitListener = window.electronAPI.terminal.onExit(
({ id, exitCode }: { id: string; exitCode: number }) => {
// 移除 tab 和实例
removeTab(id);
}
);
}
/**
* 创建新终端
* @param cwd 初始工作目录
*/
async function createTerminal(cwd?: string): Promise<string> {
initListeners();
const cols = 80;
const rows = 24;
const result = await window.electronAPI.terminal.create({ cwd, cols, rows });
const id = result.id;
// 创建 xterm 实例
const terminal = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: '#1e1e1e',
foreground: '#cccccc',
cursor: '#ffffff',
// 更多颜色跟随主题...
},
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
instances.set(id, { terminal, fitAddon, disposers: [] });
// 将键盘输入转发到 Main 进程
const dataDisposer = terminal.onData((data) => {
window.electronAPI.terminal.write(id, data);
});
// 监听 resize 事件(由外部触发 fit 时产生)
const resizeDisposer = terminal.onResize(({ cols, rows }) => {
window.electronAPI.terminal.resize(id, cols, rows);
});
instances.get(id)!.disposers.push(
() => dataDisposer.dispose(),
() => resizeDisposer.dispose()
);
// 添加 tab
const shellName = detectShellName();
tabs.value.push({ id, title: shellName, cwd: cwd || '~' });
activeTabId.value = id;
return id;
}
/**
* 将 xterm 实例挂载到 DOM 元素
*/
function mountTerminal(id: string, container: HTMLElement) {
const inst = instances.get(id);
if (!inst) return;
inst.terminal.open(container);
// 延迟 fit,等待 DOM 渲染完成
requestAnimationFrame(() => {
inst.fitAddon.fit();
// 将实际尺寸同步到 Main 进程
window.electronAPI.terminal.resize(
id,
inst.terminal.cols,
inst.terminal.rows
);
});
}
/**
* 触发 fit(窗口 resize 时调用)
*/
function fitTerminal(id: string) {
const inst = instances.get(id);
if (!inst) return;
inst.fitAddon.fit();
}
/**
* 切换活跃 Tab
*/
function switchTab(id: string) {
activeTabId.value = id;
}
/**
* 关闭终端
*/
async function closeTerminal(id: string) {
await window.electronAPI.terminal.kill(id);
removeTab(id);
}
/**
* 内部:移除 tab 和清理实例
*/
function removeTab(id: string) {
const inst = instances.get(id);
if (inst) {
inst.disposers.forEach(d => d());
inst.terminal.dispose();
instances.delete(id);
}
const idx = tabs.value.findIndex(t => t.id === id);
if (idx !== -1) {
tabs.value.splice(idx, 1);
}
// 如果关闭的是当前活跃 tab,切换到相邻 tab
if (activeTabId.value === id) {
if (tabs.value.length > 0) {
const newIdx = Math.min(idx, tabs.value.length - 1);
activeTabId.value = tabs.value[newIdx].id;
} else {
activeTabId.value = null;
isOpen.value = false; // 所有 tab 关闭后收起面板
}
}
}
/**
* 切换面板展开/收起
*/
function togglePanel() {
isOpen.value = !isOpen.value;
// 展开时如果没有终端,自动创建一个
if (isOpen.value && tabs.value.length === 0) {
createTerminal();
}
}
/**
* 获取活跃终端实例(供外部组件使用)
*/
function getActiveInstance() {
if (!activeTabId.value) return null;
return instances.get(activeTabId.value) || null;
}
function detectShellName(): string {
const shell = import.meta.env.VITE_SHELL || '/bin/zsh';
return shell.split('/').pop() || 'zsh';
}
// 清理
onUnmounted(() => {
// 不在这里清理全局监听,因为 useTerminal 是单例模式
// 清理在应用退出时由 Main 进程的 terminalManager.killAll() 处理
});
return {
// 状态
tabs,
activeTabId,
isOpen,
// 操作
createTerminal,
mountTerminal,
fitTerminal,
switchTab,
closeTerminal,
togglePanel,
getActiveInstance,
};
}
```
#### 3.4.3 TerminalPanel.vue — 底部面板
```vue
<!-- 概念结构,非完整代码 -->
<template>
<div v-show="isOpen" class="terminal-panel" :style="{ height: panelHeight + 'px' }">
<!-- 拖拽条 -->
<div class="terminal-resize-handle" @mousedown="startResize" />
<!-- Tab 栏 -->
<TerminalTabs
:tabs="tabs"
:active-id="activeTabId"
@switch="switchTab"
@close="closeTerminal"
@create="createTerminal"
/>
<!-- 终端容器 -->
<div class="terminal-container">
<!-- 每个 tab 对应一个容器,通过 v-show 控制显隐 -->
<div
v-for="tab in tabs"
:key="tab.id"
v-show="tab.id === activeTabId"
:ref="el => setContainerRef(tab.id, el)"
class="terminal-instance"
/>
</div>
</div>
</template>
```
**关键设计决策:**
- 使用 `v-show` 而非 `v-if` 切换终端实例,避免 xterm 实例被销毁重建
- 每个终端实例挂载到独立的 `<div>` 容器
- 通过 `:ref` 回调收集容器引用,在 Tab 切换时调用 `mountTerminal` / `fitTerminal`
#### 3.4.4 面板高度拖拽
```typescript
// TerminalPanel.vue 中的拖拽逻辑
const panelHeight = ref(300); // 默认高度 300px
const MIN_HEIGHT = 100;
const MAX_HEIGHT_RATIO = 0.7; // 最大不超过视口高度的 70%
function startResize(e: MouseEvent) {
const startY = e.clientY;
const startHeight = panelHeight.value;
function onMove(e: MouseEvent) {
const delta = startY - e.clientY; // 向上拖拽为正值
const maxHeight = window.innerHeight * MAX_HEIGHT_RATIO;
panelHeight.value = Math.max(MIN_HEIGHT, Math.min(maxHeight, startHeight + delta));
// 触发当前活跃终端的 fit
if (activeTabId.value) {
fitTerminal(activeTabId.value);
}
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
}
```
### 3.5 布局集成
#### TaskView.vue 改造
当前 TaskView 的 `.task-container` 结构:
```
.task-container (flex-column)
├── .messages (flex: 1, overflow-y: auto)
├── QueuePanel (flex-shrink: 0)
└── ChatInput (flex-shrink: 0)
```
改造后:
```
.task-container (flex-column)
├── .chat-area (flex: 1, min-height: 0, flex-column)
│ ├── .messages (flex: 1, overflow-y: auto)
│ ├── QueuePanel (flex-shrink: 0)
│ └── ChatInput (flex-shrink: 0)
└── TerminalPanel (flex-shrink: 0, 可拖拽高度, v-show)
```
将原来的 `messages + QueuePanel + ChatInput` 包裹在一个新的 `.chat-area` 容器中,TerminalPanel 放在其下方。TerminalPanel 的高度由拖拽控制,`.chat-area` 自动占据剩余空间。
#### CSS 变量新增
```css
:root {
--aw-terminal-bg: #1e1e1e;
--aw-terminal-header-height: 36px; /* Tab 栏高度 */
}
```
### 3.6 快捷键
在 `Layout.vue` 或 `TaskView.vue` 中注册全局快捷键:
```typescript
// TaskView.vue
useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// Ctrl+` 或 Cmd+` 切换终端面板
if ((e.ctrlKey || e.metaKey) && e.key === '`') {
e.preventDefault();
togglePanel();
}
});
```
### 3.7 默认 cwd 逻辑
```typescript
// 在 TaskView.vue 中创建终端时传入 cwd
async function getDefaultCwd(): Promise<string | undefined> {
const session = sessionStore.activeSession;
if (!session) return undefined;
// LOCAL 模式:使用任务工作区
if (session.executionMode === 'LOCAL' && session.workspace) {
return session.workspace;
}
// CLOUD 模式或无工作区:undefined(Main 进程会使用 os.homedir())
return undefined;
}
```
切换活跃任务时,不自动切换已有终端的 cwd(避免打断用户操作)。新创建的终端使用当前任务的工作区。
## 4. 文件变更清单
### 4.1 新增文件
| 文件 | 说明 |
|------|------|
| `desktop/electron/terminalManager.cjs` | Main 进程终端管理器 |
| `desktop/src/composables/useTerminal.ts` | 终端 Composable |
| `desktop/src/components/terminal/TerminalPanel.vue` | 底部面板容器 |
| `desktop/src/components/terminal/TerminalTabs.vue` | Tab 栏组件 |
| `desktop/src/components/terminal/TerminalView.vue` | xterm 实例封装(可选,逻辑可内联到 Panel) |
| `desktop/src/types/electron.d.ts` | electronAPI 类型声明(补充 terminal 命名空间) |
### 4.2 修改文件
| 文件 | 变更 |
|------|------|
| `desktop/package.json` | 新增依赖:@xterm/xterm、@xterm/addon-fit、node-pty;新增 postinstall 脚本 |
| `desktop/electron/main.cjs` | 引入 TerminalManager,注册 terminal:* IPC handlers,before-quit 清理 |
| `desktop/electron/preload.cjs` | 暴露 window.electronAPI.terminal.* API |
| `desktop/src/views/task/TaskView.vue` | 集成 TerminalPanel,改造布局结构,注册快捷键 |
| `desktop/src/components/common/Layout.vue` | 可能需要调整布局(如果终端面板需要跨全宽) |
| `desktop/src/style.css` | 新增终端相关 CSS 变量 |
### 4.3 不变文件
- 后端代码无任何变更
- 现有 Shell 会话管理(`handleShellFromWebSocket`)不变
- WebSocket 连接逻辑不变
- Pinia stores 不变(终端状态在 useTerminal composable 内管理)
## 5. 数据流
### 5.1 创建终端
```
用户点击 "+" 按钮
→ TerminalTabs emit "create"
→ useTerminal.createTerminal(cwd)
→ ipcRenderer.invoke('terminal:create', { cwd, cols, rows })
→ Main: terminalManager.create(options)
→ pty.spawn(shell, [], { cwd, cols, rows, ... })
→ 返回 { id }
→ Renderer: 创建 xterm.Terminal 实例
→ 添加 tab 到 tabs[]
→ activeTabId = id
→ TerminalPanel 渲染新 tab 的容器 div
→ mountTerminal(id, container)
→ terminal.open(container)
→ fitAddon.fit()
```
### 5.2 键盘输入
```
用户在 xterm 中按键
→ xterm.onData callback
→ ipcRenderer.send('terminal:data', { id, data })
→ Main: terminalManager.write(id, data)
→ pty.write(data)
→ pty 产生输出
→ pty.onData callback
→ mainWindow.webContents.send('terminal:data', { id, data })
→ Renderer: ipcRenderer 'terminal:data' handler
→ terminal.write(data)
→ xterm 渲染输出
```
### 5.3 窗口 resize
```
用户拖拽窗口边缘
→ TaskView 监听 resize 事件
→ fitTerminal(activeTabId)
→ fitAddon.fit()
→ xterm.onResize callback
→ ipcRenderer.send('terminal:resize', { id, cols, rows })
→ Main: pty.resize(cols, rows)
```
### 5.4 关闭终端
```
用户点击 Tab 的关闭按钮
→ TerminalTabs emit "close" (id)
→ useTerminal.closeTerminal(id)
→ ipcRenderer.invoke('terminal:kill', { id })
→ Main: pty.kill() + sessions.delete(id)
→ Renderer: terminal.dispose() + 移除 tab
→ 如果无剩余 tab,isOpen = false(面板收起)
```
## 6. node-pty 编译与打包
### 6.1 开发环境
```bash
# 安装依赖后自动执行 electron-rebuild
npm install
# 或手动执行
npx electron-rebuild -f -w node-pty
```
### 6.2 生产打包
electron-builder 默认会处理原生模块的 rebuild。需要确认:
```jsonc
// package.json electron-builder 配置
{
"build": {
"npmRebuild": true,
"mac": {
"target": "dmg"
}
}
}
```
### 6.3 潜在风险
| 风险 | 应对 |
|------|------|
| node-pty 编译失败(缺少 C++ 编译工具链) | 文档中说明需要 Xcode Command Line Tools (macOS) 或 build-essential (Linux) |
| Electron 版本升级后 node-pty 不兼容 | 通过 electron-rebuild 重新编译,node-pty 对 Electron 版本一般兼容 |
| macOS ARM64 vs x64 架构差异 | electron-builder 支持 per-arch 构建,node-pty 支持多架构 |
## 7. 样式规范
遵循现有的 Apple 设计风格(`style.css` 中的设计令牌):
```css
.terminal-panel {
background: var(--aw-canvas);
border-top: 1px solid var(--aw-divider-soft);
display: flex;
flex-direction: column;
position: relative;
}
.terminal-resize-handle {
position: absolute;
top: -3px;
left: 0;
right: 0;
height: 6px;
cursor: row-resize;
z-index: 10;
}
.terminal-resize-handle:hover {
background: var(--aw-primary);
opacity: 0.3;
}
.terminal-tabs {
height: var(--aw-terminal-header-height);
display: flex;
align-items: center;
padding: 0 12px;
gap: 4px;
border-bottom: 1px solid var(--aw-divider-soft);
background: var(--aw-bg-secondary);
flex-shrink: 0;
}
.terminal-container {
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--aw-terminal-bg);
}
.terminal-instance {
width: 100%;
height: 100%;
}
```
## 8. 测试验证
### 8.1 功能验证项
- [ ] 快捷键 `` Ctrl+` `` 展开/收起终端面板
- [ ] 点击 "+" 创建新终端 Tab
- [ ] Tab 切换,终端实例保持状态
- [ ] Tab 关闭,实例正确清理
- [ ] 最后一个 Tab 关闭后面板自动收起
- [ ] 键盘输入正确传递,命令执行有输出
- [ ] ANSI 颜色正确渲染
- [ ] 交互式程序(如 `vim`、`top`)可正常使用
- [ ] 拖拽调整面板高度,终端自适应
- [ ] 窗口 resize 后终端自适应
- [ ] LOCAL 模式下默认 cwd 为任务工作区
- [ ] CLOUD 模式下默认 cwd 为 home 目录
- [ ] 应用退出时终端进程正确清理(无僵尸进程)
- [ ] 多终端并行运行不互相干扰