import { ensureWorkbenchAuth, initWorkbenchLogout } from "./auth.ts"; import { codeAgentRuntimePathFromMessage, runnerTraceSummary as compactRunnerTraceSummary, skillsSummary as compactSkillsSummary, toolCallsSummary as compactToolCallsSummary } from "./code-agent-facts.ts"; import { classifyCodeAgentStatusSummary, codeAgentAvailabilityFromLive } from "./code-agent-status.ts"; import { classifyWorkbenchLiveStatus } from "./live-status.ts"; import { marked } from "./third_party/marked/marked.esm.js"; import { hardenRenderedMarkdown, renderMessageMarkdown } from "./message-markdown.ts"; const DEFAULT_API_TIMEOUT_MS = 4500; const WORKBENCH_PROJECT_ID = "prj_device_pod_workbench"; const DEFAULT_LIVE_SURFACE_TIMEOUT_MS = 12000; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1800000; const MAX_CODE_AGENT_TIMEOUT_MS = 2400000; const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000; const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000; const DEFAULT_GATEWAY_SHELL_TIMEOUT_MS = 120000; const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const GATEWAY_SHELL_TIMEOUT_STORAGE_KEY = "hwlab.workbench.gatewayShellTimeoutMs.v1"; const CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY = "hwlab.workbench.codeAgentProviderProfile.v1"; const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek"; const CODE_AGENT_PROVIDER_PROFILES = Object.freeze(["deepseek", "codex-api"]); const CODE_AGENT_SESSION_STORAGE_KEY = "hwlab.workbench.codeAgentSession.v1"; const CODE_AGENT_SESSION_STORAGE_VERSION = 1; const CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; const CODE_AGENT_SESSION_STORAGE_MESSAGE_LIMIT = 30; const CODE_AGENT_SESSION_STORAGE_TRACE_EVENT_LIMIT = 80; const CODE_AGENT_SESSION_PERSIST_DEBOUNCE_MS = 250; const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const FULL_TRACE_REPLAY_TIMEOUT_MS = 15000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); const LIVE_SURFACE_TIMEOUT_MS = resolveTimeoutMs("liveSurfaceTimeoutMs", DEFAULT_LIVE_SURFACE_TIMEOUT_MS, { min: 5000, max: 60000 }); let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs(); let GATEWAY_SHELL_TIMEOUT_MS = resolveUserGatewayShellTimeoutMs(); let CODE_AGENT_PROVIDER_PROFILE = resolveUserCodeAgentProviderProfile(); const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: configuredTimeoutMinMs("codeAgentSubmitTimeoutMs", 5000), max: 120000 }); const CODE_AGENT_CANCEL_TIMEOUT_MS = resolveTimeoutMs("codeAgentCancelTimeoutMs", DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS, { min: 5000, max: 120000 }); const rpcReadMethods = Object.freeze([ "system.health", "cloud.adapter.describe" ]); const STATUS_LABELS = Object.freeze({ active: "活动", available: "可用", blocked_after_cloud_api: "cloud-api 后阻塞", blocked: "待处理", canceled: "已取消", completed: "完成", connected: "已连接", degraded: "降级", disabled: "禁用", entry: "入口", failed: "失败", healthy: "健康", live: "实况", ok: "正常", open: "未关闭", pass: "通过", pending: "等待", running: "处理中", timeout: "超时", error: "错误", probing: "探测中", ready: "就绪", recorded: "已记录", sent: "已发送", requires: "依赖", source: "来源", unverified: "未验证", "dry-run": "演练", "dev-live": "实况已验证" }); const TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["codex-stdio"]); const CODEX_RUNNER_CAPABLE_PROVIDERS = Object.freeze(["codex-stdio"]); const CODEX_APP_SERVER_RUNNER_KIND = "codex-app-server-stdio-runner"; const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived"; const CODEX_APP_SERVER_IMPLEMENTATION_TYPE = "repo-owned-codex-app-server-stdio-session"; const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio"; const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([ "codex_cli_binary_missing", "runner_lifecycle_missing", "stdio_protocol_not_wired", "codex_stdio_supervisor_disabled", "not-codex-stdio", "not-write-capable", "process-local-session-registry" ]); const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu; const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1"; const LEFT_SIDEBAR_DEFAULT_WIDTH = 92; const LEFT_SIDEBAR_MIN_WIDTH = 72; const LEFT_SIDEBAR_MAX_WIDTH = 180; const RIGHT_SIDEBAR_DEFAULT_WIDTH = 728; const RIGHT_SIDEBAR_MIN_WIDTH = 560; const RIGHT_SIDEBAR_MAX_WIDTH = 740; const SCROLL_BOTTOM_PIN_PX = 24; const SCROLL_USER_ACTIVITY_MS = 700; const MESSAGE_MARKDOWN_STATUSES = Object.freeze(["completed", "source"]); const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); let rpcSequence = 0; const el = { loginShell: byId("login-shell"), shell: query("[data-app-shell]"), logoutButton: byId("logout-button"), leftSidebarToggle: byId("left-sidebar-toggle"), leftSidebarResize: byId("left-sidebar-resize"), rightSidebarResize: byId("right-sidebar-resize"), routePath: byId("route-path"), liveStatus: byId("live-status"), liveDetail: byId("live-detail"), liveBuildSummary: byId("live-build-summary"), liveBuildToggle: byId("live-build-toggle"), liveBuildLatest: byId("live-build-latest"), liveBuildList: byId("live-build-list"), agentChatStatus: byId("agent-chat-status"), codeAgentSummary: byId("code-agent-summary"), codeAgentSummaryIcon: byId("code-agent-summary-icon"), codeAgentSummaryLabel: byId("code-agent-summary-label"), codeAgentSummaryCapability: byId("code-agent-summary-capability"), codeAgentSummaryTrace: byId("code-agent-summary-trace"), codeAgentSummaryDetail: byId("code-agent-summary-detail"), conversationList: byId("conversation-list"), gateSourceStatus: byId("gate-source-status"), gateStatusFilter: byId("gate-status-filter"), gateSearch: byId("gate-search"), gateRefresh: byId("gate-refresh"), gateReviewBody: byId("gate-review-body"), skillStatus: byId("skill-status"), skillRefresh: byId("skill-refresh"), skillUploadForm: byId("skill-upload-form"), skillUploadInput: byId("skill-upload-input"), skillUploadButton: byId("skill-upload-button"), skillList: byId("skill-list"), skillDetailTitle: byId("skill-detail-title"), skillTree: byId("skill-tree"), skillPreview: byId("skill-preview"), commandForm: byId("command-form"), commandInput: byId("command-input"), codeAgentProviderProfile: byId("code-agent-provider-profile"), codeAgentTimeout: byId("code-agent-timeout"), gatewayShellTimeout: byId("gateway-shell-timeout"), commandSend: byId("command-send"), commandClear: byId("command-clear"), devicePodSelect: byId("device-pod-select"), devicePodStatusTag: byId("device-pod-status-tag"), devicePodSummary: byId("device-pod-summary"), devicePodId: byId("device-pod-id"), devicePodTarget: byId("device-pod-target"), devicePodProfile: byId("device-pod-profile"), devicePodFreshness: byId("device-pod-freshness"), devicePodInterfaces: byId("device-pod-interfaces"), deviceEventFollow: byId("device-event-follow"), deviceEventJump: byId("device-event-jump"), deviceEventScroll: byId("device-event-scroll"), deviceEventText: byId("device-event-text"), deviceDetailDialog: byId("device-detail-dialog"), deviceDetailTitle: byId("device-detail-title"), deviceDetailBody: byId("device-detail-body"), helpContent: byId("help-content"), helpStatus: byId("help-status") }; let activeWorkbenchDialog = null; await ensureWorkbenchAuth({ loginShell: el.loginShell, appShell: el.shell }); const state = { conversationId: null, sessionId: null, threadId: null, codeAgentAvailability: null, chatMessages: [], traceStreams: new Map(), traceDetailsOpen: new Map(), conversationRenderVersion: 0, conversationScrollUserActiveUntil: 0, conversationScrollPosition: { top: 0, left: 0 }, traceScrollPositions: new Map(), traceScrollUserActiveUntil: new Map(), traceBodyScrollPositions: new Map(), traceBodyScrollUserActiveUntil: new Map(), traceProgrammaticScrollWrites: 0, fullTraceReplayInFlight: new Set(), resultReconciliationInFlight: new Set(), canceledTraces: new Set(), currentRequest: null, sessionStatus: null, chatPending: false, liveSurface: null, gateDiagnostics: { loading: false, payload: null, rows: [], error: null, loadedAt: null }, skills: { loading: false, uploadPending: false, detailLoading: false, previewLoading: false, items: [], roots: null, selectedId: null, detail: null, preview: null, error: null }, devicePod: { selectedDevicePodId: "device-pod-71-freq", list: null, status: null, events: null, chipId: null, uart: null, uartTail: null, followEvents: true, unreadEvents: 0, lastEventLineCount: 0, eventScrollUserActiveUntil: 0, details: new Map() }, layout: { leftSidebarCollapsed: false, leftSidebarWidth: LEFT_SIDEBAR_DEFAULT_WIDTH, leftDrag: null, rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH, rightDrag: null } }; let codeAgentSessionPersistTimer = null; restoreCodeAgentSessionState(); initRoutes(); initLayoutSizing(); initLeftSidebarToggle(); initLeftSidebarResize(); initRightSidebarResize(); initDevicePodPanel(); initSkillsPanel(); initCodeAgentTimeoutControl(); initConversationScrollMemory(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); el.logoutButton.addEventListener("click", clearCodeAgentSessionState); initGateControls(); installWorkbenchTestHooks(); renderStaticWorkbench(); renderProbePending(); renderCodeAgentSummary(); loadHelpSurface(); loadLiveSurface().then(renderLiveSurface); window.setTimeout(refreshRestoredCodeAgentTraces, 0); function byId(id) { const element = document.getElementById(id); if (!element) { throw new Error(`missing required element #${id}`); } return element; } function query(selector) { const element = document.querySelector(selector); if (!element) { throw new Error(`missing required element ${selector}`); } return element; } function resolveTimeoutMs(name, fallback, { min, max }) { const config = globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? globalThis.HWLAB_CLOUD_WEB_TIMEOUTS ?? {}; const configured = Number(config?.[name]); if (!Number.isFinite(configured)) return fallback; return Math.min(Math.max(Math.trunc(configured), min), max); } function configuredTimeoutMinMs(name, fallback) { const config = globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? globalThis.HWLAB_CLOUD_WEB_TIMEOUTS ?? {}; const configured = Number(config?.[`${name}MinMs`]); if (!Number.isFinite(configured)) return fallback; return Math.max(1, Math.min(Math.trunc(configured), fallback)); } function clampTimeoutMs(value, { min, max }) { if (value === null || value === undefined) return null; if (typeof value === "string" && value.trim() === "") return null; const numeric = Number(value); if (!Number.isFinite(numeric)) return null; return Math.min(Math.max(Math.trunc(numeric), min), max); } function resolveUserCodeAgentTimeoutMs() { const min = configuredTimeoutMinMs("codeAgentTimeoutMs", 30000); let stored = null; try { stored = window.localStorage?.getItem(CODE_AGENT_TIMEOUT_STORAGE_KEY) ?? null; } catch { stored = null; } return clampTimeoutMs(stored, { min, max: MAX_CODE_AGENT_TIMEOUT_MS }) ?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min, max: MAX_CODE_AGENT_TIMEOUT_MS }); } function resolveUserGatewayShellTimeoutMs() { let stored = null; try { stored = window.localStorage?.getItem(GATEWAY_SHELL_TIMEOUT_STORAGE_KEY) ?? null; } catch { stored = null; } return clampTimeoutMs(stored, { min: 30000, max: 600000 }) ?? resolveTimeoutMs("gatewayShellTimeoutMs", DEFAULT_GATEWAY_SHELL_TIMEOUT_MS, { min: 30000, max: 600000 }); } function resolveUserCodeAgentProviderProfile() { let stored = null; try { stored = window.localStorage?.getItem(CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY) ?? null; } catch { stored = null; } return normalizeCodeAgentProviderProfile(stored); } function normalizeCodeAgentProviderProfile(value) { const text = String(value ?? DEFAULT_CODE_AGENT_PROVIDER_PROFILE).trim().toLowerCase(); return CODE_AGENT_PROVIDER_PROFILES.includes(text) ? text : DEFAULT_CODE_AGENT_PROVIDER_PROFILE; } function initCodeAgentTimeoutControl() { syncCodeAgentTimeoutControl(); el.codeAgentProviderProfile.addEventListener("change", () => { CODE_AGENT_PROVIDER_PROFILE = normalizeCodeAgentProviderProfile(el.codeAgentProviderProfile.value); try { window.localStorage?.setItem(CODE_AGENT_PROVIDER_PROFILE_STORAGE_KEY, CODE_AGENT_PROVIDER_PROFILE); } catch { // localStorage can be unavailable in hardened browser modes. } syncCodeAgentTimeoutControl(); renderCodeAgentSummary(); }); el.codeAgentTimeout.addEventListener("change", () => { const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: MAX_CODE_AGENT_TIMEOUT_MS }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS; CODE_AGENT_TIMEOUT_MS = next; try { window.localStorage?.setItem(CODE_AGENT_TIMEOUT_STORAGE_KEY, String(next)); } catch { // localStorage can be unavailable in hardened browser modes. } syncCodeAgentTimeoutControl(); renderCodeAgentSummary(); renderConversation(); }); el.gatewayShellTimeout.addEventListener("change", () => { const next = clampTimeoutMs(el.gatewayShellTimeout.value, { min: 30000, max: 600000 }) ?? DEFAULT_GATEWAY_SHELL_TIMEOUT_MS; GATEWAY_SHELL_TIMEOUT_MS = next; try { window.localStorage?.setItem(GATEWAY_SHELL_TIMEOUT_STORAGE_KEY, String(next)); } catch { // localStorage can be unavailable in hardened browser modes. } syncCodeAgentTimeoutControl(); renderCodeAgentSummary(); }); } function syncCodeAgentTimeoutControl() { el.codeAgentProviderProfile.value = CODE_AGENT_PROVIDER_PROFILE; el.codeAgentProviderProfile.title = CODE_AGENT_PROVIDER_PROFILE === "codex-api" ? "本次 Code Agent 请求使用原 Codex API / Responses 通道" : "本次 Code Agent 请求使用 G14 DeepSeek Responses bridge"; el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS); el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`; el.gatewayShellTimeout.value = String(GATEWAY_SHELL_TIMEOUT_MS); el.gatewayShellTimeout.title = `Code Agent 调用 PC gateway wrapper 时默认使用 --timeout-ms ${GATEWAY_SHELL_TIMEOUT_MS}`; } function restoreCodeAgentSessionState() { let payload = null; try { const raw = window.localStorage?.getItem(CODE_AGENT_SESSION_STORAGE_KEY); payload = raw ? JSON.parse(raw) : null; } catch { payload = null; } if (!payload || payload.version !== CODE_AGENT_SESSION_STORAGE_VERSION) return; const updatedAtMs = Number(payload.updatedAtMs); if (!Number.isFinite(updatedAtMs) || Date.now() - updatedAtMs > CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS) { clearCodeAgentSessionState(); return; } state.conversationId = nonEmptyString(payload.conversationId); state.sessionId = nonEmptyString(payload.sessionId); state.threadId = nonEmptyString(payload.threadId); state.sessionStatus = nonEmptyString(payload.sessionStatus); state.chatMessages = Array.isArray(payload.chatMessages) ? payload.chatMessages.map(restoreStoredChatMessage).filter(Boolean) : []; reconcileRestoredCodeAgentResults(); } function restoreStoredChatMessage(message) { if (!message || typeof message !== "object") return null; const restored = { ...message }; restored.id = nonEmptyString(restored.id) ?? nextProtocolId("msg"); restored.role = restored.role === "user" ? "user" : restored.role === "system" ? "system" : "agent"; restored.title = nonEmptyString(restored.title) ?? (restored.role === "user" ? "用户" : "Agent"); restored.text = typeof restored.text === "string" ? restored.text : ""; restored.status = nonEmptyString(restored.status) ?? "source"; if (restored.status === "running") restored.status = "source"; restored.conversationId = nonEmptyString(restored.conversationId) ?? state.conversationId; restored.sessionId = nonEmptyString(restored.sessionId) ?? state.sessionId; restored.threadId = nonEmptyString(restored.threadId) ?? state.threadId; restored.traceId = nonEmptyString(restored.traceId); if (restored.runnerTrace && typeof restored.runnerTrace === "object") { restored.runnerTrace = restoreStoredRunnerTrace(restored.runnerTrace); } return restored; } function restoreStoredRunnerTrace(trace) { return { ...trace, events: Array.isArray(trace.events) ? trace.events : [], eventCount: Number.isInteger(trace.eventCount) ? trace.eventCount : Array.isArray(trace.events) ? trace.events.length : 0, eventsCompacted: trace.eventsCompacted === true, fullTraceLoaded: trace.fullTraceLoaded === true }; } function scheduleCodeAgentSessionPersist() { if (codeAgentSessionPersistTimer !== null) window.clearTimeout(codeAgentSessionPersistTimer); codeAgentSessionPersistTimer = window.setTimeout(() => { codeAgentSessionPersistTimer = null; persistCodeAgentSessionState(); }, CODE_AGENT_SESSION_PERSIST_DEBOUNCE_MS); } function persistCodeAgentSessionState() { if (!state.conversationId && !state.sessionId && !state.threadId && state.chatMessages.length === 0) { clearCodeAgentSessionState(); return; } const payload = { version: CODE_AGENT_SESSION_STORAGE_VERSION, updatedAtMs: Date.now(), conversationId: state.conversationId, sessionId: state.sessionId, threadId: state.threadId, sessionStatus: state.sessionStatus, chatMessages: state.chatMessages.slice(-CODE_AGENT_SESSION_STORAGE_MESSAGE_LIMIT).map(storedChatMessage) }; try { window.localStorage?.setItem(CODE_AGENT_SESSION_STORAGE_KEY, JSON.stringify(payload)); } catch { // Keep the in-memory conversation when localStorage is unavailable or full. } } function clearCodeAgentSessionState() { if (codeAgentSessionPersistTimer !== null) { window.clearTimeout(codeAgentSessionPersistTimer); codeAgentSessionPersistTimer = null; } try { window.localStorage?.removeItem(CODE_AGENT_SESSION_STORAGE_KEY); } catch { // Ignore storage availability errors. } } function storedChatMessage(message) { const stored = { ...message }; if (stored.runnerTrace && typeof stored.runnerTrace === "object") { stored.runnerTrace = storedRunnerTrace(stored.runnerTrace); } delete stored.availability; return stored; } function storedRunnerTrace(trace) { const events = Array.isArray(trace.events) ? trace.events : []; const storedEvents = trace.fullTraceLoaded === true && events.length <= CODE_AGENT_SESSION_STORAGE_TRACE_EVENT_LIMIT ? events : []; const fullTraceLoaded = storedEvents.length > 0 && trace.fullTraceLoaded === true; return { ...trace, events: storedEvents, eventsCompacted: !fullTraceLoaded, fullTraceLoaded }; } function refreshRestoredCodeAgentTraces() { for (const message of state.chatMessages) { if (!message.traceId) continue; if (message.runnerTrace?.fullTraceLoaded === true) continue; replayFullTrace(message.id, { quiet: true }); } } function reconcileRestoredCodeAgentResults() { for (const message of state.chatMessages) { if (!message.traceId || message.role !== "agent") continue; if (!shouldReconcileRestoredCodeAgentResult(message)) continue; reconcileCodeAgentResult(message.id, { quiet: true }); } } function initLiveBuildOverlay() { el.liveBuildSummary.open = false; el.liveBuildToggle.addEventListener("click", (event) => { event.preventDefault(); el.liveBuildSummary.open = false; openWorkbenchDialog({ title: "构建版本明细", body: cloneLiveBuildList(), restoreFocus: el.liveBuildToggle }); }); } function cloneLiveBuildList() { const clone = el.liveBuildList.cloneNode(true); clone.id = ""; clone.hidden = false; clone.removeAttribute("hidden"); clone.tabIndex = 0; clone.classList.remove("live-build-list-template"); return clone; } function openWorkbenchDialog({ title, body, restoreFocus = null }) { closeWorkbenchDialog({ restoreFocus: false }); const layer = document.createElement("div"); layer.className = "workbench-dialog-layer"; const backdrop = document.createElement("button"); backdrop.type = "button"; backdrop.className = "workbench-dialog-backdrop"; backdrop.setAttribute("aria-label", "关闭弹窗"); const dialog = document.createElement("section"); dialog.className = "workbench-dialog"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.setAttribute("aria-label", title); const head = document.createElement("div"); head.className = "workbench-dialog-head"; const heading = document.createElement("strong"); heading.textContent = title; const close = document.createElement("button"); close.type = "button"; close.className = "workbench-dialog-close"; close.textContent = "关闭"; close.setAttribute("aria-label", `关闭${title}`); const content = document.createElement("div"); content.className = "workbench-dialog-content"; if (body) { body.hidden = false; body.removeAttribute("hidden"); content.append(body); } head.append(heading, close); dialog.append(head, content); layer.append(backdrop, dialog); const closeDialog = () => closeWorkbenchDialog({ restoreFocus: true }); const keydown = (event) => { if (event.key !== "Escape") return; event.preventDefault(); closeDialog(); }; close.addEventListener("click", closeDialog); backdrop.addEventListener("click", closeDialog); document.addEventListener("keydown", keydown); document.body.append(layer); activeWorkbenchDialog = { layer, keydown, restoreFocus }; close.focus(); } function closeWorkbenchDialog({ restoreFocus = true } = {}) { if (!activeWorkbenchDialog) return; const { layer, keydown, restoreFocus: target } = activeWorkbenchDialog; document.removeEventListener("keydown", keydown); layer.remove(); activeWorkbenchDialog = null; if (restoreFocus && target && typeof target.focus === "function") { target.focus(); } } function initRoutes() { window.addEventListener("hashchange", () => showView(routeFromLocation())); for (const button of document.querySelectorAll("[data-route]")) { button.addEventListener("click", () => { window.location.hash = `/${button.dataset.route}`; }); } showView(routeFromLocation()); } function initLayoutSizing() { const persisted = readPersistedLayout(); const leftSidebarWidth = clampLeftSidebarWidth(persisted?.leftSidebarWidth ?? LEFT_SIDEBAR_DEFAULT_WIDTH); const rightSidebarWidth = clampRightSidebarWidth(persisted?.rightSidebarWidth ?? RIGHT_SIDEBAR_DEFAULT_WIDTH); setLeftSidebarWidth(leftSidebarWidth, { persist: false }); setLeftSidebarCollapsed(persisted?.leftSidebarCollapsed === true, { persist: false }); setRightSidebarWidth(rightSidebarWidth, { persist: false }); } function readPersistedLayout() { try { const raw = window.localStorage?.getItem(LAYOUT_STORAGE_KEY); if (!raw) return null; const payload = JSON.parse(raw); if (payload?.version !== 1) return null; return { leftSidebarCollapsed: payload.leftSidebarCollapsed === true, leftSidebarWidth: Number(payload.leftSidebarWidth), rightSidebarWidth: Number(payload.rightSidebarWidth) }; } catch { return null; } } function persistLayout() { try { window.localStorage?.setItem( LAYOUT_STORAGE_KEY, JSON.stringify({ version: 1, leftSidebarCollapsed: state.layout.leftSidebarCollapsed, leftSidebarWidth: state.layout.leftSidebarWidth, rightSidebarWidth: state.layout.rightSidebarWidth }) ); } catch { // Browser storage can be unavailable; layout still works for the session. } } function initLeftSidebarToggle() { el.leftSidebarToggle.addEventListener("click", () => { setLeftSidebarCollapsed(!state.layout.leftSidebarCollapsed); }); } function setLeftSidebarCollapsed(collapsed, options = {}) { state.layout.leftSidebarCollapsed = collapsed === true; el.shell.classList.toggle("is-left-sidebar-collapsed", state.layout.leftSidebarCollapsed); el.leftSidebarToggle.setAttribute("aria-expanded", state.layout.leftSidebarCollapsed ? "false" : "true"); el.leftSidebarToggle.setAttribute( "aria-label", state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航" ); el.leftSidebarToggle.title = state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航"; el.leftSidebarToggle.textContent = state.layout.leftSidebarCollapsed ? "展开" : "收起"; syncLeftSidebarResizeA11y(); if (options.persist !== false) persistLayout(); } function setLeftSidebarWidth(width, options = {}) { const clamped = clampLeftSidebarWidth(width); state.layout.leftSidebarWidth = clamped; el.shell.style.setProperty("--left-sidebar-width", `${clamped}px`); syncLeftSidebarResizeA11y(); if (options.persist !== false) persistLayout(); return clamped; } function setRightSidebarWidth(width, options = {}) { const clamped = clampRightSidebarWidth(width); state.layout.rightSidebarWidth = clamped; el.shell.style.setProperty("--right-sidebar-width", `${clamped}px`); syncRightSidebarResizeA11y(); if (options.persist !== false) persistLayout(); return clamped; } function clampLeftSidebarWidth(value) { const numeric = Number(value); const width = Number.isFinite(numeric) ? numeric : LEFT_SIDEBAR_DEFAULT_WIDTH; const bounds = leftSidebarResizeBounds(); 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; const bounds = rightSidebarResizeBounds(); return Math.min(Math.max(Math.round(width), bounds.min), bounds.max); } 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))); return { min: safeMin, max: safeMax }; } function leftSidebarResizeBounds() { const styles = getComputedStyle(el.shell); const cssMin = readCssPixel(styles.getPropertyValue("--left-sidebar-min-width"), LEFT_SIDEBAR_MIN_WIDTH); const cssMax = readCssPixel(styles.getPropertyValue("--left-sidebar-max-width"), LEFT_SIDEBAR_MAX_WIDTH); if (window.matchMedia("(max-width: 1240px)").matches) { const fixedRailWidth = readCssPixel(styles.getPropertyValue("--rail-width"), LEFT_SIDEBAR_DEFAULT_WIDTH); return { min: Math.round(fixedRailWidth), max: Math.round(fixedRailWidth) }; } const rightWidth = state.layout.rightSidebarWidth; const dynamicMax = window.innerWidth - rightWidth - 420 - 4; return boundedResizeRange(cssMin, cssMax, dynamicMax); } function rightSidebarResizeBounds() { const styles = getComputedStyle(el.shell); const cssMin = readCssPixel(styles.getPropertyValue("--right-sidebar-min-width"), RIGHT_SIDEBAR_MIN_WIDTH); const cssMax = readCssPixel(styles.getPropertyValue("--right-sidebar-max-width"), RIGHT_SIDEBAR_MAX_WIDTH); if (window.matchMedia("(max-width: 1240px)").matches) { return { min: 0, max: Math.max(0, window.innerWidth) }; } const railWidth = state.layout.leftSidebarCollapsed ? readCssPixel(styles.getPropertyValue("--rail-collapsed-width"), LEFT_SIDEBAR_DEFAULT_WIDTH) : state.layout.leftSidebarWidth; const dynamicMax = window.innerWidth - railWidth - 420 - 4; return boundedResizeRange(cssMin, cssMax, dynamicMax); } function readCssPixel(value, fallback) { const parsed = Number.parseFloat(String(value ?? "").trim()); return Number.isFinite(parsed) ? parsed : fallback; } function syncRightSidebarResizeA11y() { const bounds = rightSidebarResizeBounds(); const disabled = !canResizeRightSidebar(); el.rightSidebarResize.tabIndex = disabled ? -1 : 0; el.rightSidebarResize.setAttribute("aria-disabled", disabled ? "true" : "false"); el.rightSidebarResize.setAttribute("aria-hidden", disabled ? "true" : "false"); el.rightSidebarResize.setAttribute("aria-valuemin", String(bounds.min)); el.rightSidebarResize.setAttribute("aria-valuemax", String(bounds.max)); el.rightSidebarResize.setAttribute("aria-valuenow", String(state.layout.rightSidebarWidth)); el.rightSidebarResize.setAttribute("aria-valuetext", `右侧 Device Pod 看板宽度 ${state.layout.rightSidebarWidth} 像素`); } function syncLeftSidebarResizeA11y() { const bounds = leftSidebarResizeBounds(); const disabled = !canResizeLeftSidebar(); el.leftSidebarResize.tabIndex = disabled ? -1 : 0; el.leftSidebarResize.setAttribute("aria-disabled", disabled ? "true" : "false"); el.leftSidebarResize.setAttribute("aria-hidden", disabled ? "true" : "false"); el.leftSidebarResize.setAttribute("aria-valuemin", String(bounds.min)); el.leftSidebarResize.setAttribute("aria-valuemax", String(bounds.max)); el.leftSidebarResize.setAttribute("aria-valuenow", String(state.layout.leftSidebarWidth)); el.leftSidebarResize.setAttribute("aria-valuetext", `左侧导航宽度 ${state.layout.leftSidebarWidth} 像素`); } function routeFromLocation() { const hashRoute = window.location.hash.replace(/^#\/?/, ""); if (viewIds.has(hashRoute)) return hashRoute; if (helpPathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "help"; if (skillsPathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "skills"; if (internalGatePathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "gate"; return "workspace"; } function showView(route) { const activeRoute = viewIds.has(route) ? route : "workspace"; for (const view of document.querySelectorAll("[data-view]")) { view.hidden = view.dataset.view !== activeRoute; } for (const button of document.querySelectorAll("[data-route]")) { const active = button.dataset.route === activeRoute; button.classList.toggle("active", active); button.setAttribute("aria-current", active ? "page" : "false"); } if (activeRoute === "gate" && state.gateDiagnostics.rows.length === 0 && !state.gateDiagnostics.loading) { loadGateDiagnostics(); } if (activeRoute === "skills" && state.skills.items.length === 0 && !state.skills.loading) { loadSkillsSurface(); } } function initLeftSidebarResize() { window.addEventListener("resize", () => { if (canResizeLeftSidebar()) setLeftSidebarWidth(state.layout.leftSidebarWidth, { persist: false }); syncLeftSidebarResizeA11y(); }); el.leftSidebarResize.addEventListener("pointerdown", (event) => { if (!canResizeLeftSidebar()) return; event.preventDefault(); el.leftSidebarResize.setPointerCapture?.(event.pointerId); state.layout.leftDrag = { pointerId: event.pointerId, startX: event.clientX, startWidth: state.layout.leftSidebarWidth }; el.shell.classList.add("is-resizing"); }); el.leftSidebarResize.addEventListener("pointermove", (event) => { const drag = state.layout.leftDrag; if (!drag || drag.pointerId !== event.pointerId) return; setLeftSidebarWidth(drag.startWidth + event.clientX - drag.startX, { persist: false }); }); el.leftSidebarResize.addEventListener("pointerup", finishLeftSidebarResize); el.leftSidebarResize.addEventListener("pointercancel", finishLeftSidebarResize); el.leftSidebarResize.addEventListener("keydown", (event) => { if (!canResizeLeftSidebar()) return; const keySteps = { ArrowLeft: -8, ArrowRight: 8, PageUp: 24, PageDown: -24 }; if (Object.hasOwn(keySteps, event.key)) { event.preventDefault(); setLeftSidebarWidth(state.layout.leftSidebarWidth + keySteps[event.key]); } else if (event.key === "Home") { event.preventDefault(); setLeftSidebarWidth(leftSidebarResizeBounds().min); } else if (event.key === "End") { event.preventDefault(); setLeftSidebarWidth(leftSidebarResizeBounds().max); } }); } function finishLeftSidebarResize(event) { const drag = state.layout.leftDrag; if (!drag || drag.pointerId !== event.pointerId) return; state.layout.leftDrag = null; el.shell.classList.remove("is-resizing"); persistLayout(); } function canResizeLeftSidebar() { return !state.layout.leftSidebarCollapsed && !window.matchMedia("(max-width: 1240px)").matches; } function initRightSidebarResize() { window.addEventListener("resize", () => { if (canResizeRightSidebar()) setRightSidebarWidth(state.layout.rightSidebarWidth, { persist: false }); syncRightSidebarResizeA11y(); }); el.rightSidebarResize.addEventListener("pointerdown", (event) => { if (!canResizeRightSidebar()) return; event.preventDefault(); el.rightSidebarResize.setPointerCapture?.(event.pointerId); state.layout.rightDrag = { pointerId: event.pointerId, startX: event.clientX, startWidth: state.layout.rightSidebarWidth }; el.shell.classList.add("is-resizing"); }); el.rightSidebarResize.addEventListener("pointermove", (event) => { const drag = state.layout.rightDrag; if (!drag || drag.pointerId !== event.pointerId) return; setRightSidebarWidth(drag.startWidth + drag.startX - event.clientX, { persist: false }); }); el.rightSidebarResize.addEventListener("pointerup", finishRightSidebarResize); el.rightSidebarResize.addEventListener("pointercancel", finishRightSidebarResize); el.rightSidebarResize.addEventListener("keydown", (event) => { if (!canResizeRightSidebar()) return; const keySteps = { ArrowLeft: 16, ArrowRight: -16, PageUp: 48, PageDown: -48 }; if (Object.hasOwn(keySteps, event.key)) { event.preventDefault(); setRightSidebarWidth(state.layout.rightSidebarWidth + keySteps[event.key]); } else if (event.key === "Home") { event.preventDefault(); setRightSidebarWidth(rightSidebarResizeBounds().min); } else if (event.key === "End") { event.preventDefault(); setRightSidebarWidth(rightSidebarResizeBounds().max); } }); } function finishRightSidebarResize(event) { const drag = state.layout.rightDrag; if (!drag || drag.pointerId !== event.pointerId) return; state.layout.rightDrag = null; el.shell.classList.remove("is-resizing"); persistLayout(); } function canResizeRightSidebar() { return !window.matchMedia("(max-width: 1240px)").matches; } async function loadHelpSurface() { el.helpStatus.textContent = "加载中"; el.helpStatus.className = "state-tag tone-dry-run"; el.helpContent.dataset.helpState = "loading"; try { const response = await fetch("/help.md", { cache: "no-store" }); if (!response.ok) { throw new Error(`help.md HTTP ${response.status}`); } const markdown = await response.text(); el.helpContent.innerHTML = marked.parse(markdown, { gfm: true, breaks: false }); el.helpContent.dataset.helpState = "ready"; el.helpStatus.textContent = "已就绪"; el.helpStatus.className = "state-tag tone-source"; } catch (error) { el.helpContent.dataset.helpState = "error"; el.helpContent.replaceChildren(); const notice = document.createElement("div"); notice.className = "help-error"; notice.textContent = `帮助内容加载失败:${error.message}。默认工作台仍可继续使用。`; el.helpContent.append(notice); el.helpStatus.textContent = "加载失败"; el.helpStatus.className = "state-tag tone-blocked"; } }