# 内嵌终端模块技术设计文档 > 版本: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 │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ 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([]); const activeTabId = ref(null); const isOpen = ref(false); // 终端实例缓存:id -> { terminal, fitAddon, disposers } const instances = new Map 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 { 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 ``` **关键设计决策:** - 使用 `v-show` 而非 `v-if` 切换终端实例,避免 xterm 实例被销毁重建 - 每个终端实例挂载到独立的 `
` 容器 - 通过 `: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 { 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 目录 - [ ] 应用退出时终端进程正确清理(无僵尸进程) - [ ] 多终端并行运行不互相干扰