Merge pull request #365 from pikasTech/fix/remove-resource-explorer-352

fix: remove resource explorer from workbench
This commit is contained in:
Lyon
2026-05-23 23:40:09 +08:00
committed by GitHub
7 changed files with 205 additions and 1355 deletions
+25 -64
View File
@@ -219,9 +219,9 @@ test("dedicated layout smoke CLI supports static build and DEV live shorthand",
assert.equal(liveExplicit.urlExplicit, true);
});
test("source/default smoke covers #278 sidebar collapse static contract", () => {
test("source/default smoke covers #352 resource explorer removal contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
assert.equal(report.checks.find((check) => check.id === "feedback-278-sidebar-collapse-contract")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "feedback-352-resource-explorer-removed")?.status, "pass");
});
test("source/default smoke covers #276 two-column wiring long-table contract", () => {
@@ -693,7 +693,7 @@ test("Code Agent quick prompt fixture fills input, does not autosend writes, and
assert.equal(layout.observations.some((item) => item.viewport.width === 390 && item.layoutOk), true);
});
test("layout smoke verifies desktop and mobile sidebar collapse geometry", async () => {
test("layout smoke verifies desktop and mobile default workbench geometry without resource explorer", async () => {
const report = await runDevCloudWorkbenchLayoutSmoke();
if (report.status === "skip") {
assert.match(report.summary, /Playwright is unavailable/u);
@@ -718,17 +718,9 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
assert.equal(report.validationCommands.includes(command), true, `missing ${command}`);
}
for (const id of [
"layout-desktop-expanded",
"layout-desktop-collapsed",
"layout-desktop-restored",
"layout-narrow-desktop-expanded",
"layout-narrow-desktop-collapsed",
"layout-narrow-desktop-restored",
"layout-mobile-collapsed",
"layout-mobile-drawer",
"layout-explorer-resize-desktop",
"layout-explorer-resize-narrow-desktop",
"layout-explorer-resize-mobile",
"layout-desktop-default",
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-gate-desktop",
"layout-gate-narrow-desktop",
"layout-gate-mobile"
@@ -750,60 +742,29 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip");
const desktopCollapsed = report.checks.find((check) => check.id === "layout-desktop-collapsed")?.observations;
assert.equal(desktopCollapsed.explorerCollapsed, true);
assert.equal(desktopCollapsed.toggle.ariaLabel, "展开左侧资源树");
assert.equal(desktopCollapsed.toggle.text, "展开资源树");
assert.equal(desktopCollapsed.collapsedWidthGain, true);
assert.equal(desktopCollapsed.keyTargetsReachable, true);
assert.equal(desktopCollapsed.wiring.longTableOk, true);
assert.equal(desktopCollapsed.wiring.noHorizontalScroll, true);
assert.equal(desktopCollapsed.wiring.headers.length, 2);
assert.deepEqual(desktopCollapsed.wiring.legacyHeaderHits, []);
assert.equal(desktopCollapsed.wiring.horizontalScroll.panelScrollWidth <= desktopCollapsed.wiring.horizontalScroll.panelClientWidth + 2, true);
assert.equal(desktopCollapsed.resizeHandle.ok, false);
const desktopDefault = report.checks.find((check) => check.id === "layout-desktop-default")?.observations;
assert.equal(desktopDefault.removedSelectorsAbsent, true);
assert.equal(desktopDefault.keyTargetsReachable, true);
assert.equal(desktopDefault.wiring.longTableOk, true);
assert.equal(desktopDefault.wiring.noHorizontalScroll, true);
assert.equal(desktopDefault.wiring.headers.length, 2);
assert.deepEqual(desktopDefault.wiring.legacyHeaderHits, []);
assert.equal(desktopDefault.wiring.horizontalScroll.panelScrollWidth <= desktopDefault.wiring.horizontalScroll.panelClientWidth + 2, true);
assert.equal(Object.hasOwn(desktopDefault.boxes.shell, "text"), false);
assert.equal(Object.hasOwn(desktopDefault.semanticOverlapChecks[0].boxes["#m3-control-form"], "text"), false);
assert.equal(desktopDefault.failures.length, 0);
assert.equal(desktopDefault.semanticOverlapChecks.every((check) => !check.overlaps), true);
assert.equal(desktopDefault.overflowChecks.every((check) => check.ok), true);
const mobileCollapsed = report.checks.find((check) => check.id === "layout-mobile-collapsed")?.observations;
assert.equal(mobileCollapsed.explorerCollapsed, true);
assert.equal(mobileCollapsed.defaultCollapsedMobile, true);
assert.equal(mobileCollapsed.keyTargetsReachable, true);
assert.equal(mobileCollapsed.wiring.longTableOk, true);
assert.equal(mobileCollapsed.wiring.noHorizontalScroll, true);
assert.equal(mobileCollapsed.resizeHandle.ok, false);
const desktopExpanded = report.checks.find((check) => check.id === "layout-desktop-expanded")?.observations;
assert.equal(Object.hasOwn(desktopExpanded.boxes.shell, "text"), false);
assert.equal(Object.hasOwn(desktopExpanded.semanticOverlapChecks[0].boxes["#m3-control-form"], "text"), false);
assert.equal(desktopExpanded.failures.length, 0);
assert.equal(desktopExpanded.semanticOverlapChecks.every((check) => !check.overlaps), true);
assert.equal(desktopExpanded.overflowChecks.every((check) => check.ok), true);
assert.equal(desktopExpanded.resizeHandle.ok, true);
const desktopResize = report.checks.find((check) => check.id === "layout-explorer-resize-desktop")?.observations;
assert.equal(desktopResize.pass, true);
assert.equal(desktopResize.before.handle.ariaLabel, "拖拽调整左侧资源树宽度");
assert.equal(desktopResize.before.handle.role, "separator");
assert.equal(Math.abs(desktopResize.afterDrag.explorerWidth - desktopResize.before.explorerWidth) >= 24, true);
assert.equal(desktopResize.afterEnd.explorerWidth, desktopResize.afterEnd.bounds.max);
assert.equal(desktopResize.afterHome.explorerWidth, desktopResize.afterHome.bounds.min);
assert.equal(desktopResize.collapsed.explorerCollapsed, true);
assert.equal(desktopResize.collapsed.handleTabIndex, -1);
assert.equal(desktopResize.restored.storage.version, 1);
assert.equal(desktopResize.restoredFromStorage.keyTargetsReachable, true);
const narrowResize = report.checks.find((check) => check.id === "layout-explorer-resize-narrow-desktop")?.observations;
assert.equal(narrowResize.pass, true);
assert.equal(narrowResize.afterEnd.explorerWidth <= narrowResize.afterEnd.bounds.max, true);
assert.equal(narrowResize.restoredFromStorage.noHorizontalOverflow, true);
const mobileResize = report.checks.find((check) => check.id === "layout-explorer-resize-mobile")?.observations;
assert.equal(mobileResize.pass, true);
assert.equal(mobileResize.handleDisplay, "none");
assert.equal(mobileResize.handleTabIndex, -1);
const mobileDefault = report.checks.find((check) => check.id === "layout-mobile-default")?.observations;
assert.equal(mobileDefault.removedSelectorsAbsent, true);
assert.equal(mobileDefault.keyTargetsReachable, true);
assert.equal(mobileDefault.wiring.longTableOk, true);
assert.equal(mobileDefault.wiring.noHorizontalScroll, true);
const compact = compactLayoutSmokeCliOutput(report);
assert.equal(compact.status, "pass");
assert.equal(compact.artifacts.screenshotCount, 21);
assert.equal(compact.artifacts.screenshotCount, 12);
assert.equal(compact.failures.length, 0);
assert.equal(compact.blockers.length, 0);
assert.equal(JSON.stringify(compact).includes('"checks"'), false);
File diff suppressed because it is too large Load Diff
+1 -272
View File
@@ -79,9 +79,6 @@ const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([
]);
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1";
const EXPLORER_DEFAULT_WIDTH = 292;
const EXPLORER_MIN_WIDTH = 220;
const EXPLORER_MAX_WIDTH = 420;
const RIGHT_SIDEBAR_DEFAULT_WIDTH = 620;
const RIGHT_SIDEBAR_MIN_WIDTH = 560;
const RIGHT_SIDEBAR_MAX_WIDTH = 760;
@@ -91,14 +88,7 @@ let rpcSequence = 0;
const el = {
shell: query("[data-app-shell]"),
explorerToggle: byId("explorer-toggle"),
explorerResize: byId("explorer-resize"),
rightSidebarResize: byId("right-sidebar-resize"),
explorerStatus: byId("explorer-status"),
treeCount: byId("tree-count"),
resourceTree: byId("resource-tree"),
capabilityCount: byId("capability-count"),
explorerCapabilities: byId("explorer-capabilities"),
routePath: byId("route-path"),
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
@@ -159,18 +149,13 @@ const state = {
pending: false
},
layout: {
explorerWidth: EXPLORER_DEFAULT_WIDTH,
rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH,
drag: null,
rightDrag: null
}
};
initRoutes();
initLayoutSizing();
initExplorerToggle();
syncMobileExplorer();
initExplorerResize();
initRightSidebarResize();
initSideTabs();
initHardwareTabs();
@@ -219,9 +204,7 @@ function initRoutes() {
function initLayoutSizing() {
const persisted = readPersistedLayout();
const explorerWidth = clampExplorerWidth(persisted?.explorerWidth ?? EXPLORER_DEFAULT_WIDTH);
const rightSidebarWidth = clampRightSidebarWidth(persisted?.rightSidebarWidth ?? RIGHT_SIDEBAR_DEFAULT_WIDTH);
setExplorerWidth(explorerWidth, { persist: false });
setRightSidebarWidth(rightSidebarWidth, { persist: false });
}
@@ -232,7 +215,6 @@ function readPersistedLayout() {
const payload = JSON.parse(raw);
if (payload?.version !== 1) return null;
return {
explorerWidth: Number(payload.explorerWidth),
rightSidebarWidth: Number(payload.rightSidebarWidth)
};
} catch {
@@ -246,7 +228,6 @@ function persistLayout() {
LAYOUT_STORAGE_KEY,
JSON.stringify({
version: 1,
explorerWidth: state.layout.explorerWidth,
rightSidebarWidth: state.layout.rightSidebarWidth
})
);
@@ -255,16 +236,6 @@ function persistLayout() {
}
}
function setExplorerWidth(width, options = {}) {
const clamped = clampExplorerWidth(width);
state.layout.explorerWidth = clamped;
el.shell.style.setProperty("--explorer-width", `${clamped}px`);
syncExplorerResizeA11y();
syncRightSidebarResizeA11y();
if (options.persist !== false) persistLayout();
return clamped;
}
function setRightSidebarWidth(width, options = {}) {
const clamped = clampRightSidebarWidth(width);
state.layout.rightSidebarWidth = clamped;
@@ -274,13 +245,6 @@ function setRightSidebarWidth(width, options = {}) {
return clamped;
}
function clampExplorerWidth(value) {
const numeric = Number(value);
const width = Number.isFinite(numeric) ? numeric : EXPLORER_DEFAULT_WIDTH;
const bounds = explorerResizeBounds();
return Math.min(Math.max(Math.round(width), bounds.min), bounds.max);
}
function clampRightSidebarWidth(value) {
const numeric = Number(value);
const width = Number.isFinite(numeric) ? numeric : RIGHT_SIDEBAR_DEFAULT_WIDTH;
@@ -288,24 +252,6 @@ function clampRightSidebarWidth(value) {
return Math.min(Math.max(Math.round(width), bounds.min), bounds.max);
}
function explorerResizeBounds() {
const styles = getComputedStyle(el.shell);
const cssMin = readCssPixel(styles.getPropertyValue("--explorer-min-width"), EXPLORER_MIN_WIDTH);
const cssMax = readCssPixel(styles.getPropertyValue("--explorer-max-width"), EXPLORER_MAX_WIDTH);
if (window.matchMedia("(max-width: 860px)").matches) {
return { min: EXPLORER_MIN_WIDTH, max: EXPLORER_MAX_WIDTH };
}
if (window.matchMedia("(max-width: 1240px)").matches) {
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 86);
const dynamicMax = window.innerWidth - railWidth - 420 - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 92);
const rightWidth = Math.min(Math.max(window.innerWidth * 0.45, 560), 740);
const dynamicMax = window.innerWidth - railWidth - 360 - rightWidth - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
function boundedResizeRange(min, max, dynamicMax) {
const safeMin = Math.max(0, Math.round(min));
const safeMax = Math.max(safeMin, Math.round(Math.min(max, Number.isFinite(dynamicMax) ? dynamicMax : max)));
@@ -320,9 +266,7 @@ function rightSidebarResizeBounds() {
return { min: 0, max: Math.max(0, window.innerWidth) };
}
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 92);
const explorerWidth = el.shell.classList.contains("explorer-collapsed") ? 0 : state.layout.explorerWidth;
const centerMinWidth = el.shell.classList.contains("explorer-collapsed") ? 420 : 360;
const dynamicMax = window.innerWidth - railWidth - explorerWidth - centerMinWidth - 4;
const dynamicMax = window.innerWidth - railWidth - 420 - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
@@ -331,14 +275,6 @@ function readCssPixel(value, fallback) {
return Number.isFinite(parsed) ? parsed : fallback;
}
function syncExplorerResizeA11y() {
const bounds = explorerResizeBounds();
el.explorerResize.setAttribute("aria-valuemin", String(bounds.min));
el.explorerResize.setAttribute("aria-valuemax", String(bounds.max));
el.explorerResize.setAttribute("aria-valuenow", String(state.layout.explorerWidth));
el.explorerResize.setAttribute("aria-valuetext", `左侧资源树宽度 ${state.layout.explorerWidth} 像素`);
}
function syncRightSidebarResizeA11y() {
const bounds = rightSidebarResizeBounds();
const disabled = !canResizeRightSidebar();
@@ -374,57 +310,6 @@ function showView(route) {
}
}
function initExplorerToggle() {
el.explorerToggle.addEventListener("click", () => {
const collapsed = !el.shell.classList.contains("explorer-collapsed");
setExplorerCollapsed(collapsed);
});
}
function initExplorerResize() {
window.addEventListener("resize", () => {
if (canResizeExplorer()) setExplorerWidth(state.layout.explorerWidth, { persist: false });
syncExplorerResizeA11y();
});
el.explorerResize.addEventListener("pointerdown", (event) => {
if (!canResizeExplorer()) return;
event.preventDefault();
el.explorerResize.setPointerCapture?.(event.pointerId);
state.layout.drag = {
pointerId: event.pointerId,
startX: event.clientX,
startWidth: state.layout.explorerWidth
};
el.shell.classList.add("is-resizing");
});
el.explorerResize.addEventListener("pointermove", (event) => {
const drag = state.layout.drag;
if (!drag || drag.pointerId !== event.pointerId) return;
setExplorerWidth(drag.startWidth + event.clientX - drag.startX, { persist: false });
});
el.explorerResize.addEventListener("pointerup", finishExplorerResize);
el.explorerResize.addEventListener("pointercancel", finishExplorerResize);
el.explorerResize.addEventListener("keydown", (event) => {
if (!canResizeExplorer()) return;
const keySteps = {
ArrowLeft: -16,
ArrowRight: 16,
PageDown: -48,
PageUp: 48
};
if (Object.hasOwn(keySteps, event.key)) {
event.preventDefault();
setExplorerWidth(state.layout.explorerWidth + keySteps[event.key]);
} else if (event.key === "Home") {
event.preventDefault();
setExplorerWidth(explorerResizeBounds().min);
} else if (event.key === "End") {
event.preventDefault();
setExplorerWidth(explorerResizeBounds().max);
}
});
}
function initRightSidebarResize() {
window.addEventListener("resize", () => {
if (canResizeRightSidebar()) setRightSidebarWidth(state.layout.rightSidebarWidth, { persist: false });
@@ -469,14 +354,6 @@ function initRightSidebarResize() {
});
}
function finishExplorerResize(event) {
const drag = state.layout.drag;
if (!drag || drag.pointerId !== event.pointerId) return;
state.layout.drag = null;
el.shell.classList.remove("is-resizing");
persistLayout();
}
function finishRightSidebarResize(event) {
const drag = state.layout.rightDrag;
if (!drag || drag.pointerId !== event.pointerId) return;
@@ -485,45 +362,10 @@ function finishRightSidebarResize(event) {
persistLayout();
}
function canResizeExplorer() {
return !window.matchMedia("(max-width: 860px)").matches && !el.shell.classList.contains("explorer-collapsed");
}
function canResizeRightSidebar() {
return !window.matchMedia("(max-width: 1240px)").matches;
}
function setExplorerCollapsed(collapsed) {
if (!collapsed) {
setExplorerWidth(state.layout.explorerWidth, { persist: false });
}
el.shell.classList.toggle("explorer-collapsed", collapsed);
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
el.explorerToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
el.explorerToggle.setAttribute("aria-label", collapsed ? "展开左侧资源树" : "收起左侧资源树");
el.explorerToggle.title = collapsed ? "展开左侧资源树,恢复导航和硬件资源" : "收起左侧资源树,给工作区更多宽度";
el.explorerToggle.textContent = collapsed ? "展开资源树" : "收起资源树";
const resizeDisabled = collapsed || window.matchMedia("(max-width: 860px)").matches;
el.explorerResize.tabIndex = resizeDisabled ? -1 : 0;
el.explorerResize.setAttribute("aria-disabled", resizeDisabled ? "true" : "false");
el.explorerResize.setAttribute("aria-hidden", resizeDisabled ? "true" : "false");
setRightSidebarWidth(state.layout.rightSidebarWidth, { persist: false });
}
function syncMobileExplorer() {
const media = window.matchMedia("(max-width: 860px)");
const apply = () => {
if (media.matches) {
setExplorerCollapsed(true);
} else {
setExplorerCollapsed(false);
setExplorerWidth(state.layout.explorerWidth, { persist: false });
}
};
apply();
media.addEventListener("change", apply);
}
async function loadHelpSurface() {
el.helpStatus.textContent = "加载中";
el.helpStatus.className = "state-tag tone-dry-run";
@@ -558,22 +400,14 @@ function initSideTabs() {
for (const jump of document.querySelectorAll("[data-side-tab-jump]")) {
jump.addEventListener("click", () => {
selectSideTab(jump.dataset.sideTabJump);
collapseExplorerAfterMobileAction();
});
}
}
function initQuickActions() {
for (const button of document.querySelectorAll("[data-focus-command]")) {
button.addEventListener("click", () => {
el.commandInput.focus();
collapseExplorerAfterMobileAction();
});
}
for (const button of document.querySelectorAll("[data-agent-quick-prompt]")) {
button.addEventListener("click", () => {
fillAgentQuickPrompt(button);
collapseExplorerAfterMobileAction();
});
}
}
@@ -586,12 +420,6 @@ function fillAgentQuickPrompt(button) {
el.commandInput.setSelectionRange(prompt.length, prompt.length);
}
function collapseExplorerAfterMobileAction() {
if (window.matchMedia("(max-width: 860px)").matches) {
setExplorerCollapsed(true);
}
}
function selectSideTab(tabId) {
for (const tab of document.querySelectorAll("[data-side-tab]")) {
const active = tab.dataset.sideTab === tabId;
@@ -788,12 +616,8 @@ function initGateControls() {
}
function renderStaticWorkbench() {
el.explorerStatus.textContent = "只读工作区";
el.explorerStatus.className = "status-dot tone-source";
el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。";
renderResourceTree();
renderCapabilityList();
renderConversation();
renderTaskList();
renderHardwareStatus(null);
@@ -1125,86 +949,6 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res
};
}
function renderResourceTree() {
const items = [
{
label: gateSummary.topology.projectId,
meta: "当前项目",
tone: "source",
children: [
...gateSummary.topology.gateways.map((gateway) => ({
label: gateway.gatewaySessionId,
meta: `${gateway.serviceId} / ${statusLabel(gateway.status)}`,
tone: gateway.status
})),
...gateSummary.topology.boxResources.map((resource) => ({
label: resource.resourceId,
meta: `${resourceTypeLabel(resource.resourceType)} / ${statusLabel(resource.state)}`,
tone: resource.state
})),
{
label: gateSummary.topology.patchPanel.patchPanelStatusId,
meta: `${gateSummary.topology.patchPanel.serviceId} / ${statusLabel(gateSummary.topology.patchPanel.state)}`,
tone: gateSummary.topology.patchPanel.state
}
]
},
{
label: gateSummary.agent.agentSessionId,
meta: `${gateSummary.agent.agentServiceId} / Agent 会话`,
tone: gateSummary.agent.agentStatus,
children: [
{
label: gateSummary.agent.workerSessionId,
meta: `${gateSummary.agent.workerServiceId} / ${statusLabel(gateSummary.agent.workerStatus)}`,
tone: gateSummary.agent.workerStatus
}
]
}
];
el.treeCount.textContent = String(items.reduce((count, item) => count + 1 + (item.children?.length ?? 0), 0));
replaceChildren(el.resourceTree, ...items.map(treeGroup));
}
function treeGroup(item) {
const details = document.createElement("details");
details.open = true;
const summary = document.createElement("summary");
summary.append(statusLight(item.tone), textSpan(item.label, "tree-label"), textSpan(item.meta, "tree-meta"));
details.append(summary);
for (const child of item.children ?? []) {
const row = document.createElement("button");
row.type = "button";
row.className = "tree-row";
row.append(statusLight(child.tone), textSpan(child.label, "tree-label"), textSpan(child.meta, "tree-meta"));
details.append(row);
}
return details;
}
function renderCapabilityList() {
const capabilities = [
{
title: "任务草稿",
detail: "在底部输入区记录要交给 Agent 的任务,不会直接发送硬件请求。",
tone: "source"
},
{
title: "资源查看",
detail: "查看 Gateway-SIMU、BOX-SIMU 与 hwlab-patch-panel 的只读资源状态。",
tone: "source"
},
{
title: "接线核对",
detail: "在右侧两列接线表核对互连设备、DO/DI 对应关系与证据记录。",
tone: "source"
}
];
el.capabilityCount.textContent = String(capabilities.length);
replaceChildren(el.explorerCapabilities, ...capabilities.map(infoCard));
}
function renderConversation() {
const introMessages = [
{
@@ -2785,12 +2529,6 @@ function infoCard(item) {
return article;
}
function statusLight(tone) {
const span = document.createElement("span");
span.className = `tree-light tone-bg-${toneClass(tone)}`;
return span;
}
function badge(text, tone = text) {
const span = document.createElement("span");
span.className = `badge tone-${toneClass(tone)}`;
@@ -3252,15 +2990,6 @@ function roleLabel(role) {
);
}
function resourceTypeLabel(type) {
return (
{
board: "板卡",
relay: "继电器"
}[String(type ?? "").toLowerCase()] ?? statusLabel(type)
);
}
function recordKindLabel(kind) {
return (
{
+4 -4
View File
@@ -2,12 +2,12 @@
本页面说明当前 Cloud Workbench 的前端区域与状态含义。默认 `/` 仍进入工作台;本说明只通过工作台内部的 `#help` 入口打开,不作为默认页。
## 左侧资源与功能导航
## 活动栏与默认工作区
- 活动栏使用完整中文标签:`工作台` 回到默认工作台,`内部复核` 打开只读复核表,`使用说明` 打开本说明`资源树` 展开或收起资源树
- 活动栏使用完整中文标签:`工作台` 回到默认工作台,`内部复核` 打开只读复核表,`使用说明` 打开本说明。
- 左侧不再放独立的可信记录跳转;可信记录保留在右侧子面板。
- 资源树只展示当前项目、Gateway-SIMU、BOX-SIMU、`hwlab-patch-panel`、Agent manager / worker 等用户可理解资源,不把 M0-M5 Gate 状态放在默认首屏
- 常用能力区提供任务草稿、资源查看与接线核对入口。`LOCAL` 只表示浏览器本地草稿,不等同于 DEV-LIVE。
- 默认首屏不再放资源树、能力卡片或左侧抽屉,空间直接分配给 Code Agent 对话、执行证据、硬件状态、接线和控制
- `LOCAL` 只表示浏览器本地草稿,不等同于 DEV-LIVE。
## 中间 Agent 对话与工作清单
-43
View File
@@ -14,49 +14,6 @@
<button class="rail-button" type="button" data-route="gate" title="内部复核" aria-label="内部复核">内部复核</button>
<button class="rail-button" type="button" data-route="help" title="使用说明" aria-label="使用说明">使用说明</button>
<span class="rail-spacer" aria-hidden="true"></span>
<button class="rail-button" id="explorer-toggle" type="button" title="收起左侧资源树,给工作区更多宽度" aria-label="收起左侧资源树" aria-controls="resource-explorer" aria-expanded="true" aria-pressed="false">收起资源树</button>
</aside>
<aside class="explorer" id="resource-explorer" aria-label="资源浏览与硬件资源树">
<header class="explorer-head">
<div>
<p class="eyebrow">HWLAB Cloud</p>
<h1>云工作台</h1>
</div>
<span class="status-dot tone-source" id="explorer-status">只读工作区</span>
</header>
<section class="quick-actions" aria-label="常用工作入口">
<button type="button" data-focus-command>编写任务</button>
<button type="button" data-side-tab-jump="wiring">查看接线</button>
<button type="button" data-side-tab-jump="control">查看状态</button>
</section>
<section class="tree-panel">
<div class="panel-title-row">
<h2>硬件资源</h2>
<span id="tree-count">0</span>
</div>
<nav id="resource-tree" class="resource-tree" aria-label="HWLAB 硬件资源"></nav>
</section>
<section class="tree-panel blockers-panel">
<div class="panel-title-row">
<h2>常用能力</h2>
<span id="capability-count">0</span>
</div>
<div id="explorer-capabilities" class="compact-list"></div>
</section>
<div
class="resize-handle explorer-resize"
id="explorer-resize"
role="separator"
tabindex="0"
aria-label="拖拽调整左侧资源树宽度"
aria-controls="resource-explorer"
aria-orientation="vertical"
aria-valuemin="220"
aria-valuemax="420"
aria-valuenow="292"
title="拖拽调整左侧资源树宽度;用左右方向键微调,Home/End 到最小或最大。"
></div>
</aside>
<section class="center-workspace">
+35 -52
View File
@@ -245,17 +245,9 @@ assert.equal(
"#288 single-table gate is future coverage; current /gate route remains covered by layout smoke"
);
for (const layoutCheckId of [
"layout-desktop-expanded",
"layout-desktop-collapsed",
"layout-desktop-restored",
"layout-narrow-desktop-expanded",
"layout-narrow-desktop-collapsed",
"layout-narrow-desktop-restored",
"layout-mobile-collapsed",
"layout-mobile-drawer",
"layout-explorer-resize-desktop",
"layout-explorer-resize-narrow-desktop",
"layout-explorer-resize-mobile",
"layout-desktop-default",
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-right-sidebar-resize-desktop",
"layout-right-sidebar-resize-narrow-desktop",
"layout-right-sidebar-resize-mobile",
@@ -387,10 +379,6 @@ assert.match(html, /href="\/styles\.css"/);
assert.match(html, /src="\/app\.mjs"/);
for (const workbenchElement of [
"activity-rail",
"explorer",
"explorer-resize",
"resource-explorer",
"resource-tree",
"conversation-list",
"agent-chat-status",
"code-agent-summary",
@@ -410,29 +398,41 @@ for (const workbenchElement of [
]) {
assert.match(html, new RegExp(workbenchElement));
}
assert.match(html, /id="explorer-toggle"/);
assert.match(html, /aria-controls="resource-explorer"/);
assert.match(html, /aria-expanded="true"/);
assert.match(html, /aria-pressed="false"/);
assert.match(html, /aria-label="收起左侧资源树"/);
assert.match(html, />收起资源树<\/button>/);
assert.match(html, /id="explorer-resize"/);
assert.match(html, /role="separator"/);
assert.match(html, /aria-label="拖拽调整左侧资源树宽度"/);
assert.match(html, /aria-valuemin="220"/);
assert.match(html, /aria-valuemax="420"/);
assert.match(html, /aria-valuenow="292"/);
for (const removedSelectorTerm of [
"id=\"resource-explorer\"",
"id=\"explorer-toggle\"",
"id=\"explorer-resize\"",
"id=\"resource-tree\"",
"explorer-capabilities",
"capability-count",
"tree-count",
"quick-actions",
"data-focus-command"
]) {
assert.doesNotMatch(html, new RegExp(escapeRegExp(removedSelectorTerm)), `removed resource explorer selector returned: ${removedSelectorTerm}`);
}
for (const removedUiText of [
"资源浏览与硬件资源树",
"只读工作区",
"常用能力",
"收起资源树",
"展开资源树",
"拖拽调整左侧资源树宽度",
"资源查看",
"查看接线",
"查看状态",
"编写任务"
]) {
assert.doesNotMatch(`${html}\n${app}\n${styles}`, new RegExp(escapeRegExp(removedUiText)), `removed resource explorer copy returned: ${removedUiText}`);
}
assert.match(html, /id="hardware-sidebar"/);
assert.match(html, /id="right-sidebar-resize"/);
assert.match(html, /aria-label="拖拽调整右侧硬件状态栏宽度"/);
assert.match(html, /aria-valuemin="560"/);
assert.match(html, /aria-valuemax="760"/);
assert.match(html, /aria-valuenow="620"/);
assert.match(app, /setAttribute\("aria-label", collapsed \? "展开左侧资源树" : "收起左侧资源树"\)/);
assert.match(app, /textContent = collapsed \? "展开资源树" : "收起资源树"/);
assert.doesNotMatch(app, /setExplorerCollapsed|initExplorerResize|clampExplorerWidth|syncMobileExplorer|collapseExplorerAfterMobileAction/);
assert.match(app, /LAYOUT_STORAGE_KEY\s*=\s*"hwlab\.workbench\.layout\.v1"/);
assert.match(app, /function initExplorerResize/);
assert.match(app, /function clampExplorerWidth/);
assert.match(app, /function initRightSidebarResize/);
assert.match(app, /function clampRightSidebarWidth/);
assert.match(app, /function rightSidebarResizeBounds/);
@@ -443,20 +443,14 @@ assert.match(app, /ArrowLeft/);
assert.match(app, /ArrowRight/);
assert.match(app, /event\.key === "Home"/);
assert.match(app, /event\.key === "End"/);
assert.match(styles, /--explorer-width:\s*292px/);
assert.match(styles, /--explorer-min-width:\s*220px/);
assert.match(styles, /--explorer-max-width:\s*420px/);
assert.doesNotMatch(styles, /--explorer-width|--explorer-min-width|--explorer-max-width|\.explorer|\.explorer-resize|\.quick-actions|\.resource-tree|\.tree-panel|\.tree-row|\.tree-label/);
assert.match(styles, /--right-sidebar-width:\s*620px/);
assert.match(styles, /--right-sidebar-min-width:\s*560px/);
assert.match(styles, /--right-sidebar-max-width:\s*760px/);
assert.match(styles, /--right-width:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/);
assert.match(styles, /--right-width-expanded:\s*clamp\(var\(--right-sidebar-min-width\),\s*var\(--right-sidebar-width\),\s*var\(--right-sidebar-max-width\)\)/);
assert.match(styles, /\.explorer-resize\s*{[^}]*cursor:\s*col-resize;/s);
assert.match(styles, /\.right-sidebar-resize\s*{[^}]*cursor:\s*col-resize;/s);
assert.match(styles, /\.explorer-collapsed \.explorer-resize\s*{[^}]*display:\s*none;/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer-resize\s*{[\s\S]*?display:\s*none;/);
assert.match(styles, /@media \(max-width: 1240px\)[\s\S]*?\.right-sidebar-resize\s*{[\s\S]*?display:\s*none;/);
assert.match(styles, /\.explorer-collapsed\s*\{[^}]*--explorer-width:\s*0px;[^}]*grid-template-columns:\s*var\(--rail-width\)\s+0\s+minmax\(420px,\s*1fr\)\s+var\(--right-width\);/s);
for (const viewId of ["workspace", "gate"]) {
assert.match(html, new RegExp(`data-view="${viewId}"`));
}
@@ -481,7 +475,7 @@ assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*height:\s*100%;[^}]*
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100dvh;[^}]*max-height:\s*100dvh;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s);
assert.match(helpMarkdown, /^# 云工作台内部使用说明/m);
for (const helpTerm of [
"左侧资源与功能导航",
"活动栏与默认工作区",
"Agent 对话与工作清单",
"完整中文标签",
"内部复核",
@@ -512,7 +506,7 @@ const defaultFirstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(ht
for (const backendTerm of ["Gate", "诊断", "验收", "BLOCKED", "SOURCE", "DRY-RUN", "DEV-LIVE", "M0-M5", "M3 基本", "trace/evidence", "执行轨迹", "OPENAI_API_KEY", "Secret", "secretRef", "hwlab-code-agent-provider", "m3-route-required", "waiting-for-dev-live"]) {
assert.equal(textHasForbiddenTerm(defaultFirstViewportText, backendTerm), false, `default workbench must not expose backend term ${backendTerm}`);
}
for (const fullNavLabel of ["工作台", "内部复核", "使用说明", "收起资源树"]) {
for (const fullNavLabel of ["工作台", "内部复核", "使用说明"]) {
assert.match(html, new RegExp(`>${escapeRegExp(fullNavLabel)}<`), `missing full nav label ${fullNavLabel}`);
}
for (const oneCharNavLabel of [">台<", ">证<", ">诊<", ">帮<"]) {
@@ -625,10 +619,6 @@ assert.doesNotMatch(app, /API 错误 \/ 只读模式/);
assert.doesNotMatch(app, /可信记录受阻 \/ 只读模式/);
assert.doesNotMatch(app, /API 降级 \/ 只读模式/);
assert.doesNotMatch(functionBody(app, "workbenchApiSurfaceStatus"), /runtime_durable_adapter_query_blocked|DB live readiness|Gate|诊断|验收|M0-M5|执行轨迹/u);
assert.match(html, /quick-actions/);
assert.match(html, /编写任务/);
assert.match(html, /查看接线/);
assert.match(html, /查看状态/);
assert.match(html, /placeholder="输入要发送给 Code Agent 的消息;不会直接触发硬件变更。"/);
assert.match(html, />发送<\/button>/);
assert.doesNotMatch(html, />添加草稿<\/button>/);
@@ -828,15 +818,8 @@ assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hid
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(app, /function syncMobileExplorer/);
assert.match(app, /window\.matchMedia\("\(max-width: 860px\)"\)/);
assert.match(app, /setExplorerCollapsed\(true\)/);
assert.match(app, /function collapseExplorerAfterMobileAction/);
assert.match(functionBody(app, "setExplorerCollapsed"), /aria-expanded/);
assert.match(styles, /\.explorer-collapsed \.explorer\s*{[^}]*pointer-events:\s*none;/s);
assert.match(styles, /\.(?:compact-list|conversation-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer\s*{[\s\S]*?grid-row:\s*1 \/ 3;[\s\S]*?z-index:\s*4;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/);
assert.match(styles, /@media \(max-width: 520px\)[\s\S]*?\.command-bar\s*{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto auto;/);
+8 -207
View File
@@ -18,9 +18,6 @@
--bad: #e76e5e;
--info: #7ea6c9;
--rail-width: 92px;
--explorer-width: 292px;
--explorer-min-width: 220px;
--explorer-max-width: 420px;
--right-sidebar-width: 620px;
--right-sidebar-min-width: 560px;
--right-sidebar-max-width: 760px;
@@ -83,7 +80,7 @@ ul {
max-height: 100dvh;
min-height: 0;
display: grid;
grid-template-columns: var(--rail-width) var(--explorer-width) minmax(360px, 1fr) var(--right-width-expanded);
grid-template-columns: var(--rail-width) minmax(420px, 1fr) var(--right-width-expanded);
grid-template-rows: 1fr;
overflow: hidden;
overscroll-behavior: contain;
@@ -91,7 +88,6 @@ ul {
}
.activity-rail,
.explorer,
.right-sidebar,
.center-workspace {
min-width: 0;
@@ -100,7 +96,7 @@ ul {
.activity-rail {
display: grid;
grid-template-rows: repeat(3, minmax(42px, auto)) 1fr minmax(42px, auto);
grid-template-rows: repeat(3, minmax(42px, auto)) minmax(0, 1fr);
gap: 6px;
padding: 8px;
background: var(--rail);
@@ -129,10 +125,6 @@ ul {
word-break: keep-all;
}
#explorer-toggle {
min-height: 44px;
}
.rail-button:hover,
.rail-button:focus-visible,
.rail-button.active {
@@ -146,22 +138,6 @@ ul {
box-shadow: inset 3px 0 0 var(--accent);
}
.explorer {
position: relative;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
gap: 8px;
padding: 10px 14px 10px 10px;
background: rgba(22, 25, 22, 0.98);
border-right: 1px solid var(--line);
overflow: hidden;
}
.explorer-collapsed {
--explorer-width: 0px;
grid-template-columns: var(--rail-width) 0 minmax(420px, 1fr) var(--right-width);
}
.workbench-shell.is-resizing {
cursor: col-resize;
user-select: none;
@@ -181,21 +157,6 @@ ul {
opacity: 0.56;
}
.explorer-resize {
top: 0;
right: 0;
width: 12px;
height: 100%;
cursor: col-resize;
}
.explorer-resize::before {
top: 10px;
bottom: 10px;
left: 5px;
width: 2px;
}
.right-sidebar-resize {
top: 0;
left: 0;
@@ -223,25 +184,6 @@ ul {
outline-offset: -2px;
}
.explorer-collapsed .explorer {
padding: 0;
border-right: 0;
overflow: hidden;
}
.explorer-collapsed .explorer > * {
display: none;
}
.explorer-collapsed .explorer {
pointer-events: none;
}
.explorer-collapsed .explorer-resize {
display: none;
}
.explorer-head,
.topbar,
.hardware-status,
.side-workspace,
@@ -250,15 +192,6 @@ ul {
border: 1px solid var(--line);
}
.explorer-head {
min-height: 78px;
display: flex;
justify-content: space-between;
gap: 10px;
align-items: start;
padding: 12px;
}
.eyebrow {
color: var(--accent-2);
font-size: 10px;
@@ -305,42 +238,6 @@ h3 {
overflow-wrap: anywhere;
}
.quick-actions {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
}
.quick-actions button {
width: 100%;
min-height: 32px;
padding: 6px 8px;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--text);
font-weight: 760;
text-align: left;
cursor: pointer;
}
.quick-actions button:hover,
.quick-actions button:focus-visible {
border-color: var(--line-strong);
background: var(--surface-3);
outline: 0;
}
.tree-panel {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
padding: 10px;
background: var(--surface);
border: 1px solid var(--line);
overflow: hidden;
}
.blockers-panel {
max-height: 230px;
}
@@ -362,7 +259,6 @@ h3 {
color: var(--muted);
}
.resource-tree,
.compact-list,
.conversation-list,
.task-list,
@@ -371,70 +267,6 @@ h3 {
overflow: auto;
}
.resource-tree {
display: grid;
align-content: start;
gap: 6px;
}
.resource-tree details {
border: 1px solid var(--line);
background: var(--surface-2);
}
.resource-tree summary,
.tree-row {
width: 100%;
min-height: 31px;
display: grid;
grid-template-columns: 8px minmax(0, 1fr);
gap: 7px;
align-items: center;
padding: 7px 8px;
border: 0;
background: transparent;
text-align: left;
}
.resource-tree summary {
cursor: pointer;
}
.tree-row {
padding-left: 22px;
border-top: 1px solid var(--line);
cursor: pointer;
}
.tree-row:hover {
background: rgba(212, 173, 67, 0.06);
}
.tree-label,
.tree-meta {
display: block;
min-width: 0;
overflow-wrap: anywhere;
}
.tree-label {
color: var(--text);
font-family: var(--mono);
font-size: 11px;
}
.tree-meta {
grid-column: 2;
color: var(--muted);
font-size: 11px;
}
.tree-light {
width: 8px;
height: 8px;
background: var(--dim);
}
.center-workspace {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
@@ -1741,20 +1573,12 @@ tbody tr:last-child td {
@media (max-width: 1240px) {
.workbench-shell {
--rail-width: 86px;
--explorer-width: 260px;
--explorer-min-width: 210px;
--explorer-max-width: 320px;
grid-template-columns: var(--rail-width) var(--explorer-width) minmax(420px, 1fr);
grid-template-columns: var(--rail-width) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) clamp(220px, 36dvh, 360px);
}
.explorer-collapsed {
--explorer-width: 0px;
grid-template-columns: var(--rail-width) 0 minmax(0, 1fr);
}
.right-sidebar {
grid-column: 2 / 4;
grid-column: 2;
grid-row: 2;
min-height: 0;
padding-left: 10px;
@@ -1768,11 +1592,8 @@ tbody tr:last-child td {
}
@media (max-width: 860px) {
.workbench-shell,
.explorer-collapsed {
.workbench-shell {
--rail-width: 68px;
--explorer-min-width: 0px;
--explorer-max-width: 0px;
grid-template-columns: var(--rail-width) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) clamp(214px, 30dvh, 280px);
}
@@ -1780,7 +1601,7 @@ tbody tr:last-child td {
.activity-rail {
grid-column: 1;
grid-row: 1 / 3;
grid-template-rows: repeat(3, minmax(42px, auto)) minmax(12px, 1fr) minmax(42px, auto);
grid-template-rows: repeat(3, minmax(42px, auto)) minmax(12px, 1fr);
overflow: hidden;
}
@@ -1789,20 +1610,6 @@ tbody tr:last-child td {
line-height: 1.15;
}
.explorer {
grid-column: 2;
grid-row: 1 / 3;
z-index: 4;
max-height: 100%;
padding: 10px;
overflow: auto;
box-shadow: -1px 0 0 var(--line), 12px 0 24px rgba(0, 0, 0, 0.24);
}
.explorer-resize {
display: none;
}
.right-sidebar-resize {
display: none;
}
@@ -1824,10 +1631,6 @@ tbody tr:last-child td {
grid-template-rows: auto minmax(0, auto) minmax(0, 1fr);
}
.explorer-collapsed .explorer {
display: none;
}
.topbar {
min-height: 0;
gap: 8px;
@@ -1889,8 +1692,7 @@ tbody tr:last-child td {
.hardware-list,
.m3-control-grid,
.m3-control-actions,
.quick-actions {
.m3-control-actions {
grid-template-columns: 1fr;
}
@@ -1939,8 +1741,7 @@ tbody tr:last-child td {
}
@media (max-width: 520px) {
.workbench-shell,
.explorer-collapsed {
.workbench-shell {
--rail-width: 64px;
grid-template-columns: var(--rail-width) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) clamp(260px, 34dvh, 320px);