# Phase 5：前端集成

日期：2026-04-04
状态：待实施

依赖：Phase 4 完成

---

## 1. 本阶段目的

Vue 前端新增代码指挥官 UI：
- 页面组件
- 终端显示组件
- WebSocket 服务
- 路由配置

---

## 2. 详细任务

### 2.1 页面组件

**新文件**: `frontend/src/pages/chat/CodeCommander.vue`

```vue
<template>
  <div class="code-commander">
    <!-- AI 提供商选择器 -->
    <div class="provider-selector">
      <div class="label">选择 AI 助手</div>
      <div class="providers">
        <button
          v-for="p in providers"
          :key="p.id"
          :class="{ active: selectedProvider === p.id }"
          @click="selectedProvider = p.id"
        >
          <img :src="p.icon" :alt="p.name" />
          {{ p.name }}
        </button>
      </div>
    </div>

    <!-- 任务输入 -->
    <div class="task-input">
      <textarea
        v-model="taskPrompt"
        placeholder="描述你想让 AI 帮你做什么..."
        rows="4"
      />
      <button @click="executeTask" :disabled="isExecuting">
        {{ isExecuting ? '执行中...' : '开始执行' }}
      </button>
    </div>

    <!-- 终端输出 -->
    <TerminalDisplay
      ref="terminalRef"
      :session-id="currentSessionId"
      @input="handleUserInput"
    />

    <!-- 交互输入框 -->
    <div v-if="isWaitingForInput" class="interactive-input">
      <span>{{ inputPrompt }}</span>
      <input v-model="userInput" @keyup.enter="sendUserInput" />
    </div>

    <!-- 操作按钮 -->
    <div class="actions">
      <button @click="downloadFiles" :disabled="!canDownload">
        下载文件
      </button>
      <button @click="cleanup" :disabled="!canCleanup">
        清理
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import TerminalDisplay from '@/components/TerminalDisplay.vue'
import { terminalWsService } from '@/services/terminalWs'

const providers = [
  { id: 'claude', name: 'Claude', icon: '/icons/claude.png' },
  { id: 'gemini', name: 'Gemini', icon: '/icons/gemini.png' },
  { id: 'codex', name: 'Codex', icon: '/icons/codex.png' },
  { id: 'opencode', name: 'OpenCode', icon: '/icons/opencode.png' },
]

const selectedProvider = ref('claude')
const taskPrompt = ref('')
const isExecuting = ref(false)
const currentSessionId = ref<string | null>(null)
const isWaitingForInput = ref(false)
const inputPrompt = ref('')
const userInput = ref('')
const terminalRef = ref<InstanceType<typeof TerminalDisplay> | null>(null)

const canDownload = computed(() => currentSessionId.value !== null)
const canCleanup = computed(() => currentSessionId.value !== null)

async function executeTask() {
  if (!taskPrompt.value.trim()) return
  
  isExecuting.value = true
  currentSessionId.value = await terminalWsService.connect(selectedProvider.value)
  
  // 订阅消息
  terminalWsService.onMessage((msg) => {
    if (msg.type === 'output') {
      terminalRef.value?.write(msg.data)
    } else if (msg.type === 'waiting_input') {
      isWaitingForInput.value = true
      inputPrompt.value = msg.data
    } else if (msg.type === 'complete') {
      isExecuting.value = false
    }
  })
  
  // 发送任务
  await terminalWsService.sendTask(currentSessionId.value, taskPrompt.value)
}

function handleUserInput(data: string) {
  terminalWsService.sendInput(currentSessionId.value!, data)
}

function sendUserInput() {
  terminalWsService.sendInput(currentSessionId.value!, userInput.value)
  userInput.value = ''
  isWaitingForInput.value = false
}

async function downloadFiles() {
  // TODO: 调用下载 API
}

async function cleanup() {
  if (currentSessionId.value) {
    await terminalWsService.disconnect(currentSessionId.value)
    currentSessionId.value = null
  }
}
</script>
```

### 2.2 终端显示组件

**新文件**: `frontend/src/components/TerminalDisplay.vue`

