diff --git a/apps/web-console/assistant-chat.css b/apps/web-console/assistant-chat.css
index 6f71716..2fe2ad0 100644
--- a/apps/web-console/assistant-chat.css
+++ b/apps/web-console/assistant-chat.css
@@ -1760,6 +1760,17 @@
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
}
+.assistant-dispatched-controls-list[hidden] { display: none; }
+.assistant-dispatched-controls-footer:empty { display: none; }
+.assistant-dispatched-controls .assistant-mode-draft-inline {
+ width: 100%;
+ max-width: none;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ box-shadow: none;
+}
+
.assistant-dispatched-controls-list > li {
min-height: 55px;
display: grid;
diff --git a/apps/web-console/assistant-chat.js b/apps/web-console/assistant-chat.js
index cd372d4..56d2639 100644
--- a/apps/web-console/assistant-chat.js
+++ b/apps/web-console/assistant-chat.js
@@ -553,6 +553,17 @@
function assistantControlCategory(operation = {}, fallback = "控制指令") {
const capability = String(operation.capability || "");
+ if (capability.startsWith("window.")) {
+ const target = String(operation.target?.zone || operation.target?.instance
+ || operation.target_scope || (typeof operation.target === "string" ? operation.target : ""))
+ .replaceAll("-", "_");
+ return {
+ front_left: "左前车窗(主驾)", driver: "左前车窗(主驾)",
+ front_right: "右前车窗(副驾)", passenger: "右前车窗(副驾)",
+ rear_left: "左后车窗", rear_right: "右后车窗",
+ all: "全部车窗", front: "前排车窗", rear: "后排车窗",
+ }[target] || "车窗(位置待确认)";
+ }
if (capability === "infrared.emit") {
const deviceName = String(
operation.target?.name || operation.target?.device || "红外外设",
@@ -673,8 +684,30 @@
commands = [];
}
if (!commands.length) commands = fallbackAssistantControlCommands(result);
+ if (result.mode_profile_draft && Array.isArray(result.actions)) {
+ const operationSignature = (operation = {}) => {
+ const rawTarget = String(operation.target?.instance || operation.target?.zone
+ || operation.target?.device || operation.target_scope
+ || (typeof operation.target === "string" ? operation.target : ""))
+ .replaceAll("-", "_");
+ const target = ({ passenger: "front_right", driver: "front_left" })[rawTarget] || rawTarget;
+ const parameters = operation.parameters || {};
+ return JSON.stringify([operation.capability, target,
+ Object.keys(parameters).sort().map((key) => [key, parameters[key]])]);
+ };
+ const presentationByAction = new Map(projections.map((presentation) => [
+ operationSignature(presentation.operation), presentation,
+ ]));
+ commands = result.actions.filter((action) => action && typeof action === "object")
+ .map((operation, index) => ({
+ operation, operationRef: `draft-action-${index}`,
+ semantic: String(operation.capability || ""),
+ command: String(presentationByAction.get(operationSignature(operation))?.actual_command || ""),
+ }));
+ }
return commands.map((item, index) => {
- const operation = projectionByRef.get(String(item.operationRef || ""))
+ const operation = item.operation
+ || projectionByRef.get(String(item.operationRef || ""))
|| operationByRef.get(String(item.operationRef || ""))
|| projections[index]?.operation
|| {};
@@ -737,7 +770,13 @@
const temperature = numericValue(parameters.temperature_c);
if (temperature !== null) return `${temperature}°C`;
const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct);
- if (position !== null) return `${position}%`;
+ if (position !== null) {
+ if (String(item.operation?.capability || "").startsWith("window.")) {
+ return position === 0 ? "0%(完全关闭)"
+ : position === 100 ? "100%(完全打开)" : `${position}%`;
+ }
+ return `${position}%`;
+ }
const illumination = String(parameters.color_hex || "").trim();
const brightnessPercent = numericValue(parameters.brightness_pct);
if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`;
@@ -816,17 +855,20 @@
if (!commands.length) return;
const previewOnly = String(mergedResult.execution_mode || "dry_run") !== "hardware"
|| mergedResult.orchestration_route === "mode_profile_create_preview";
- const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令";
+ const headingText = mergedResult.mode_profile_draft?.name
+ ? `${mergedResult.mode_profile_draft.name}(未下发)`
+ : previewOnly ? "控制指令预览(未下发)" : "下发控制指令";
- const signature = JSON.stringify(commands.map((item) => [
+ const signature = JSON.stringify([headingText, commands.map((item) => [
item.operationRef,
item.category,
item.command,
item.semantic,
+ assistantControlValue(item),
item.deliveryState,
item.deliveryLabel,
item.deliveryDetail,
- ]));
+ ])]);
const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls;
if (typeof vueRenderer === "function") {
let host = $(".assistant-dispatched-controls-vue-host", message.body);
@@ -1114,8 +1156,8 @@
all: "全部车窗",
front: "前排车窗",
rear: "后排车窗",
- front_left: "主驾车窗",
- front_right: "副驾车窗",
+ front_left: "左前车窗(主驾)",
+ front_right: "右前车窗(副驾)",
rear_left: "左后车窗",
rear_right: "右后车窗",
};
@@ -1383,27 +1425,15 @@
rememberModeDraftContext(draft);
supersedeModeDraftCards(draft);
const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`;
- if ($(selector, message.body)) return;
+ if ($(selector, message.body)
+ || message.modeDraftCard?.dataset.assistantModeDraft === String(draft.source_request_id)) return;
const card = document.createElement("section");
- card.className = "assistant-mode-draft";
+ card.className = "assistant-mode-draft assistant-mode-draft-inline";
card.dataset.assistantModeDraft = String(draft.source_request_id);
card.dataset.assistantModeName = normalizeModeName(draft.name);
message.article?.classList.add("assistant-message-mode-draft");
-
- const heading = document.createElement("header");
- const headingCopy = document.createElement("span");
- const eyebrow = document.createElement("small");
- eyebrow.textContent = draft.existing_mode_id
- ? "已载入现有功能模式"
- : "已为你生成功能模式(未下发)";
- const title = document.createElement("strong");
- title.textContent = `「${draft.name}」`;
- headingCopy.append(eyebrow, title);
- const badge = document.createElement("span");
- badge.className = "assistant-mode-draft-badge";
+ message.modeDraftCard = card;
const scheduleView = modeDraftScheduleView(draft);
- badge.textContent = `${scheduleView?.operationCount ?? (Number(draft.operation_count) || 0)} 项动作`;
- heading.append(headingCopy, badge);
const list = document.createElement("div");
list.className = "assistant-mode-draft-list";
@@ -1431,12 +1461,10 @@
hint.className = "assistant-mode-schedule-hint";
hint.textContent = scheduleView.stopHint;
list.append(hint);
- } else {
- appendModeDraftRows(list, modeDraftRows(draft, actions));
}
const previewNotice = document.createElement("p");
previewNotice.className = "assistant-mode-draft-preview-notice";
- previewNotice.textContent = "创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。";
+ previewNotice.textContent = `${draft.existing_mode_id ? "已载入现有功能模式。" : ""}创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。`;
list.append(previewNotice);
const controls = document.createElement("div");
@@ -1464,8 +1492,22 @@
void tryModeProfileDraft(card, draft, actions, buttons, status);
});
- card.append(heading, list, controls, status);
- message.body.append(card);
+ card.append(list, controls, status);
+ appendDispatchedControls(message, {
+ mode_profile_draft: draft, actions, execution_mode: "dry_run",
+ orchestration_route: "mode_profile_create_preview",
+ });
+ const host = $(".assistant-dispatched-controls-vue-host", message.body);
+ const attached = host && window.CognitiveAgentVueUI?.setDispatchedControlsFooter?.(
+ host, card, { hideCommands: Boolean(scheduleView) },
+ );
+ if (!attached) {
+ const panel = $(".assistant-dispatched-controls", message.body);
+ if (!panel) return;
+ const commandList = $(".assistant-dispatched-controls-list", panel);
+ if (commandList) commandList.hidden = Boolean(scheduleView);
+ panel.append(card);
+ }
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
diff --git a/apps/web-console/index.html b/apps/web-console/index.html
index ce87cb3..1a8a1d7 100755
--- a/apps/web-console/index.html
+++ b/apps/web-console/index.html
@@ -45,7 +45,7 @@
-
+
diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js
index 5c8fce4..573bdfe 100644
--- a/apps/web-console/service-worker.js
+++ b/apps/web-console/service-worker.js
@@ -1,4 +1,4 @@
-const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows";
+const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -16,13 +16,13 @@ const APP_SHELL = [
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
- "/assistant-chat.css?v=codex-assistant-30",
- "/assistant-chat.js?v=codex-assistant-40-seat-rows",
+ "/assistant-chat.css?v=codex-assistant-31-unified-preview",
+ "/assistant-chat.js?v=codex-assistant-41-unified-preview",
"/energy-monitor.css?v=energy-monitor-5-dialog-size",
"/energy-monitor.js?v=energy-monitor-5-performance",
"/ui-consistency.css?v=spatial-ui-8",
"/codepen-glass-theme.css?v=creative-cloud-6-performance",
- "/vue-ui.js?v=vue-presentation-4",
+ "/vue-ui.js?v=vue-presentation-5-draft-footer",
"/firmware-workbench-entry.css?v=2",
"/firmware-workbench-entry.js?v=3",
"/firmware-workbench.html",
diff --git a/apps/web-console/vue-ui.js b/apps/web-console/vue-ui.js
index 1c12813..dfe20c1 100644
--- a/apps/web-console/vue-ui.js
+++ b/apps/web-console/vue-ui.js
@@ -111,7 +111,7 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
}
function createCommandMount(container) {
- const state = reactive({ commands: [], open: true, heading: "下发控制指令" });
+ const state = reactive({ commands: [], open: true, heading: "下发控制指令", footer: null, hideCommands: false });
const app = createApp({
name: "N7DispatchedControlsCard",
setup() {
@@ -138,7 +138,16 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
"data-lucide": "chevron-down",
}),
]),
- h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
+ h("ol", { class: "assistant-dispatched-controls-list", hidden: state.hideCommands }, state.commands.map(renderCommandRow)),
+ h("div", {
+ class: "assistant-dispatched-controls-footer",
+ hidden: !state.footer,
+ ref(element) {
+ if (element && state.footer && state.footer.parentNode !== element) {
+ element.replaceChildren(state.footer);
+ }
+ },
+ }),
]);
},
});
@@ -160,12 +169,22 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
return true;
}
+ function setDispatchedControlsFooter(container, footer, options = {}) {
+ const mount = commandMounts.get(container);
+ if (!mount || !(footer instanceof Element)) return false;
+ mount.state.footer = footer;
+ mount.state.hideCommands = Boolean(options.hideCommands);
+ refreshIcons();
+ return true;
+ }
+
document.documentElement.classList.add("vue-presentation-enabled");
document.documentElement.dataset.vueVersion = VUE_VERSION;
window.CognitiveAgentVueUI = Object.freeze({
available: true,
version: VUE_VERSION,
renderDispatchedControls,
+ setDispatchedControlsFooter,
renderModeChips,
});
}());
diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py
index a399233..01747fc 100755
--- a/tests/e2e/test_seat_web_api.py
+++ b/tests/e2e/test_seat_web_api.py
@@ -1071,7 +1071,7 @@ operations:
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
- f"{self.base_url}/assistant-chat.js?v=codex-assistant-40-seat-rows", timeout=3
+ f"{self.base_url}/assistant-chat.js?v=codex-assistant-41-unified-preview", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -1156,7 +1156,7 @@ operations:
self.assertIn('id="assistantTranscript"', template)
self.assertIn('import "../assistant-chat.css";', vite_entry)
self.assertIn(
- f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-40-seat-rows")}"',
+ f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-41-unified-preview")}"',
html,
)
self.assertIn('import "../energy-monitor.css";', vite_entry)
@@ -1472,13 +1472,13 @@ operations:
self.assertIn(f'/{runtime_assets[asset_name]}', service_worker)
else:
self.assertIn(
- 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"',
+ 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"',
service_worker,
)
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
self.assertIn('/console-v2.js?v=local-first-79-cinema-status', service_worker)
- self.assertIn('/assistant-chat.css?v=codex-assistant-30', service_worker)
- self.assertIn('/assistant-chat.js?v=codex-assistant-40-seat-rows', service_worker)
+ self.assertIn('/assistant-chat.css?v=codex-assistant-31-unified-preview', service_worker)
+ self.assertIn('/assistant-chat.js?v=codex-assistant-41-unified-preview', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-5-dialog-size', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', service_worker)
self.assertIn('"/app.js?v=dual-seat-148-seat-preset"', service_worker)
diff --git a/tests/unit/test_assistant_frontend_contract.py b/tests/unit/test_assistant_frontend_contract.py
index 8566814..fccab6c 100644
--- a/tests/unit/test_assistant_frontend_contract.py
+++ b/tests/unit/test_assistant_frontend_contract.py
@@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface():
assert 'placeholder="向 AI助手提问或交代工作"' in template
assert 'aria-label="发送消息"' in template
assert 'import "../assistant-chat.css";' in vite_entry
- assert 'src="/assistant-chat.js?v=codex-assistant-40-seat-rows"' in html
+ assert 'src="/assistant-chat.js?v=codex-assistant-41-unified-preview"' in html
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
assert (
- 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"'
+ 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"'
in service_worker
)
- assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker
- assert '"/assistant-chat.js?v=codex-assistant-40-seat-rows"' in service_worker
+ assert '"/assistant-chat.css?v=codex-assistant-31-unified-preview"' in service_worker
+ assert '"/assistant-chat.js?v=codex-assistant-41-unified-preview"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -91,7 +91,8 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
assert "window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result)" in script
assert "fallbackAssistantControlCommands(result)" in script
assert 'card.className = "assistant-dispatched-controls"' in script
- assert 'const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令"' in script
+ assert 'const headingText = mergedResult.mode_profile_draft?.name' in script
+ assert 'previewOnly ? "控制指令预览(未下发)" : "下发控制指令"' in script
assert 'count.className = "assistant-dispatched-controls-count"' in script
assert 'list.className = "assistant-dispatched-controls-list"' in script
assert "value.textContent = assistantControlValue(item)" in script
diff --git a/tests/unit/test_assistant_mode_draft_rows.py b/tests/unit/test_assistant_mode_draft_rows.py
index 9319ad2..ad042bc 100644
--- a/tests/unit/test_assistant_mode_draft_rows.py
+++ b/tests/unit/test_assistant_mode_draft_rows.py
@@ -204,7 +204,7 @@ def test_default_bundle_rows_show_each_passenger_action_separately(render_rows):
assert [(row["label"], row["value"]) for row in rows] == [
("空调电源", "开启"),
- ("主驾车窗", "完全关闭(目标开度 0%)"),
+ ("左前车窗(主驾)", "完全关闭(目标开度 0%)"),
("副驾座椅通风", "开启 · 1档"),
("副驾靠背位置", "45%"),
("副驾腿托位置", "35%"),
@@ -227,3 +227,55 @@ def test_cinema_draft_shows_all_four_axes_and_ventilation(render_rows):
("副驾腿托位置", "53%"),
("副驾座椅通风", "关闭"),
]
+
+
+def test_collapsible_preview_keeps_all_actions_and_identifies_each_window():
+ node = shutil.which("node")
+ assert node
+ source = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
+ start = source.index(" const assistantControlLabels =")
+ end = source.index(" function focusLatestDispatchedControls", start)
+ actions = [
+ action("seat.set_longitudinal_position", {"position_pct": 51}, "passenger"),
+ action("seat.set_cushion_tilt", {"position_pct": 30}, "passenger"),
+ action("seat.set_backrest_recline", {"position_pct": 44}, "passenger"),
+ action("seat.set_legrest_height", {"position_pct": 53}, "passenger"),
+ action("seat.set_ventilation", {"state": "off"}, "passenger"),
+ *(action("window.set_position", {"position_pct": value}, zone)
+ for zone, value in [("front_left", 0), ("front_right", 50),
+ ("rear_left", 100), ("rear_right", 0)]),
+ action("climate.set_power", {"state": "on"}, "vehicle"),
+ action("ambient_light.set_power", {"state": "on"}, "vehicle"),
+ ]
+ payload = {
+ "execution_mode": "dry_run", "mode_profile_draft": {"name": "浪漫影院模式"},
+ "actions": actions,
+ # IDs can collide across domains, and seat display projections may be absent.
+ "validated_control_commands": [{
+ "operation": {**operation, "id": "shared-operation-id"},
+ "actual_command": f"command-{index}",
+ } for index, operation in enumerate(actions[5:])],
+ }
+ script = "const window = {};\n" + source[start:end] + "\n" + r'''
+const payload = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
+const show = (value) => assistantDispatchedCommands(value).map(item => ({
+ label: item.category, value: assistantControlValue(item), command: item.command,
+}));
+const preview = show(payload);
+delete payload.validated_control_commands;
+console.log(JSON.stringify({preview, withoutProjections: show(payload)}));
+'''
+ output = subprocess.run([node, "-e", script], input=json.dumps(payload),
+ check=True, capture_output=True, text=True, encoding="utf-8")
+ results = json.loads(output.stdout)
+ for rows in (results["preview"], results["withoutProjections"]):
+ assert len(rows) == len(actions)
+ assert [row["value"] for row in rows[:5]] == ["51%", "30%", "44%", "53%", "关闭"]
+ assert [(row["label"], row["value"]) for row in rows[5:9]] == [
+ ("左前车窗(主驾)", "0%(完全关闭)"), ("右前车窗(副驾)", "50%"),
+ ("左后车窗", "100%(完全打开)"), ("右后车窗", "0%(完全关闭)"),
+ ]
+ assert [row["label"] for row in rows[-2:]] == ["空调电源", "氛围灯"]
+ assert [row["command"] for row in results["preview"][5:]] == [
+ f"command-{index}" for index in range(6)
+ ]
diff --git a/tests/unit/test_energy_monitor_frontend_contract.py b/tests/unit/test_energy_monitor_frontend_contract.py
index 3236d9d..3e1e717 100644
--- a/tests/unit/test_energy_monitor_frontend_contract.py
+++ b/tests/unit/test_energy_monitor_frontend_contract.py
@@ -37,11 +37,11 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
assert f'"{asset}"' in service_worker
assert 'import "../energy-monitor.css";' in vite_entry
assert "/energy-monitor.js?v=energy-monitor-5-performance" in html
- assert html.index("/assistant-chat.js?v=codex-assistant-40-seat-rows") < html.index(
+ assert html.index("/assistant-chat.js?v=codex-assistant-41-unified-preview") < html.index(
"/energy-monitor.js?v=energy-monitor-5-performance"
)
assert (
- 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"'
+ 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"'
in service_worker
)
diff --git a/tests/unit/test_frontend_performance_contract.py b/tests/unit/test_frontend_performance_contract.py
index 39bfa5e..04fd6a1 100644
--- a/tests/unit/test_frontend_performance_contract.py
+++ b/tests/unit/test_frontend_performance_contract.py
@@ -99,5 +99,5 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
):
assert asset in html
assert f'"{asset}"' in worker
- assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in worker
+ assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in worker
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
diff --git a/tests/unit/test_frontend_theme_contract.py b/tests/unit/test_frontend_theme_contract.py
index 09be8e6..5070234 100644
--- a/tests/unit/test_frontend_theme_contract.py
+++ b/tests/unit/test_frontend_theme_contract.py
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
assert source_asset in main
assert f'"{cached_asset}"' in service_worker
assert 'import "./styles/design-system.css"' in main
- assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker
+ assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
diff --git a/tests/unit/test_vue_frontend_contract.py b/tests/unit/test_vue_frontend_contract.py
index 1ef19fc..f64b3a2 100644
--- a/tests/unit/test_vue_frontend_contract.py
+++ b/tests/unit/test_vue_frontend_contract.py
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
assert package["scripts"]["build"] == "vite build"
assert 'import vue from "@vitejs/plugin-vue"' in vite
assert 'outDir: outputRoot' in vite
- assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker
+ assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker
assert 'createHash("sha256")' in vite
assert 'runtime-assets.json' in vite
assert 'rewriteRuntimeReferences' in vite