Files
boss/docs/superpowers/plans/2026-04-06-device-gui-cli-capability-and-conflict.md

860 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设备 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:
- 分支推送成功
- 服务器健康检查正常