# 设备 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