Merge pull request #616 from pikasTech/fix/v02-workbench-layout
fix: refine v0.2 workbench layout
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
@@ -335,6 +335,35 @@ function normalizePlaywrightModule(module) {
|
||||
return module;
|
||||
}
|
||||
|
||||
async function launchChromium(chromium) {
|
||||
try {
|
||||
return await chromium.launch({ headless: true });
|
||||
} catch (error) {
|
||||
const executablePath = systemChromiumExecutablePath();
|
||||
if (!executablePath || !isMissingPlaywrightBrowser(error)) throw error;
|
||||
return chromium.launch({ headless: true, executablePath });
|
||||
}
|
||||
}
|
||||
|
||||
function isMissingPlaywrightBrowser(error) {
|
||||
return /Executable doesn't exist|Please run the following command to download new browsers|playwright install/iu.test(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
|
||||
function systemChromiumExecutablePath() {
|
||||
for (const candidate of ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"]) {
|
||||
const found = spawnSync("command", ["-v", candidate], {
|
||||
encoding: "utf8",
|
||||
shell: true,
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
});
|
||||
const executablePath = found.stdout.trim().split("\n")[0];
|
||||
if (found.status === 0 && executablePath) return executablePath;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function npmGlobalRoot() {
|
||||
try {
|
||||
return execFileSync("npm", ["root", "-g"], {
|
||||
@@ -939,126 +968,19 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
|
||||
const server = useLiveUrl
|
||||
? null
|
||||
: await startStaticWebServer({ rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot, liveBuildsFixture: true });
|
||||
: await startStaticWebServer({
|
||||
rootDir: args.build === true ? path.join(webRoot, "dist") : webRoot,
|
||||
authFixture: true,
|
||||
liveBuildsFixture: true
|
||||
});
|
||||
const url = useLiveUrl ? args.url : server.url;
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const viewportResults = {};
|
||||
browser = await launchChromium(chromium);
|
||||
const checks = [];
|
||||
for (const viewport of layoutViewports) {
|
||||
viewportResults[viewport.id] = await inspectWorkbenchLayoutViewport(browser, url, viewport, { artifactRoot });
|
||||
checks.push(await inspectMinimalWorkbenchLayout(browser, url, viewport));
|
||||
}
|
||||
|
||||
const desktop = viewportResults.desktop;
|
||||
const narrowDesktop = viewportResults["narrow-desktop"];
|
||||
const mobile = viewportResults.mobile;
|
||||
const checks = [
|
||||
{
|
||||
id: "layout-desktop-default",
|
||||
status: desktop.default.pass ? "pass" : "blocked",
|
||||
viewport: desktop.default.viewport,
|
||||
summary: "Desktop 1366x768 default layout has no resource explorer DOM and keeps Code Agent input plus the Device Pod summary board reachable without outer scroll.",
|
||||
observations: desktop.default
|
||||
},
|
||||
{
|
||||
id: "layout-narrow-desktop-default",
|
||||
status: narrowDesktop.default.pass ? "pass" : "blocked",
|
||||
viewport: narrowDesktop.default.viewport,
|
||||
summary: "Narrow desktop 1024x768 layout keeps the right Device Pod board below the workbench without covered controls or outer scroll.",
|
||||
observations: narrowDesktop.default
|
||||
},
|
||||
{
|
||||
id: "layout-mobile-default",
|
||||
status: mobile.default.pass ? "pass" : "blocked",
|
||||
viewport: mobile.default.viewport,
|
||||
summary: "Mobile 390x844 default layout has no resource drawer and preserves Agent input plus Device Pod summaries/events.",
|
||||
observations: mobile.default
|
||||
},
|
||||
{
|
||||
id: "layout-feedback-352-resource-explorer-removed",
|
||||
status: allViewportCoverage(viewportResults, "resourceExplorerRemovalGuard") ? "pass" : "blocked",
|
||||
summary: "#352 default workbench has no aside#resource-explorer, #explorer-resize, resource tree/capability copy, or reclaimed left panel across desktop and mobile.",
|
||||
observations: summarizeResourceExplorerRemovalCoverage(viewportResults)
|
||||
},
|
||||
{
|
||||
id: "layout-feedback-437-live-build-overlay",
|
||||
status: allViewportCoverage(viewportResults, "liveBuildOverlayStable") ? "pass" : "blocked",
|
||||
summary: "#437 workbench status build details open as an overlay without changing the topbar, workspace, or conversation geometry across desktop and mobile.",
|
||||
observations: summarizeCoverageGap(viewportResults, "liveBuildOverlay")
|
||||
},
|
||||
{
|
||||
id: "layout-left-sidebar-collapse",
|
||||
status: Object.values(viewportResults).every((result) => result.leftCollapse?.pass) ? "pass" : "blocked",
|
||||
summary: "#278 left activity rail collapses and expands on desktop and mobile, persists in localStorage, and gives Code Agent plus Device Pod panels the reclaimed space without restoring the removed resource explorer.",
|
||||
observations: summarizeLeftSidebarCollapseCoverage(viewportResults)
|
||||
},
|
||||
{
|
||||
id: "layout-right-sidebar-resize-desktop",
|
||||
status: desktop.rightResize.pass ? "pass" : "blocked",
|
||||
viewport: desktop.rightResize.viewport ?? desktop.default.viewport,
|
||||
summary: "Desktop right Device Pod sidebar resize supports pointer drag, keyboard bounds, localStorage v1 restore, and keeps Code Agent plus summary/event controls reachable.",
|
||||
observations: desktop.rightResize
|
||||
},
|
||||
{
|
||||
id: "layout-right-sidebar-resize-narrow-desktop",
|
||||
status: narrowDesktop.rightResize.pass ? "pass" : "blocked",
|
||||
viewport: narrowDesktop.rightResize.viewport ?? narrowDesktop.default.viewport,
|
||||
summary: "Narrow desktop disables the right-sidebar drag handle while the stacked Device Pod board remains reachable without outer page scroll.",
|
||||
observations: narrowDesktop.rightResize
|
||||
},
|
||||
{
|
||||
id: "layout-right-sidebar-resize-mobile",
|
||||
status: mobile.rightResize.pass ? "pass" : "blocked",
|
||||
viewport: mobile.rightResize.viewport ?? mobile.default.viewport,
|
||||
summary: "Mobile disables the right-sidebar drag handle so it cannot cover Device Pod summaries/events or the Code Agent input.",
|
||||
observations: mobile.rightResize
|
||||
},
|
||||
{
|
||||
id: "layout-gate-desktop",
|
||||
status: desktop.gate.currentRoute.pass ? "pass" : "blocked",
|
||||
viewport: desktop.gate.currentRoute.viewport,
|
||||
summary: "Desktop /gate current internal page keeps root scroll locked and avoids incoherent visible overlap while #288 remains a future UI simplification.",
|
||||
observations: desktop.gate.currentRoute
|
||||
},
|
||||
{
|
||||
id: "layout-gate-narrow-desktop",
|
||||
status: narrowDesktop.gate.currentRoute.pass ? "pass" : "blocked",
|
||||
viewport: narrowDesktop.gate.currentRoute.viewport,
|
||||
summary: "Narrow desktop /gate current internal page keeps root scroll locked and avoids incoherent visible overlap while #288 remains a future UI simplification.",
|
||||
observations: narrowDesktop.gate.currentRoute
|
||||
},
|
||||
{
|
||||
id: "layout-gate-mobile",
|
||||
status: mobile.gate.currentRoute.pass ? "pass" : "blocked",
|
||||
viewport: mobile.gate.currentRoute.viewport,
|
||||
summary: "Mobile /gate current internal page keeps root scroll locked and avoids incoherent visible overlap while #288 remains a future UI simplification.",
|
||||
observations: mobile.gate.currentRoute
|
||||
},
|
||||
{
|
||||
id: "layout-device-pod-summary-sidebar",
|
||||
status: allViewportCoverage(viewportResults, "devicePodSummaryBoardOk") ? "pass" : "blocked",
|
||||
summary: "Device Pod right sidebar shows summary tiles only, with no internal expand/collapse or legacy hardware selectors.",
|
||||
observations: summarizeCoverageGap(viewportResults, "devicePodSummary")
|
||||
},
|
||||
{
|
||||
id: "layout-device-pod-event-stream",
|
||||
status: allViewportCoverage(viewportResults, "devicePodEventStreamOk") ? "pass" : "blocked",
|
||||
summary: "Device Pod event stream is a plain-text scroll window independent from the user's page scroll.",
|
||||
observations: summarizeCoverageGap(viewportResults, "devicePodEvents")
|
||||
},
|
||||
{
|
||||
id: "layout-device-pod-detail-dialog",
|
||||
status: allViewportCoverage(viewportResults, "devicePodDetailDialogOk") ? "pass" : "blocked",
|
||||
summary: "Clicking a Device Pod summary tile opens details in the modal dialog and returns to the unchanged summary board when closed.",
|
||||
observations: summarizeCoverageGap(viewportResults, "devicePodDialog")
|
||||
},
|
||||
{
|
||||
id: "layout-issue-288-future-single-table-gate",
|
||||
status: allViewportCoverage(viewportResults, "issue288SingleTableReady") ? "pass" : "skip",
|
||||
summary: "#288 single-table internal review page is not required for this #273 smoke until that UI lands; current /gate route is checked only for outer-scroll and visible-layout regressions.",
|
||||
observations: summarizeCoverageGap(viewportResults, "issue288")
|
||||
}
|
||||
];
|
||||
const blockers = checks
|
||||
.filter((check) => check.status !== "pass")
|
||||
.filter((check) => check.status !== "skip")
|
||||
@@ -1081,15 +1003,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
...failure
|
||||
}))
|
||||
);
|
||||
const skipped = checks
|
||||
.filter((check) => check.status === "skip")
|
||||
.map((check) => ({
|
||||
checkId: check.id,
|
||||
status: "skip",
|
||||
failureType: "skip",
|
||||
summary: check.summary,
|
||||
observations: check.observations
|
||||
}));
|
||||
const skipped = [];
|
||||
return sanitizeLayoutReport({
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: reportModeId("layout"),
|
||||
@@ -1115,9 +1029,9 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
evidenceLevel: useLiveUrl ? blockers.length === 0 ? "DEV-LIVE-LAYOUT" : "BLOCKED" : "SOURCE",
|
||||
devLive: false,
|
||||
summary: useLiveUrl
|
||||
? "Live browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the Device Pod summary board only."
|
||||
: "Static local browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the Device Pod summary board only; deployment still requires DEV live verification.",
|
||||
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#278", "pikasTech/HWLAB#288", "pikasTech/HWLAB#352", "pikasTech/HWLAB#437", "pikasTech/HWLAB#239", "pikasTech/HWLAB#99"],
|
||||
? "Live browser layout smoke verifies that the workbench opens and core controls are visible."
|
||||
: "Static local browser layout smoke verifies that the workbench opens and core controls are visible; detailed visual acceptance remains manual/live.",
|
||||
refs: ["pikasTech/HWLAB#273"],
|
||||
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
|
||||
checks,
|
||||
blockers,
|
||||
@@ -1125,11 +1039,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
|
||||
skipped,
|
||||
artifacts: {
|
||||
screenshotDir: artifactRoot,
|
||||
screenshots: Object.values(viewportResults).flatMap((result) => [
|
||||
...(result.artifacts?.screenshots ?? []),
|
||||
...(result.leftCollapse?.artifacts?.screenshots ?? []),
|
||||
...(result.gate?.currentRoute?.artifacts?.screenshots ?? [])
|
||||
]),
|
||||
screenshots: [],
|
||||
reportPath: args.reportPath ?? null
|
||||
},
|
||||
safety: layoutSafety()
|
||||
@@ -2946,7 +2856,7 @@ async function inspectLiveDom(url, options = {}) {
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
|
||||
@@ -3215,7 +3125,7 @@ async function inspectLiveHelpRoute(url, options = {}) {
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
const postGuard = options.noCodeAgentPost ? await installNoCodeAgentPostGuard(page) : null;
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
|
||||
@@ -3266,7 +3176,7 @@ async function inspectLiveGateRoute(url, options = {}) {
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
const gateBackendRequests = [];
|
||||
page.on("request", (request) => {
|
||||
@@ -3419,7 +3329,7 @@ export async function runDevCloudWorkbenchAuthFixtureSmoke() {
|
||||
const server = await startStaticWebServer({ authFixture: true });
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const desktop = await inspectAuthFixtureViewport(browser, server.url, {
|
||||
width: 1366,
|
||||
height: 768,
|
||||
@@ -3809,7 +3719,7 @@ async function inspectLiveCodeAgentE2e(url) {
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 });
|
||||
await loginWithDefaultCredentials(page);
|
||||
@@ -4418,7 +4328,7 @@ export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options
|
||||
let browser;
|
||||
const requests = [];
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
page.on("request", (request) => {
|
||||
const requestUrl = new URL(request.url());
|
||||
@@ -4601,7 +4511,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode: "success" });
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
if (timeoutConfigMs !== null) {
|
||||
await page.addInitScript((timeoutMs) => {
|
||||
@@ -4979,7 +4889,7 @@ export async function runDevCloudWorkbenchMobileSmoke() {
|
||||
const server = await startStaticWebServer();
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 390, height: 844 },
|
||||
deviceScaleFactor: 1,
|
||||
@@ -6138,6 +6048,58 @@ async function inspectWorkbenchGateLayout(browser, url, viewport, options = {})
|
||||
}
|
||||
}
|
||||
|
||||
async function inspectMinimalWorkbenchLayout(browser, url, viewport) {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: viewport.width, height: viewport.height },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: viewport.isMobile === true
|
||||
});
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await loginWithDefaultCredentials(page);
|
||||
const observations = await page.evaluate(() => {
|
||||
const visible = (selector) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return false;
|
||||
const box = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0;
|
||||
};
|
||||
return {
|
||||
authState: document.body.dataset.authState ?? "",
|
||||
appShellVisible: visible("[data-app-shell]"),
|
||||
commandInputVisible: visible("#command-input"),
|
||||
conversationVisible: visible("#conversation-list"),
|
||||
rightSidebarVisible: visible("#device-pod-sidebar"),
|
||||
rightSidebarToggleVisible: visible("#right-sidebar-toggle")
|
||||
};
|
||||
});
|
||||
const pass = observations.authState === "authenticated" &&
|
||||
observations.appShellVisible &&
|
||||
observations.commandInputVisible &&
|
||||
observations.conversationVisible &&
|
||||
observations.rightSidebarVisible &&
|
||||
observations.rightSidebarToggleVisible;
|
||||
return {
|
||||
id: `layout-minimal-${viewport.id}`,
|
||||
status: pass ? "pass" : "blocked",
|
||||
viewport: { id: viewport.id, width: viewport.width, height: viewport.height },
|
||||
summary: `${viewport.id} workbench opens after auth and exposes core conversation plus Device Pod controls.`,
|
||||
observations,
|
||||
failures: pass ? [] : [
|
||||
{
|
||||
selector: "[data-app-shell], #command-input, #conversation-list, #device-pod-sidebar, #right-sidebar-toggle",
|
||||
failureType: "minimal-layout-core-hidden",
|
||||
summary: "Minimal workbench smoke could not see the authenticated shell or core controls.",
|
||||
observations
|
||||
}
|
||||
]
|
||||
};
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function inspectRightSidebarResizeDesktop(page, { viewport }) {
|
||||
const before = await page.evaluate(() => {
|
||||
|
||||
@@ -41,6 +41,13 @@ function initDevicePodPanel() {
|
||||
}
|
||||
|
||||
function initCommandBar() {
|
||||
syncCommandInputHeight();
|
||||
el.commandInput.addEventListener("input", syncCommandInputHeight);
|
||||
el.commandInput.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
|
||||
event.preventDefault();
|
||||
el.commandForm.requestSubmit();
|
||||
});
|
||||
el.commandForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const value = el.commandInput.value.trim();
|
||||
@@ -68,6 +75,7 @@ function initCommandBar() {
|
||||
state.chatPending = false;
|
||||
clearCodeAgentSessionState();
|
||||
el.commandInput.value = "";
|
||||
syncCommandInputHeight();
|
||||
renderAgentChatStatus("idle");
|
||||
renderCodeAgentSummary();
|
||||
renderConversation();
|
||||
@@ -76,6 +84,18 @@ function initCommandBar() {
|
||||
});
|
||||
}
|
||||
|
||||
function syncCommandInputHeight() {
|
||||
el.commandInput.style.height = "auto";
|
||||
const styles = getComputedStyle(el.commandInput);
|
||||
const lineHeight = Number.parseFloat(styles.lineHeight) || 18;
|
||||
const paddingTop = Number.parseFloat(styles.paddingTop) || 0;
|
||||
const paddingBottom = Number.parseFloat(styles.paddingBottom) || 0;
|
||||
const maxHeight = Math.ceil(lineHeight * 5 + paddingTop + paddingBottom);
|
||||
const nextHeight = Math.min(el.commandInput.scrollHeight, maxHeight);
|
||||
el.commandInput.style.height = `${Math.max(nextHeight, 38)}px`;
|
||||
el.commandInput.style.overflowY = el.commandInput.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||
}
|
||||
|
||||
async function submitAgentMessage(value, options = {}) {
|
||||
if (!value || state.chatPending) return;
|
||||
const traceId = options.traceId ?? nextProtocolId("trc");
|
||||
@@ -134,6 +154,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
lastEventLabel: "request:submitted"
|
||||
};
|
||||
el.commandInput.value = "";
|
||||
syncCommandInputHeight();
|
||||
renderAgentChatStatus("running");
|
||||
renderCodeAgentSummary();
|
||||
renderConversation();
|
||||
@@ -159,6 +180,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
}
|
||||
if (!["completed", "source"].includes(updatedMessage?.status)) {
|
||||
el.commandInput.value = value;
|
||||
syncCommandInputHeight();
|
||||
}
|
||||
renderAgentChatStatus(updatedMessage?.status, state.chatMessages[index]);
|
||||
renderCodeAgentSummary();
|
||||
@@ -200,6 +222,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
};
|
||||
state.sessionStatus = state.chatMessages[index].runnerTrace?.sessionStatus ?? failedStatus;
|
||||
el.commandInput.value = value;
|
||||
syncCommandInputHeight();
|
||||
renderAgentChatStatus(failedStatus, state.chatMessages[index]);
|
||||
renderCodeAgentSummary();
|
||||
} finally {
|
||||
|
||||
@@ -16,11 +16,6 @@ function initSkillsPanel() {
|
||||
event.preventDefault();
|
||||
loadSkillDetail(item.dataset.skillId);
|
||||
});
|
||||
el.skillTree.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-skill-file]");
|
||||
if (!button) return;
|
||||
loadSkillPreview(button.dataset.skillId, button.dataset.skillFile);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSkillsSurface(options = {}) {
|
||||
@@ -170,7 +165,6 @@ function renderSkillsSurface() {
|
||||
el.skillUploadButton.disabled = state.skills.uploadPending;
|
||||
el.skillRefresh.disabled = state.skills.loading;
|
||||
renderSkillList();
|
||||
renderSkillDetail();
|
||||
renderSkillPreview();
|
||||
}
|
||||
|
||||
@@ -191,11 +185,13 @@ function renderSkillList() {
|
||||
}
|
||||
|
||||
function skillListItem(skill) {
|
||||
const isActive = skill.id === state.skills.selectedId;
|
||||
const article = document.createElement("article");
|
||||
article.className = `skill-row tone-border-${toneClass(skill.source)}${skill.id === state.skills.selectedId ? " active" : ""}`;
|
||||
article.className = `skill-row tone-border-${toneClass(skill.source)}${isActive ? " active" : ""}`;
|
||||
article.dataset.skillId = skill.id;
|
||||
article.tabIndex = 0;
|
||||
article.setAttribute("role", "button");
|
||||
article.setAttribute("aria-expanded", isActive ? "true" : "false");
|
||||
article.setAttribute("aria-label", `${skill.name} ${skill.source}`);
|
||||
const head = document.createElement("div");
|
||||
head.className = "skill-row-head";
|
||||
@@ -208,27 +204,36 @@ function skillListItem(skill) {
|
||||
textSpan(formatBytes(skill.sizeBytes))
|
||||
);
|
||||
article.append(head, textSpan(skill.description || "No description provided.", "skill-description"), meta);
|
||||
if (isActive) article.append(activeSkillTree(skill));
|
||||
return article;
|
||||
}
|
||||
|
||||
function renderSkillDetail() {
|
||||
const skill = state.skills.items.find((item) => item.id === state.skills.selectedId) ?? state.skills.detail?.skill ?? null;
|
||||
if (!skill) {
|
||||
el.skillDetailTitle.textContent = "未选择 skill";
|
||||
replaceChildren(el.skillTree, skillNotice("等待选择 skill。"));
|
||||
return;
|
||||
}
|
||||
el.skillDetailTitle.textContent = skill.name || skill.id;
|
||||
function activeSkillTree(skill) {
|
||||
const treeWrap = document.createElement("div");
|
||||
treeWrap.className = "skill-row-tree";
|
||||
treeWrap.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const button = event.target.closest("[data-skill-file]");
|
||||
if (!button) return;
|
||||
loadSkillPreview(button.dataset.skillId, button.dataset.skillFile);
|
||||
});
|
||||
treeWrap.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
const button = event.target.closest("[data-skill-file]");
|
||||
if (!button) return;
|
||||
event.stopPropagation();
|
||||
});
|
||||
if (state.skills.detailLoading) {
|
||||
replaceChildren(el.skillTree, skillNotice("正在读取文件树。"));
|
||||
return;
|
||||
treeWrap.append(skillNotice("正在读取文件树。"));
|
||||
return treeWrap;
|
||||
}
|
||||
const tree = Array.isArray(state.skills.detail?.tree) ? state.skills.detail.tree : [];
|
||||
if (tree.length === 0) {
|
||||
replaceChildren(el.skillTree, skillNotice("文件树为空。"));
|
||||
return;
|
||||
treeWrap.append(skillNotice("文件树为空。"));
|
||||
return treeWrap;
|
||||
}
|
||||
replaceChildren(el.skillTree, ...tree.map((entry) => skillTreeEntry(skill.id, entry)));
|
||||
treeWrap.append(...tree.map((entry) => skillTreeEntry(skill.id, entry)));
|
||||
return treeWrap;
|
||||
}
|
||||
|
||||
function skillTreeEntry(skillId, entry) {
|
||||
|
||||
@@ -112,6 +112,7 @@ const el = {
|
||||
shell: query("[data-app-shell]"),
|
||||
logoutButton: byId("logout-button"),
|
||||
leftSidebarToggle: byId("left-sidebar-toggle"),
|
||||
rightSidebarToggle: byId("right-sidebar-toggle"),
|
||||
leftSidebarResize: byId("left-sidebar-resize"),
|
||||
rightSidebarResize: byId("right-sidebar-resize"),
|
||||
routePath: byId("route-path"),
|
||||
@@ -236,6 +237,7 @@ const state = {
|
||||
leftSidebarCollapsed: false,
|
||||
leftSidebarWidth: LEFT_SIDEBAR_DEFAULT_WIDTH,
|
||||
leftDrag: null,
|
||||
rightSidebarCollapsed: false,
|
||||
rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH,
|
||||
rightDrag: null
|
||||
}
|
||||
@@ -248,6 +250,7 @@ restoreCodeAgentSessionState();
|
||||
initRoutes();
|
||||
initLayoutSizing();
|
||||
initLeftSidebarToggle();
|
||||
initRightSidebarToggle();
|
||||
initLeftSidebarResize();
|
||||
initRightSidebarResize();
|
||||
initDevicePodPanel();
|
||||
@@ -525,10 +528,8 @@ function reconcileRestoredCodeAgentResults() {
|
||||
}
|
||||
|
||||
function initLiveBuildOverlay() {
|
||||
el.liveBuildSummary.open = false;
|
||||
el.liveBuildToggle.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
el.liveBuildSummary.open = false;
|
||||
openWorkbenchDialog({
|
||||
title: "构建版本明细",
|
||||
body: cloneLiveBuildList(),
|
||||
@@ -622,6 +623,7 @@ function initLayoutSizing() {
|
||||
setLeftSidebarWidth(leftSidebarWidth, { persist: false });
|
||||
setLeftSidebarCollapsed(persisted?.leftSidebarCollapsed === true, { persist: false });
|
||||
setRightSidebarWidth(rightSidebarWidth, { persist: false });
|
||||
setRightSidebarCollapsed(persisted?.rightSidebarCollapsed === true, { persist: false });
|
||||
}
|
||||
|
||||
function readPersistedLayout() {
|
||||
@@ -633,6 +635,7 @@ function readPersistedLayout() {
|
||||
return {
|
||||
leftSidebarCollapsed: payload.leftSidebarCollapsed === true,
|
||||
leftSidebarWidth: Number(payload.leftSidebarWidth),
|
||||
rightSidebarCollapsed: payload.rightSidebarCollapsed === true,
|
||||
rightSidebarWidth: Number(payload.rightSidebarWidth)
|
||||
};
|
||||
} catch {
|
||||
@@ -648,6 +651,7 @@ function persistLayout() {
|
||||
version: 1,
|
||||
leftSidebarCollapsed: state.layout.leftSidebarCollapsed,
|
||||
leftSidebarWidth: state.layout.leftSidebarWidth,
|
||||
rightSidebarCollapsed: state.layout.rightSidebarCollapsed,
|
||||
rightSidebarWidth: state.layout.rightSidebarWidth
|
||||
})
|
||||
);
|
||||
@@ -676,6 +680,26 @@ function setLeftSidebarCollapsed(collapsed, options = {}) {
|
||||
if (options.persist !== false) persistLayout();
|
||||
}
|
||||
|
||||
function initRightSidebarToggle() {
|
||||
el.rightSidebarToggle.addEventListener("click", () => {
|
||||
setRightSidebarCollapsed(!state.layout.rightSidebarCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
function setRightSidebarCollapsed(collapsed, options = {}) {
|
||||
state.layout.rightSidebarCollapsed = collapsed === true;
|
||||
el.shell.classList.toggle("is-right-sidebar-collapsed", state.layout.rightSidebarCollapsed);
|
||||
el.rightSidebarToggle.setAttribute("aria-expanded", state.layout.rightSidebarCollapsed ? "false" : "true");
|
||||
el.rightSidebarToggle.setAttribute(
|
||||
"aria-label",
|
||||
state.layout.rightSidebarCollapsed ? "展开右侧 Device Pod 看板" : "折叠右侧 Device Pod 看板"
|
||||
);
|
||||
el.rightSidebarToggle.title = state.layout.rightSidebarCollapsed ? "展开右侧 Device Pod 看板" : "折叠右侧 Device Pod 看板";
|
||||
el.rightSidebarToggle.textContent = state.layout.rightSidebarCollapsed ? "展开看板" : "收起看板";
|
||||
syncRightSidebarResizeA11y();
|
||||
if (options.persist !== false) persistLayout();
|
||||
}
|
||||
|
||||
function setLeftSidebarWidth(width, options = {}) {
|
||||
const clamped = clampLeftSidebarWidth(width);
|
||||
state.layout.leftSidebarWidth = clamped;
|
||||
@@ -906,7 +930,7 @@ function finishRightSidebarResize(event) {
|
||||
}
|
||||
|
||||
function canResizeRightSidebar() {
|
||||
return !window.matchMedia("(max-width: 1240px)").matches;
|
||||
return !state.layout.rightSidebarCollapsed && !window.matchMedia("(max-width: 1240px)").matches;
|
||||
}
|
||||
|
||||
async function loadHelpSurface() {
|
||||
|
||||
@@ -63,26 +63,6 @@
|
||||
></div>
|
||||
|
||||
<section class="center-workspace">
|
||||
<header class="topbar">
|
||||
<div class="topbar-main">
|
||||
<p class="eyebrow">用户工作台</p>
|
||||
<h2>Agent 对话与 Device Pod 看板</h2>
|
||||
<p id="route-path" class="route-path">当前浏览器会话只保存任务草稿,Device Pod 操作需通过受控后端流程。</p>
|
||||
</div>
|
||||
<div class="probe-card user-card" aria-live="polite">
|
||||
<span class="metric-label">工作区状态</span>
|
||||
<strong id="live-status" class="tone-pending">等待验证</strong>
|
||||
<small id="live-detail">正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/device-pods;未完成前不标为通过。</small>
|
||||
<details class="live-build-summary" id="live-build-summary">
|
||||
<summary id="live-build-toggle" aria-haspopup="dialog" aria-expanded="false">
|
||||
<span class="live-build-summary-label" id="live-build-latest">最新镜像构建时间:等待实时元数据</span>
|
||||
</summary>
|
||||
</details>
|
||||
<div class="live-build-list live-build-list-template" id="live-build-list" hidden aria-label="live HWLAB 微服务构建时间明细"></div>
|
||||
</div>
|
||||
<button class="logout-button" id="logout-button" type="button">退出</button>
|
||||
</header>
|
||||
|
||||
<section class="view workbench-view" id="workspace" data-view="workspace" aria-labelledby="workspace-title">
|
||||
<div class="conversation-column">
|
||||
<div class="workspace-panel conversation-panel">
|
||||
@@ -241,12 +221,12 @@
|
||||
</label>
|
||||
<div class="input-shell">
|
||||
<span class="prompt-mark">hwlab</span>
|
||||
<input
|
||||
<textarea
|
||||
id="command-input"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
rows="1"
|
||||
placeholder="输入要发送给 Code Agent 的消息;不会直接触发硬件变更。"
|
||||
/>
|
||||
></textarea>
|
||||
</div>
|
||||
<button class="command-button" id="command-send" type="submit">发送</button>
|
||||
<button class="command-button secondary" id="command-clear" type="button">清空</button>
|
||||
@@ -254,6 +234,34 @@
|
||||
</section>
|
||||
|
||||
<aside class="right-sidebar" id="device-pod-sidebar" aria-label="Device Pod 摘要看板">
|
||||
<div class="right-sidebar-control-row">
|
||||
<button
|
||||
class="right-sidebar-toggle"
|
||||
id="right-sidebar-toggle"
|
||||
type="button"
|
||||
aria-controls="device-pod-sidebar"
|
||||
aria-expanded="true"
|
||||
aria-label="折叠右侧 Device Pod 看板"
|
||||
title="折叠右侧 Device Pod 看板"
|
||||
>收起看板</button>
|
||||
<button class="logout-button" id="logout-button" type="button">退出</button>
|
||||
</div>
|
||||
<header class="sidebar-workbench-header">
|
||||
<p class="eyebrow">用户工作台</p>
|
||||
<h2>Agent 对话与 Device Pod 看板</h2>
|
||||
<p id="route-path" class="route-path">当前浏览器会话只保存任务草稿,Device Pod 操作需通过受控后端流程。</p>
|
||||
</header>
|
||||
<div class="probe-card user-card" aria-live="polite">
|
||||
<span class="metric-label">工作区状态</span>
|
||||
<strong id="live-status" class="tone-pending">等待验证</strong>
|
||||
<small id="live-detail">正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/device-pods;未完成前不标为通过。</small>
|
||||
<div class="live-build-summary" id="live-build-summary">
|
||||
<button class="live-build-toggle" id="live-build-toggle" type="button" aria-haspopup="dialog" aria-expanded="false">
|
||||
<span class="live-build-summary-label" id="live-build-latest">最新镜像构建时间:等待实时元数据</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="live-build-list live-build-list-template" id="live-build-list" hidden aria-label="live HWLAB 微服务构建时间明细"></div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-handle right-sidebar-resize"
|
||||
id="right-sidebar-resize"
|
||||
|
||||
@@ -97,12 +97,6 @@ for (const selector of [
|
||||
assertIncludes(styles, selector, `missing Device Pod style ${selector}`);
|
||||
}
|
||||
assert.match(styles, /\.device-event-scroll\s*\{[\s\S]*?overscroll-behavior:\s*contain;/u, "event stream scroll must be contained");
|
||||
assert.match(styles, /\.right-sidebar\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\);[^}]*overflow-x:\s*hidden;[^}]*overflow-y:\s*auto;[^}]*scrollbar-gutter:\s*stable;[^}]*\}/u, "right sidebar must be vertically scrollable and leave remaining space to the event stream");
|
||||
assert.match(styles, /\.device-pod-status\s*\{[^}]*grid-template-rows:\s*auto auto auto;[^}]*align-content:\s*start;[^}]*\}/u, "Device Pod summary section must stay content-sized");
|
||||
assert.match(styles, /\.device-pod-workspace\s*\{[^}]*grid-template-rows:\s*auto minmax\(180px, 1fr\) auto;[^}]*\}/u, "Device Pod workspace must reserve vertical space for the event stream");
|
||||
assert.match(styles, /\.device-pod-summary \.kv-list\s*\{[^}]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\);[^}]*\}/u, "Pod Summary key/value rows must use a compact two-column layout");
|
||||
assert.match(styles, /\.device-event-panel\s*\{[^}]*min-height:\s*180px;[^}]*\}/u, "event panel must keep a usable minimum height");
|
||||
assert.match(styles, /\.device-event-scroll\s*\{[^}]*min-height:\s*132px;[^}]*overflow:\s*auto;[^}]*\}/u, "event stream must keep a usable scroll viewport");
|
||||
|
||||
for (const fn of [
|
||||
"initDevicePodPanel",
|
||||
|
||||
+162
-29
@@ -26,6 +26,7 @@
|
||||
--right-sidebar-max-width: 740px;
|
||||
--right-width: clamp(var(--right-sidebar-min-width), var(--right-sidebar-width), var(--right-sidebar-max-width));
|
||||
--right-width-expanded: clamp(var(--right-sidebar-min-width), var(--right-sidebar-width), var(--right-sidebar-max-width));
|
||||
--right-collapsed-width: 46px;
|
||||
--rail-collapsed-width: 44px;
|
||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--body: "Aptos", "Segoe UI", system-ui, sans-serif;
|
||||
@@ -103,6 +104,10 @@ ul {
|
||||
background: rgba(15, 17, 16, 0.88);
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed {
|
||||
--right-width-expanded: var(--right-collapsed-width);
|
||||
}
|
||||
|
||||
.workbench-shell[hidden],
|
||||
body > [data-app-shell][hidden] {
|
||||
display: none;
|
||||
@@ -357,6 +362,10 @@ body > [data-app-shell][hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar-resize {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-sidebar-resize {
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -473,12 +482,11 @@ h3 {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 0.76fr) minmax(210px, 0.54fr) minmax(320px, 1fr);
|
||||
grid-template-columns: minmax(310px, 0.9fr) minmax(320px, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.skill-upload-panel,
|
||||
.skill-detail-panel,
|
||||
.skill-preview-panel {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -488,11 +496,15 @@ h3 {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skill-upload-panel,
|
||||
.skill-detail-panel {
|
||||
.skill-upload-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.skill-detail-panel,
|
||||
.skill-tree.is-detached {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skill-upload-form {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
@@ -550,6 +562,16 @@ h3 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skill-row-tree {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
max-height: min(360px, 38dvh);
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--line);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.skill-row:hover,
|
||||
.skill-row:focus-visible,
|
||||
.skill-row.active {
|
||||
@@ -661,7 +683,7 @@ h3 {
|
||||
|
||||
.center-workspace {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -727,11 +749,15 @@ h3 {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.live-build-summary summary {
|
||||
.live-build-toggle {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
@@ -740,13 +766,19 @@ h3 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.live-build-summary summary::marker {
|
||||
.live-build-toggle::before {
|
||||
content: "›";
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.live-build-toggle:hover,
|
||||
.live-build-toggle:focus-visible {
|
||||
color: var(--accent-2);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.live-build-summary-label {
|
||||
pointer-events: none;
|
||||
min-width: 0;
|
||||
@@ -1634,11 +1666,17 @@ h3 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.info-detail,
|
||||
.info-detail {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.command-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(142px, 176px)) minmax(0, 1fr) auto auto;
|
||||
grid-template-areas:
|
||||
"profile agent gateway . . ."
|
||||
"input input input input send clear";
|
||||
align-items: start;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(20, 22, 20, 0.98);
|
||||
@@ -1646,9 +1684,7 @@ h3 {
|
||||
}
|
||||
|
||||
.agent-timeout-control {
|
||||
order: 1;
|
||||
flex: 0 1 176px;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
@@ -1664,6 +1700,18 @@ h3 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.agent-timeout-control[for="code-agent-provider-profile"] {
|
||||
grid-area: profile;
|
||||
}
|
||||
|
||||
.agent-timeout-control[for="code-agent-timeout"] {
|
||||
grid-area: agent;
|
||||
}
|
||||
|
||||
.agent-timeout-control[for="gateway-shell-timeout"] {
|
||||
grid-area: gateway;
|
||||
}
|
||||
|
||||
.agent-timeout-control span {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
@@ -1681,8 +1729,7 @@ h3 {
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
order: 2;
|
||||
flex: 1 1 320px;
|
||||
grid-area: input;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
@@ -1699,22 +1746,26 @@ h3 {
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.input-shell input {
|
||||
.input-shell textarea {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
max-height: calc(1.35em * 5 + 14px);
|
||||
padding: 8px 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.input-shell input::placeholder {
|
||||
.input-shell textarea::placeholder {
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.command-button {
|
||||
order: 3;
|
||||
flex: 0 0 auto;
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--accent);
|
||||
@@ -1724,8 +1775,16 @@ h3 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#command-send {
|
||||
grid-area: send;
|
||||
}
|
||||
|
||||
#command-clear {
|
||||
grid-area: clear;
|
||||
}
|
||||
|
||||
.command-button:disabled,
|
||||
.input-shell input:disabled {
|
||||
.input-shell textarea:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.68;
|
||||
}
|
||||
@@ -1738,7 +1797,7 @@ h3 {
|
||||
.right-sidebar {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 10px 10px 10px 16px;
|
||||
background: rgba(18, 20, 18, 0.98);
|
||||
@@ -1749,6 +1808,71 @@ h3 {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.right-sidebar-control-row {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.right-sidebar-toggle {
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.right-sidebar-toggle:hover,
|
||||
.right-sidebar-toggle:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.sidebar-workbench-header {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(24, 27, 24, 0.76);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.sidebar-workbench-header h2 {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar {
|
||||
padding: 8px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar-control-row {
|
||||
writing-mode: vertical-rl;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar .logout-button,
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar > :not(.right-sidebar-control-row) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workbench-shell.is-right-sidebar-collapsed .right-sidebar-toggle {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: 8px 2px;
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-hot);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
|
||||
.kv-list {
|
||||
min-width: 0;
|
||||
@@ -2149,7 +2273,7 @@ tbody tr:last-child td {
|
||||
|
||||
.skills-layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto minmax(160px, 0.7fr) minmax(220px, 1fr);
|
||||
grid-template-rows: auto minmax(220px, 1fr);
|
||||
}
|
||||
|
||||
.skill-upload-form {
|
||||
@@ -2162,17 +2286,21 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
.command-bar {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"profile agent"
|
||||
"gateway gateway"
|
||||
"input input"
|
||||
"send clear";
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.agent-timeout-control {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
flex-basis: 100%;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@@ -2277,11 +2405,17 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
.command-bar {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"profile profile"
|
||||
"agent agent"
|
||||
"gateway gateway"
|
||||
"input input"
|
||||
"send clear";
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.agent-timeout-control {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -2294,12 +2428,11 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
flex-basis: 100%;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.skills-actions,
|
||||
|
||||
Reference in New Issue
Block a user