docs: add gui cli capability implementation plan

This commit is contained in:
kris
2026-04-06 09:04:19 +08:00
parent 9de4fb7d40
commit d04eca4703

View File

@@ -0,0 +1,859 @@
# 设备 GUI+CLI 双能力接入与并行冲突控制 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让 Boss 支持同一台 Mac/Windows 设备同时接入 Codex GUI 与 CLI并在同项目 GUI/CLI 并行写入风险出现时默认阻断,由用户在当前异常项目/文件夹级别选择禁止、允许本次或永久放行。
**Architecture:** 继续沿用现有单设备模型,不拆成两个虚拟设备。设备层新增 `gui/cli` 双能力状态和默认执行模式;项目/文件夹层新增并行冲突状态与放行策略;执行链在进入 CLI 写入任务前先做规则层冲突检测,命中后返回结构化风险卡,等待用户确认。
**Tech Stack:** Next.js App Router、TypeScript、文件型状态存储 `data/boss-state.json`、现有 `local-agent` heartbeat 链路、Android 原生客户端、Node test runner、Gradle unit tests
---
## 文件结构
### 状态模型与执行冲突检测
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
- Test: `/Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts`
- Test: `/Users/kris/code/boss/tests/device-execution-conflict.test.ts`
职责:
-`Device` 增加 `gui/cli` 能力状态
- 给项目/文件夹增加并行冲突策略与状态
- 在进入 CLI 写入型任务前做规则层冲突检测
-`禁止 / 允许本次 / 永久放行` 限定在当前异常项目/文件夹
### 设备详情与设备 API
- Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts`
- Modify: `/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts`
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
职责:
- 把设备的 GUI/CLI 双能力与默认执行模式暴露给 Web/Android
- 允许设备详情页更新 `preferredExecutionMode`
### 本地 agent 心跳能力上报
- Modify: `/Users/kris/code/boss/local-agent/server.mjs`
- Modify: `/Users/kris/code/boss/local-agent/config.example.json`
- Test: `/Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs`
职责:
- heartbeat 上报当前设备的 `gui/cli` 能力状态
- 不改变现有 `local-agent -> codex exec resume` 主链
### Android 设备详情与冲突提示
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java`
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java`
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java`
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java`
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java`
职责:
- 在设备详情页显示 `GUI / CLI` 状态
- 增加默认执行模式切换
- 展示当前项目/文件夹级冲突状态和放行策略
### Web 设备详情与冲突提示
- Modify: `/Users/kris/code/boss/src/components/app-ui.tsx`
- Modify: `/Users/kris/code/boss/src/app/devices/page.tsx`
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
职责:
- Web 端设备详情显示双能力与默认执行模式
- 当前异常项目/文件夹的冲突卡三动作可操作
### 文档与回归
- Modify: `/Users/kris/code/boss/README.md`
- Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md`
职责:
- 记录 GUI/CLI 双能力设备模型
- 记录项目/文件夹级冲突控制行为
---
### Task 1: 先定义设备双能力与项目级冲突状态
**Files:**
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
- Test: `/Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts`
- [ ] **Step 1: 写失败测试,锁住设备双能力默认值与局部冲突策略**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { readState, writeStateForTests } from "@/lib/boss-data";
test("device stores gui and cli capabilities without splitting the physical device", async () => {
const state = await readState();
const device = state.devices.find((item) => item.id === "mac-studio");
assert.ok(device);
assert.equal(device.capabilities?.gui?.connected, true);
assert.equal(device.capabilities?.cli?.connected, true);
assert.equal(device.preferredExecutionMode, "cli");
});
test("conflict policy is scoped to the active folder instead of the whole device", async () => {
const state = await readState();
state.projectExecutionPolicies = [
{
deviceId: "mac-studio",
folderKey: "mac-studio:boss",
projectId: "thread-ui",
allowPolicy: "allow_always",
conflictState: "warning",
updatedAt: "2026-04-06T10:00:00.000Z",
},
];
await writeStateForTests(state);
const nextState = await readState();
const bossPolicy = nextState.projectExecutionPolicies.find((item) => item.folderKey === "mac-studio:boss");
const otherPolicy = nextState.projectExecutionPolicies.find((item) => item.folderKey === "mac-studio:talking");
assert.equal(bossPolicy?.allowPolicy, "allow_always");
assert.equal(otherPolicy, undefined);
});
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
```
Expected:
- FAIL提示 `Device` 没有 `capabilities``preferredExecutionMode`
- [ ] **Step 3: 在状态模型里增加设备能力与冲突策略类型**
`/Users/kris/code/boss/src/lib/boss-data.ts` 增加最小实现:
```ts
export type DeviceExecutionMode = "gui" | "cli";
export type ProjectConflictAllowPolicy = "forbid" | "allow_once" | "allow_always";
export type ProjectConflictState = "none" | "warning" | "blocked";
export interface DeviceCapabilityState {
connected: boolean;
lastSeenAt?: string;
lastActiveProjectId?: string;
}
export interface DeviceCapabilities {
gui: DeviceCapabilityState;
cli: DeviceCapabilityState;
}
export interface ProjectExecutionPolicy {
deviceId: string;
folderKey?: string;
projectId: string;
allowPolicy: ProjectConflictAllowPolicy;
conflictState: ProjectConflictState;
activeCliExecution?: boolean;
recentExternalActivityAt?: string;
updatedAt: string;
}
export interface Device {
id: string;
name: string;
avatar: string;
account: string;
source: DeviceSource;
status: DeviceStatus;
projects: string[];
quota5h: number;
quota7d: number;
lastSeenAt: string;
endpoint?: string;
token?: string;
note?: string;
capabilities?: DeviceCapabilities;
preferredExecutionMode?: DeviceExecutionMode;
}
```
- [ ] **Step 4: 给初始种子设备补默认值**
`/Users/kris/code/boss/src/lib/boss-data.ts` 的种子设备里补:
```ts
capabilities: {
gui: { connected: true, lastSeenAt: "2026-04-06T09:00:00+08:00", lastActiveProjectId: "master-agent" },
cli: { connected: true, lastSeenAt: "2026-04-06T09:00:00+08:00", lastActiveProjectId: "master-agent" },
},
preferredExecutionMode: "cli",
```
Windows demo 设备补:
```ts
capabilities: {
gui: { connected: true, lastSeenAt: "2026-04-06T08:50:00+08:00", lastActiveProjectId: "audit-collab" },
cli: { connected: false, lastSeenAt: "2026-04-06T08:40:00+08:00", lastActiveProjectId: "" },
},
preferredExecutionMode: "gui",
```
- [ ] **Step 5: 给状态恢复逻辑补兼容默认值**
`readState()` 的 normalize 阶段补:
```ts
capabilities: {
gui: {
connected: Boolean(device.capabilities?.gui?.connected),
lastSeenAt: device.capabilities?.gui?.lastSeenAt || device.lastSeenAt,
lastActiveProjectId: device.capabilities?.gui?.lastActiveProjectId || "",
},
cli: {
connected: Boolean(device.capabilities?.cli?.connected),
lastSeenAt: device.capabilities?.cli?.lastSeenAt || device.lastSeenAt,
lastActiveProjectId: device.capabilities?.cli?.lastActiveProjectId || "",
},
},
preferredExecutionMode:
device.preferredExecutionMode === "gui" || device.preferredExecutionMode === "cli"
? device.preferredExecutionMode
: "cli",
```
- [ ] **Step 6: 重跑测试,确认通过**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
```
Expected:
- PASS
- [ ] **Step 7: Commit**
```bash
git add /Users/kris/code/boss/src/lib/boss-data.ts /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
git commit -m "feat: add gui cli device capability state"
```
### Task 2: 在规则层实现项目/文件夹级并行冲突检测
**Files:**
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
- Test: `/Users/kris/code/boss/tests/device-execution-conflict.test.ts`
- [ ] **Step 1: 写失败测试,锁住默认阻断、允许本次、永久放行都只作用于当前 folder**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import {
applyProjectConflictDecision,
detectProjectExecutionConflict,
readState,
writeStateForTests,
} from "@/lib/boss-data";
test("detectProjectExecutionConflict blocks cli execution when the same folder has new external activity", async () => {
const state = await readState();
state.projectExecutionPolicies = [];
await writeStateForTests(state);
const result = await detectProjectExecutionConflict({
deviceId: "mac-studio",
folderKey: "mac-studio:boss",
projectId: "thread-ui",
executionMode: "cli",
activityAt: "2026-04-06T10:05:00.000Z",
externalActivityAt: "2026-04-06T10:04:00.000Z",
});
assert.equal(result.blocked, true);
assert.equal(result.policy.allowPolicy, "forbid");
});
test("allow_once only clears the active folder conflict after a single execution", async () => {
await applyProjectConflictDecision({
deviceId: "mac-studio",
folderKey: "mac-studio:boss",
projectId: "thread-ui",
decision: "allow_once",
});
let result = await detectProjectExecutionConflict({
deviceId: "mac-studio",
folderKey: "mac-studio:boss",
projectId: "thread-ui",
executionMode: "cli",
activityAt: "2026-04-06T10:10:00.000Z",
externalActivityAt: "2026-04-06T10:09:00.000Z",
});
assert.equal(result.blocked, false);
result = await detectProjectExecutionConflict({
deviceId: "mac-studio",
folderKey: "mac-studio:boss",
projectId: "thread-ui",
executionMode: "cli",
activityAt: "2026-04-06T10:20:00.000Z",
externalActivityAt: "2026-04-06T10:19:00.000Z",
});
assert.equal(result.blocked, true);
});
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-execution-conflict.test.ts
```
Expected:
- FAIL提示函数不存在或策略未生效
- [ ] **Step 3: 增加冲突检测与决策函数**
`/Users/kris/code/boss/src/lib/boss-data.ts` 增加:
```ts
export async function detectProjectExecutionConflict(input: {
deviceId: string;
folderKey?: string;
projectId: string;
executionMode: DeviceExecutionMode;
activityAt: string;
externalActivityAt?: string;
}) {
const state = await readState();
const policy = matchProjectExecutionPolicy(state, input);
const hasConflict =
input.executionMode === "cli" &&
Boolean(input.externalActivityAt) &&
input.externalActivityAt! <= input.activityAt;
if (!hasConflict) {
return { blocked: false, policy };
}
if (policy?.allowPolicy === "allow_always") {
return { blocked: false, policy };
}
if (policy?.allowPolicy === "allow_once") {
await clearAllowOncePolicy(input);
return { blocked: false, policy };
}
const nextPolicy = await upsertProjectExecutionPolicy({
deviceId: input.deviceId,
folderKey: input.folderKey,
projectId: input.projectId,
allowPolicy: "forbid",
conflictState: "blocked",
activeCliExecution: true,
recentExternalActivityAt: input.externalActivityAt,
});
return { blocked: true, policy: nextPolicy };
}
export async function applyProjectConflictDecision(input: {
deviceId: string;
folderKey?: string;
projectId: string;
decision: ProjectConflictAllowPolicy;
}) {
return upsertProjectExecutionPolicy({
deviceId: input.deviceId,
folderKey: input.folderKey,
projectId: input.projectId,
allowPolicy: input.decision,
conflictState: input.decision === "forbid" ? "blocked" : "warning",
});
}
```
- [ ] **Step 4: 确保匹配范围只落在当前异常项目/文件夹**
匹配函数必须先按:
```ts
deviceId + folderKey
```
再退化到:
```ts
deviceId + projectId
```
不得在设备级匹配整个设备的默认策略。
- [ ] **Step 5: 重跑测试,确认通过**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-execution-conflict.test.ts
```
Expected:
- PASS
- [ ] **Step 6: Commit**
```bash
git add /Users/kris/code/boss/src/lib/boss-data.ts /Users/kris/code/boss/tests/device-execution-conflict.test.ts
git commit -m "feat: add folder scoped gui cli conflict guard"
```
### Task 3: 让设备详情 API 与投影视图暴露 GUI/CLI 双能力
**Files:**
- Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts`
- Modify: `/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts`
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
- [ ] **Step 1: 写失败测试,锁住设备详情返回 GUI/CLI 能力与默认执行模式**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { GET, PATCH } from "@/app/api/v1/devices/[deviceId]/route";
test("device detail exposes gui cli capabilities and preferred mode", async () => {
const response = await GET(new Request("http://localhost/api/v1/devices/mac-studio"), {
params: Promise.resolve({ deviceId: "mac-studio" }),
});
const data = await response.json();
const device = data.workspace.selectedDevice;
assert.equal(device.capabilities.gui.connected, true);
assert.equal(device.capabilities.cli.connected, true);
assert.equal(device.preferredExecutionMode, "cli");
});
test("device detail patch updates preferred execution mode only", async () => {
const response = await PATCH(
new Request("http://localhost/api/v1/devices/mac-studio", {
method: "PATCH",
body: JSON.stringify({ preferredExecutionMode: "gui" }),
headers: { "Content-Type": "application/json" },
}),
{ params: Promise.resolve({ deviceId: "mac-studio" }) },
);
const data = await response.json();
assert.equal(data.device.preferredExecutionMode, "gui");
});
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
```
Expected:
- FAIL提示返回字段缺失
- [ ] **Step 3: 在投影视图里暴露 GUI/CLI 状态**
`/Users/kris/code/boss/src/lib/boss-projections.ts``DeviceWorkspaceView` 构造里把:
```ts
selectedDevice: {
...device,
capabilities: device.capabilities,
preferredExecutionMode: device.preferredExecutionMode ?? "cli",
},
```
稳定输出。
- [ ] **Step 4: 在设备 PATCH 路由里支持只更新默认执行模式**
`/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts` 收窄允许字段:
```ts
const body = await request.json();
const payload = {
preferredExecutionMode:
body.preferredExecutionMode === "gui" || body.preferredExecutionMode === "cli"
? body.preferredExecutionMode
: undefined,
};
const device = await updateDevice(deviceId, payload);
```
- [ ] **Step 5: 重跑测试,确认通过**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
```
Expected:
- PASS
- [ ] **Step 6: Commit**
```bash
git add /Users/kris/code/boss/src/lib/boss-projections.ts /Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
git commit -m "feat: expose gui cli device capability details"
```
### Task 4: 让 local-agent heartbeat 上报 CLI 能力
**Files:**
- Modify: `/Users/kris/code/boss/local-agent/server.mjs`
- Modify: `/Users/kris/code/boss/local-agent/config.example.json`
- Test: `/Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs`
- [ ] **Step 1: 写失败测试,锁住 heartbeat 载荷包含 `capabilities.cli`**
```js
import test from "node:test";
import assert from "node:assert/strict";
import { buildHeartbeatPayload } from "../local-agent/server.mjs";
test("heartbeat payload includes cli capability state", async () => {
const payload = await buildHeartbeatPayload({
deviceId: "mac-studio",
name: "Mac Studio",
cliConnected: true,
guiConnected: false,
});
assert.equal(payload.device.capabilities.cli.connected, true);
assert.equal(payload.device.capabilities.gui.connected, false);
});
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
node --test /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
```
Expected:
- FAIL提示 heartbeat payload 未包含该字段
- [ ] **Step 3: 在 heartbeat 载荷里新增双能力状态**
`/Users/kris/code/boss/local-agent/server.mjs` 的 heartbeat payload 里补:
```js
capabilities: {
gui: {
connected: Boolean(config.guiConnected),
lastSeenAt: now,
lastActiveProjectId: "",
},
cli: {
connected: true,
lastSeenAt: now,
lastActiveProjectId: "",
},
},
preferredExecutionMode: config.preferredExecutionMode || "cli",
```
- [ ] **Step 4: 在示例配置中补默认模式**
`/Users/kris/code/boss/local-agent/config.example.json` 补:
```json
"preferredExecutionMode": "cli",
"guiConnected": false
```
- [ ] **Step 5: 重跑测试,确认通过**
Run:
```bash
node --test /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
```
Expected:
- PASS
- [ ] **Step 6: Commit**
```bash
git add /Users/kris/code/boss/local-agent/server.mjs /Users/kris/code/boss/local-agent/config.example.json /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
git commit -m "feat: report gui cli capabilities in heartbeat"
```
### Task 5: Android 设备详情页显示 GUI/CLI 双能力并支持默认模式切换
**Files:**
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java`
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java`
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java`
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java`
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java`
- [ ] **Step 1: 写失败测试,锁住设备详情显示 GUI/CLI 状态与默认模式**
```java
@Test
public void renderDeviceShowsGuiCliCapabilitiesAndPreferredMode() {
TestDeviceDetailActivity activity = Robolectric
.buildActivity(TestDeviceDetailActivity.class, new Intent()
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio"))
.setup()
.get();
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "GUI"));
assertTrue(viewTreeContainsText(content, "CLI"));
assertTrue(viewTreeContainsText(content, "默认执行模式"));
assertTrue(viewTreeContainsText(content, "CLI"));
}
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --no-daemon
```
Expected:
- FAIL提示页面文本不存在
- [ ] **Step 3: 给 Android client 增加更新默认模式接口**
`/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java` 加:
```java
public ApiResponse updateDeviceExecutionMode(String deviceId, String mode) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("preferredExecutionMode", mode);
return patch("/api/v1/devices/" + encodePathSegment(deviceId), payload);
}
```
- [ ] **Step 4: 在设备详情页新增能力状态与模式切换**
`/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java``renderDevice(...)` 中追加:
```java
JSONObject capabilities = device.optJSONObject("capabilities");
JSONObject gui = capabilities == null ? null : capabilities.optJSONObject("gui");
JSONObject cli = capabilities == null ? null : capabilities.optJSONObject("cli");
String preferredExecutionMode = device.optString("preferredExecutionMode", "cli");
appendContent(BossUi.buildWechatMenuRow(
this,
"GUI",
gui != null && gui.optBoolean("connected", false) ? "已连接" : "未连接",
null,
null,
null
));
appendContent(BossUi.buildWechatMenuRow(
this,
"CLI",
cli != null && cli.optBoolean("connected", false) ? "已连接" : "未连接",
null,
null,
null
));
appendContent(BossUi.buildMenuRow(
this,
"默认执行模式",
"当前:" + ("gui".equals(preferredExecutionMode) ? "GUI" : "CLI"),
null,
v -> openExecutionModeDialog(preferredExecutionMode)
));
```
- [ ] **Step 5: 实现切换弹窗和保存**
`openExecutionModeDialog(...)` 里:
```java
String[] items = new String[] { "GUI", "CLI" };
new AlertDialog.Builder(this)
.setTitle("默认执行模式")
.setSingleChoiceItems(items, "gui".equals(currentMode) ? 0 : 1, null)
.setNegativeButton("取消", null)
.setPositiveButton("保存", (dialog, which) -> saveExecutionMode(selectedMode))
.show();
```
- [ ] **Step 6: 重跑测试,确认通过**
Run:
```bash
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --tests com.hyzq.boss.BossApiClientDeviceModeTest --no-daemon
```
Expected:
- PASS
- [ ] **Step 7: Commit**
```bash
git add /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java
git commit -m "feat: surface gui cli capabilities on android devices"
```
### Task 6: Web 设备详情页显示双能力并支持冲突策略
**Files:**
- Modify: `/Users/kris/code/boss/src/components/app-ui.tsx`
- Modify: `/Users/kris/code/boss/src/app/devices/page.tsx`
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
- [ ] **Step 1: 写失败测试,锁住设备详情展示 GUI/CLI 与默认模式**
```ts
test("device page renders gui cli capability badges and preferred mode", async () => {
const view = getDeviceWorkspaceView(await readState(), "mac-studio");
assert.equal(view.selectedDevice?.capabilities?.gui.connected, true);
assert.equal(view.selectedDevice?.capabilities?.cli.connected, true);
assert.equal(view.selectedDevice?.preferredExecutionMode, "cli");
});
```
- [ ] **Step 2: 跑测试,确认先失败**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
```
Expected:
- FAIL
- [ ] **Step 3: 在 Web 设备详情区域渲染能力与默认模式**
`/Users/kris/code/boss/src/components/app-ui.tsx` 的设备详情块加入:
```tsx
<div className="text-xs text-zinc-500">
GUI{device.capabilities?.gui?.connected ? "已连接" : "未连接"} · CLI
{device.capabilities?.cli?.connected ? "已连接" : "未连接"}
</div>
<button onClick={() => openDeviceExecutionMode(device.id)}>
默认执行模式:{device.preferredExecutionMode === "gui" ? "GUI" : "CLI"}
</button>
```
- [ ] **Step 4: 为冲突卡预留当前项目/文件夹级入口**
在同一区块加入:
```tsx
{policy ? (
<div>
<div>当前项目并行策略:{policy.allowPolicy}</div>
<button>禁止</button>
<button>允许本次</button>
<button>永久放行</button>
</div>
) : null}
```
先把数据通道和入口铺上,不在这一任务里完成所有交互。
- [ ] **Step 5: 重跑测试,确认通过**
Run:
```bash
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
```
Expected:
- PASS
- [ ] **Step 6: Commit**
```bash
git add /Users/kris/code/boss/src/components/app-ui.tsx /Users/kris/code/boss/src/app/devices/page.tsx /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
git commit -m "feat: show gui cli capability state on web devices"
```
### Task 7: 完整验证并同步文档
**Files:**
- Modify: `/Users/kris/code/boss/README.md`
- Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md`
- [ ] **Step 1: 跑服务端与 Android 关键回归**
Run:
```bash
cd /Users/kris/code/boss && npx --yes tsx --test tests/device-gui-cli-capabilities.test.ts tests/device-execution-conflict.test.ts tests/device-detail-capabilities-route.test.ts
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --tests com.hyzq.boss.BossApiClientDeviceModeTest --no-daemon
```
Expected:
- PASS
- [ ] **Step 2: 跑 lint / build / release**
Run:
```bash
cd /Users/kris/code/boss && npm run lint
cd /Users/kris/code/boss && npm run build
cd /Users/kris/code/boss/android && ./gradlew assembleRelease --no-daemon
```
Expected:
- 全部 PASS
- [ ] **Step 3: 更新文档**
`/Users/kris/code/boss/README.md``/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md` 增加:
```md
- 当前设备模型已支持同一台设备同时接入 Codex GUI 与 CLI
- 设备详情页可查看 GUI / CLI 状态,并切换默认执行模式
- 同项目 GUI/CLI 并行写入风险默认阻断,用户可在当前异常项目/文件夹级别选择 禁止 / 允许本次 / 永久放行
```
- [ ] **Step 4: 提交文档与收口代码**
```bash
git add /Users/kris/code/boss/README.md /Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md
git commit -m "docs: describe gui cli device capability workflow"
```
- [ ] **Step 5: 推分支并部署**
Run:
```bash
cd /Users/kris/code/boss && git push gitea codex/wechat-native-ui-rollback
cd /Users/kris/code/boss && ./scripts/deploy-server.sh
```
Expected:
- 分支推送成功
- 服务器健康检查正常