```vue
<template>
  <div class="terminal-display" ref="containerRef">
    <div class="terminal-output" ref="outputRef"></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'

const props = defineProps<{
  sessionId: string | null
}>()

const emit = defineEmits<{
  input: [data: string]
}>()

const containerRef = ref<HTMLElement | null>(null)
const outputRef = ref<HTMLElement | null>(null)
let terminal: Terminal | null = null
let fitAddon: FitAddon | null = null

onMounted(() => {
  terminal = new Terminal({
    theme: { background: '#1e1e1e' },
    cursorBlink: true,
  })
  
  fitAddon = new FitAddon()
  terminal.loadAddon(fitAddon)
  
  terminal.open(outputRef.value!)
  fitAddon.fit()
  
  // 用户输入
  terminal.onData((data) => {
    emit('input', data)
  })
})

onUnmounted(() => {
  terminal?.dispose()
})

function write(data: string) {
  terminal?.write(data)
}

function clear() {
  terminal?.clear()
}

defineExpose({ write, clear })
</script>

<style scoped>
.terminal-display {
  background: #1e1e1e;
  border-radius: 8px;
  overflow: hidden;
}

.terminal-output {
  padding: 12px;
  min-height: 400px;
}
</style>
```

### 2.3 WebSocket 服务

**新文件**: `frontend/src/services/terminalWs.ts`

```typescript
type MessageHandler = (msg: StreamMessage) => void

interface StreamMessage {
  type: 'output' | 'error' | 'status' | 'waiting_input' | 'complete'
  session_id: string
  data: string
  timestamp: string
}

class TerminalWsService {
  private ws: WebSocket | null = null
  private sessionId: string | null = null
  private handlers: MessageHandler[] = []
  private reconnectAttempts = 0
  private maxReconnectAttempts = 5

  async connect(provider: string): Promise<string> {
    // 创建会话
    const response = await fetch('/api/code-commander/sessions', {
      method: 'POST',
      body: JSON.stringify({ provider }),
    })
    const { session_id } = await response.json()
    
    // 建立 WebSocket
    this.ws = new WebSocket(`ws://localhost:8000/ws/terminal/${session_id}`)
    
    this.ws.onmessage = (event) => {
      const msg: StreamMessage = JSON.parse(event.data)
      this.handlers.forEach((h) => h(msg))
    }
    
    this.ws.onclose = () => {
      this.attemptReconnect()
    }
    
    this.sessionId = session_id
    return session_id
  }

  async sendTask(sessionId: string, prompt: string) {
    await fetch(`/api/code-commander/sessions/${sessionId}/task`, {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    })
  }

  sendInput(sessionId: string, input: string) {
    this.ws?.send(JSON.stringify({ type: 'input', data: input }))
  }

  onMessage(handler: MessageHandler) {
    this.handlers.push(handler)
  }

  removeHandler(handler: MessageHandler) {
    this.handlers = this.handlers.filter((h) => h !== handler)
  }

  async disconnect(sessionId: string) {
    await fetch(`/api/code-commander/sessions/${sessionId}`, {
      method: 'DELETE',
    })
    this.ws?.close()
    this.ws = null
    this.sessionId = null
  }

  private async attemptReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      return
    }
    this.reconnectAttempts++
    await new Promise((r) => setTimeout(r, 1000 * this.reconnectAttempts))
    // 重新连接
  }
}

export const terminalWsService = new TerminalWsService()
```

### 2.4 路由配置

**文件**: `frontend/src/router/index.ts`

```typescript
{
  path: '/code-commander',
  name: 'CodeCommander',
  component: () => import('@/pages/chat/CodeCommander.vue'),
}
```

---

## 3. 核心文件清单

| 文件 | 操作 | 说明 |
|------|------|------|
| `CodeCommander.vue` | 新增 | 主页面组件 |
| `TerminalDisplay.vue` | 新增 | 终端显示组件（xterm.js） |
| `terminalWs.ts` | 新增 | WebSocket 服务 |
| `router/index.ts` | 修改 | 新增路由 |

---

## 4. 验收标准

- [ ] 用户可以选择 AI 提供商
- [ ] 可以输入任务描述并执行
- [ ] 终端实时显示 AI 输出
- [ ] 用户可以输入交互（如 "y"）
- [ ] 可以下载和清理文件

---

## 5. 依赖

| 依赖 | 版本 | 用途 |
|------|------|------|
| xterm | ^5.x | 终端渲染 |
| xterm-addon-fit | ^0.8.x | 自适应大小 |

---

## 6. 依赖关系

```
Phase 4（流式交互）
       ↓
本阶段（前端集成）→ 端到端测试
```
