diff --git a/cmd/hwlab-cloud-api/main.mjs b/cmd/hwlab-cloud-api/main.mjs index c4e16be0..6b19241e 100644 --- a/cmd/hwlab-cloud-api/main.mjs +++ b/cmd/hwlab-cloud-api/main.mjs @@ -4,9 +4,9 @@ import { createCloudApiServer } from "../../internal/cloud/server.mjs"; const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0"; const port = Number.parseInt(process.env.HWLAB_CLOUD_API_PORT || process.env.PORT || "6667", 10); const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA; -const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 150000, { +const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 600000, { min: 1000, - max: 300000 + max: 900000 }); const codeAgentHardTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_HARD_TIMEOUT_MS, Math.max(600000, codeAgentTimeoutMs * 4), { min: codeAgentTimeoutMs, diff --git a/cmd/hwlab-edge-proxy/main.mjs b/cmd/hwlab-edge-proxy/main.mjs index a9051043..38ecd5c0 100644 --- a/cmd/hwlab-edge-proxy/main.mjs +++ b/cmd/hwlab-edge-proxy/main.mjs @@ -10,9 +10,9 @@ import { createServer } from "node:http"; const serviceId = "hwlab-edge-proxy"; const upstream = process.env.HWLAB_EDGE_UPSTREAM || "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"; -const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 180000, { +const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 660000, { min: 1000, - max: 300000 + max: 900000 }); const { host, port } = resolveHostPort({ listenEnv: "HWLAB_EDGE_LISTEN", diff --git a/deploy/deploy.json b/deploy/deploy.json index af685e4a..99d5a6f4 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -204,7 +204,7 @@ "HWLAB_M3_PATCH_PANEL_URL": "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301", "HWLAB_CODE_AGENT_PROVIDER": "codex-stdio", "HWLAB_CODE_AGENT_MODEL": "gpt-5.5", - "HWLAB_CODE_AGENT_TIMEOUT_MS": "150000", + "HWLAB_CODE_AGENT_TIMEOUT_MS": "600000", "HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://172.26.26.227:17680/v1/responses", "HWLAB_CODE_AGENT_HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1", @@ -230,7 +230,7 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "180000", + "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000", "HWLAB_COMMIT_ID": "4d478a1", "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:4d478a1", "HWLAB_IMAGE_TAG": "4d478a1" @@ -368,7 +368,7 @@ "env": { "HWLAB_EDGE_LISTEN": "0.0.0.0:6667", "HWLAB_EDGE_UPSTREAM": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_EDGE_PROXY_TIMEOUT_MS": "180000" + "HWLAB_EDGE_PROXY_TIMEOUT_MS": "660000" } }, { diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index a44992a4..b7904e34 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -115,7 +115,7 @@ }, { "name": "HWLAB_CODE_AGENT_TIMEOUT_MS", - "value": "150000" + "value": "600000" }, { "name": "HWLAB_CODE_AGENT_OPENAI_BASE_URL", @@ -293,7 +293,7 @@ }, { "name": "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" }, { "name": "HWLAB_COMMIT_ID", @@ -870,7 +870,7 @@ }, { "name": "HWLAB_EDGE_PROXY_TIMEOUT_MS", - "value": "180000" + "value": "660000" } ], "readinessProbe": { diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index d3dc8e12..3e3124bb 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -86,6 +86,35 @@ runner 二进制/协议适配、session supervisor 生命周期、workspace moun 注入边界、trace/cancel/reap 机制,以及与 cloud-api/workbench 的持久 session 映射。缺任一项时,必须 在 `codexStdioFeasibility` 中报告 blocker。 +## PC Gateway Windows Skill 调用 + +Code Agent 通过已登记 PC gateway 执行 Windows 侧命令时,必须让 Codex turn 自己调用仓库 wrapper,不能由 cloud-api 字符串匹配短路到 gateway,也不能直接访问 gateway URL: + +```sh +node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin <<'PS1' + +PS1 +``` + +PowerShell 默认使用 wrapper 的 `-EncodedCommand` 路径;不要手写 `cmd /c powershell ...` 的管道、引号或中文转义。简单 `cmd` 命令仍可用 `--command "cmd /d /s /c ..."`,但涉及目录枚举、Unicode、管道、排序或 JSON 输出时优先用 `--powershell-stdin`。 + +Windows 文件系统探测必须是有界小输出: + +- 对 `F:\work`、`F:\work\ConStart` 或同类目录先做顶层目录/项目标记探测,不要读取 Secret、env、kubeconfig、DB URL 或完整源码内容。 +- 使用 `-LiteralPath`、`Select-Object -First `、`ConvertTo-Json -Compress -Depth `,stdout 目标控制在约 12 KB 内。 +- 不要先输出完整目录 JSON 再依赖终端截断;需要更多信息时按明确候选项目二次查询。 +- 如果命令已经到达 gateway 但因脚本语法或输出大小失败,只允许简化修正一次;最终回复要记录失败 `operationId`、修正后的成功 `operationId` 和有界输出摘要。 + +Keil 编译、下载或探测请求必须优先使用 Windows 侧 skill,而不是在 prompt 中重写 Keil 调用逻辑: + +```sh +cd C:\Users\liang\.agents\skills\keil +py -3 keil-cli.py build -p -t +py -3 keil-cli.py job-status +``` + +多 probe、烧录和 reset-run 的具体参数以 Windows 侧 `C:\Users\liang\.agents\skills\keil\SKILL.md` 为准;Code Agent 只负责通过 repo wrapper 调用该 skill CLI 并返回 trace、operation/evidence 和 bounded stdout/stderr 摘要。 + ## Smoke Gate 本地合同检查: diff --git a/docs/reference/dev-runtime-hotfix-runbook.md b/docs/reference/dev-runtime-hotfix-runbook.md index 5d3b70d6..80e6a70e 100644 --- a/docs/reference/dev-runtime-hotfix-runbook.md +++ b/docs/reference/dev-runtime-hotfix-runbook.md @@ -24,6 +24,41 @@ kubectl get nodes -o jsonpath='{.items[*].metadata.name}' 结果必须包含 `d601`。出现 `docker-desktop`、`desktop-control-plane`、`127.0.0.1:11700` 或第二套 `hwlab-dev` 控制面时,停止 hotfix、回滚和验收判断。此边界与 [dev-runtime-boundary.md](dev-runtime-boundary.md) 和 [deployment-publish.md](deployment-publish.md) 保持一致。 +## kubectl 热修命令形状 + +运行面热修应优先使用小补丁、短连接和远端临时文件,避免把大对象穿过 shell argv、CLI 输出截断或 `kubectl apply` 的 last-applied annotation。 + +推荐的 ConfigMap data 局部覆盖方式是:在本地生成只包含本次 key 的 merge patch,通过 D601 短连接写入远端临时文件,再让 `kubectl` 从真实文件读取: + +```sh +node - <<'NODE' | bun scripts/cli.ts ssh D601 argv bash -lc ' +set -e +export KUBECONFIG=/etc/rancher/k3s/k3s.yaml +kubectl get nodes -o jsonpath="{.items[*].metadata.name}" | grep -w d601 >/dev/null +patch_file=/tmp/hwlab-hotfix.patch.json +cat > "$patch_file" +kubectl -n hwlab-dev patch configmap --type merge --patch-file "$patch_file" +rm -f "$patch_file" +kubectl -n hwlab-dev rollout restart deployment/ +kubectl -n hwlab-dev rollout status deployment/ --timeout=180s +' +const fs = require("node:fs"); +process.stdout.write(JSON.stringify({ + metadata: { annotations: { "hwlab.pikastech.com/hotfix-reason": "" } }, + data: { "": fs.readFileSync("", "utf8") } +})); +NODE +``` + +固定避坑规则: + +- 不要用 `kubectl patch --patch-file -` 假设 stdin 可用;目标 `kubectl` 版本可能把 `-` 当普通文件名。用 `/tmp/*.patch.json` 再 `--patch-file`。 +- 不要把大 JSON patch 放进 `kubectl patch -p "$patch"` 或 shell argv;ConfigMap 覆盖源码文件时很容易触发 `Argument list too long`。用 stdin 写远端临时文件。 +- 不要对已经很大的 hotfix ConfigMap 使用 `kubectl apply -f -`;`apply` 会尝试写入 `kubectl.kubernetes.io/last-applied-configuration`,可能超过 Kubernetes annotation 256 KiB 限制。局部更新用 `patch --type merge --patch-file`。 +- 不要通过会截断 stdout 的控制 CLI 把 `kubectl get configmap -o json` 回读到本地再 `replace`;大 data 可能被日志/截断污染成非法 JSON。只有在确认完整无截断的原生通道中,才允许对完整对象做 `kubectl replace -f -`。 +- 每次 `rollout restart` 后立即执行 `rollout status`,再用 running pod 做最小只读验证,例如 `node --check`、`grep` marker、`curl` health 或公开入口 trace/result 轮询。 +- 临时补丁文件必须放在远端 `/tmp`,命名包含 hotfix 目标,命令结束后删除;issue 里只记录 ConfigMap 名、key、mountPath、operationId/traceId 和验证方法,不粘贴完整 data。 + ## 必须记录 每次 DEV hotfix 至少记录以下字段,写入 PR/issue/comment,不写入仓库 `reports/**`: diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index e3b492fb..e440dcb1 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1883,6 +1883,11 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u); assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills/u); assert.match(turn.args.prompt, /cmd \/c echo HWLAB-PC-GW && hostname/u); + assert.match(turn.args.prompt, /--powershell-stdin/u); + assert.match(turn.args.prompt, /-EncodedCommand/u); + assert.match(turn.args.prompt, /Windows filesystem inventory/u); + assert.match(turn.args.prompt, /Select-Object -First/u); + assert.match(turn.args.prompt, /ConvertTo-Json -Compress/u); } finally { await rm(fakeCodex.root, { recursive: true, force: true }); await rm(codexHome, { recursive: true, force: true }); @@ -1947,6 +1952,8 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn const turn = calls.find((call) => call.method === "turn/start"); assert.ok(turn, "Codex stdio turn/start should be called"); assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u); + assert.match(turn.args.prompt, /--powershell-stdin/u); + assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\keil/u); assert.match(turn.args.prompt, /keil-cli\.py build/u); assert.match(turn.args.prompt, /job-status/u); assert.match(turn.args.prompt, /FREQ_Controller_FW\.uvprojx/u); diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index aa36507e..4813f2f3 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -31,7 +31,7 @@ export const DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS = 30 * 60 * 1000; export const DEFAULT_CODEX_STDIO_MAX_SESSIONS = 64; export const DEFAULT_CODEX_STDIO_COMMAND = "codex"; export const DEFAULT_CODEX_STDIO_PROBE_TTL_MS = 30 * 1000; -const DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS = 150000; +const DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; const DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS = 10 * 60 * 1000; export const CODEX_STDIO_SESSION_STATUSES = Object.freeze([ "creating", @@ -57,6 +57,7 @@ const CODEX_STDIO_SKILL_SUMMARY_LIMIT = 180; const CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS = 3000; const CODEX_STDIO_COMMAND_PROBE_TRACE_ID = "trc_codex_stdio_command_probe"; const CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID = "cnv_codex_stdio_command_probe"; +const CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS = 15 * 1000; const EXTERNAL_NETWORK_TOOL_NAME = "external.network.check"; const EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS = 8000; const EXTERNAL_NETWORK_DNS_TIMEOUT_MS = 1500; @@ -95,6 +96,10 @@ const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [ "Do not call gateway, box-simu, or patch-panel directly.", "Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.", "For registered PC gateway Windows cmd or Keil requests, invoke the repo-owned wrapper with the Codex exec tool: node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --command \"cmd /c ...\".", + "For PowerShell through the PC gateway, do not hand-escape cmd pipes or quotes; use node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms --powershell-stdin with a single-quoted heredoc. The wrapper sends powershell.exe -EncodedCommand for Unicode-safe execution.", + "For Windows filesystem inventory, start with one small bounded PowerShell stdin script: use -LiteralPath, Select-Object -First, ConvertTo-Json -Compress, and keep stdout under about 12 KB. Do not dump full directory JSON and then retry.", + "For F:\\work or F:\\work\\ConStart discovery, first list top-level project markers and *.uvprojx candidates with bounded output. For Keil build/program requests, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\keil with py -3 keil-cli.py; do not reimplement Keil build logic.", + "If a Windows gateway command reaches the gateway but fails due script syntax or output size, simplify once and report the failed operationId plus the corrected bounded command evidence. Do not spend multiple turns on exploratory rewrites.", "Use the Windows-side skill CLIs under C:\\Users\\liang\\.agents\\skills\\ from that cmd command when they exist; do not reimplement those tools in the prompt.", "Do not satisfy PC gateway requests by string matching, assistant-only claims, internal shortcut dispatch, or direct gateway URLs; the Codex turn must run the wrapper and report command evidence.", "Do not claim M3, M4, or M5 acceptance unless the response includes live HWLAB evidence." @@ -1581,6 +1586,35 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { return appended; } + function appendProgressTrace(event = {}) { + return traceRecorder?.append(event); + } + + function hasAssistantOutput() { + return Boolean(firstNonEmpty(finalResponse, assistantText)); + } + + function appendFirstTokenProgressTrace(referenceNow = Date.now()) { + const activity = activitySnapshot(referenceNow); + if (hasAssistantOutput() || !turnId || terminal) return null; + if (!["assistant-message", "turn/completed"].includes(activity.waitingFor)) return null; + return appendProgressTrace({ + type: "turn", + status: "running", + label: "turn:waiting:first_assistant_token", + message: "Codex app-server is waiting for the first assistant token; this progress trace does not reset the backend idle timer.", + progressOnly: true, + idleMs: activity.idleMs, + lastActivityAt: activity.lastActivityAt, + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message" + }); + } + function finish(status, error = null, extra = {}) { if (terminal) return; terminal = { @@ -1728,11 +1762,33 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { async function wait(timeoutMs, closedPromise, { hardTimeoutMs = null } = {}) { let activityTimer; let hardTimer; + let progressTimer; const timeoutPromise = new Promise((_, reject) => { const checkActivity = () => { const now = Date.now(); const activity = activitySnapshot(now); if (activity.idleMs >= timeoutMs) { + if (hasAssistantOutput()) { + appendTrace({ + type: "turn", + status: "completed", + label: "turn:completed:idle_timeout_after_assistant", + message: "Codex app-server became idle after assistant output; preserving the completed assistant response.", + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message-complete" + }); + finish("completed", null, { + threadId, + turnId, + idleTimeoutAfterAssistant: true, + ...activity + }); + return; + } const error = appServerActivityTimeoutError({ timeoutMs, idleMs: activity.idleMs, @@ -1765,6 +1821,13 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { }; activityTimer = setTimeout(checkActivity, timeoutMs); }); + const progressPromise = new Promise(() => { + const tick = () => { + appendFirstTokenProgressTrace(); + progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS); + }; + progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS); + }); const hardTimeoutPromise = hardTimeoutMs ? new Promise((_, reject) => { hardTimer = setTimeout(() => { @@ -1804,6 +1867,27 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { if (closedPromise && typeof closedPromise.then === "function") { candidates.push(closedPromise.then((exit) => { if (terminal) return terminal; + if (hasAssistantOutput()) { + appendTrace({ + type: "turn", + status: "completed", + label: "turn:completed:transport_closed_after_assistant", + message: "Codex app-server transport closed after assistant message completion; preserving the completed assistant response.", + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: "assistant-message-complete" + }); + finish("completed", null, { + threadId, + turnId, + appServerExit: exit, + transportClosedAfterAssistant: true + }); + return terminal; + } return { terminalStatus: "failed", terminalError: "Codex app-server transport closed before turn/completed.", @@ -1815,10 +1899,11 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { })); } try { - return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise].filter(Boolean)); + return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise, progressPromise].filter(Boolean)); } finally { clearTimeout(activityTimer); clearTimeout(hardTimer); + clearTimeout(progressTimer); } } diff --git a/internal/dev-entrypoint/cloud-web-proxy.mjs b/internal/dev-entrypoint/cloud-web-proxy.mjs index 0634e584..79d74d60 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.mjs @@ -93,7 +93,11 @@ export function proxyCloudApiRequest({ settle(resolve); }); upstreamResponse.on("error", (error) => { - if (response.headersSent) response.destroy(error); + if (response.headersSent) { + response.destroy(error); + settle(resolve); + return; + } settle(reject, error); }); upstreamResponse.on("close", () => { diff --git a/internal/dev-entrypoint/cloud-web-proxy.test.mjs b/internal/dev-entrypoint/cloud-web-proxy.test.mjs index 80dad275..3fcd1e8b 100644 --- a/internal/dev-entrypoint/cloud-web-proxy.test.mjs +++ b/internal/dev-entrypoint/cloud-web-proxy.test.mjs @@ -71,6 +71,47 @@ test("cloud web proxy streams SSE chunks without fixed content-length", async () } }); +test("cloud web proxy does not reject after SSE headers are already sent", async () => { + const upstream = createServer((request, response) => { + request.resume(); + response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8" }); + response.write("event: snapshot\ndata: {\"events\":[{\"seq\":1}]}\n\n"); + setTimeout(() => response.destroy(new Error("simulated upstream stream failure")), 40); + }); + await listen(upstream); + + let rejected = false; + const proxy = createServer((request, response) => { + const url = new URL(request.url || "/", "http://cloud-web.local"); + proxyCloudApiRequest({ + target: new URL(url.pathname + url.search, serverUrl(upstream)), + request, + response, + timeoutMs: 1000, + forceStream: isCloudWebSseRoute(url.pathname) + }).catch((error) => { + rejected = true; + if (!response.headersSent) response.destroy(error); + }); + }); + await listen(proxy); + + try { + const response = await fetch(`${serverUrl(proxy)}/v1/agent/chat/trace/trc_stream_proxy_error/stream`, { + headers: { accept: "text/event-stream" } + }); + assert.equal(response.status, 200); + const reader = response.body.getReader(); + await reader.read(); + await reader.read().catch(() => {}); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.equal(rejected, false, "stream errors after headers must not bubble into JSON error writers"); + } finally { + await close(proxy); + await close(upstream); + } +}); + test("cloud web proxy buffers normal JSON and sets content-length", async () => { const upstream = createServer((request, response) => { request.resume(); diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index 0f91eeda..61568031 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -15,7 +15,10 @@ export function cloudWebProxyRoutePolicy(method, pathname) { const normalizedPath = normalizePathname(pathname); const routeKey = `${normalizedMethod} ${normalizedPath}`; const proxy = isCloudApiProxyRoute(normalizedMethod, normalizedPath); - const publicRoute = proxy && PUBLIC_PROXY_ROUTES.has(routeKey); + const publicRoute = proxy && ( + PUBLIC_PROXY_ROUTES.has(routeKey) || + isPublicCodeAgentPollRoute(normalizedMethod, normalizedPath) + ); return { proxy, authRequired: proxy && !publicRoute, @@ -37,6 +40,13 @@ export function isCloudApiProxyRoute(method, pathname) { return false; } +function isPublicCodeAgentPollRoute(method, pathname) { + return method === "GET" && ( + pathname.startsWith("/v1/agent/chat/result/") || + pathname.startsWith("/v1/agent/chat/trace/") + ); +} + function normalizeMethod(method) { return String(method || "GET").trim().toUpperCase() || "GET"; } diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index e34c5f83..8c04bc11 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -32,10 +32,16 @@ test("cloud web route policy proxies public Code Agent chat without gating other }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/trace/trc_live"), { proxy: true, - authRequired: true, - publicRoute: false, + authRequired: false, + publicRoute: true, routeKey: "GET /v1/agent/chat/trace/trc_live" }); + assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/result/trc_live"), { + proxy: true, + authRequired: false, + publicRoute: true, + routeKey: "GET /v1/agent/chat/result/trc_live" + }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.mjs"), { proxy: false, authRequired: false, diff --git a/scripts/artifact-runtime-readiness-guard.test.mjs b/scripts/artifact-runtime-readiness-guard.test.mjs index 1052d690..111af27f 100644 --- a/scripts/artifact-runtime-readiness-guard.test.mjs +++ b/scripts/artifact-runtime-readiness-guard.test.mjs @@ -258,10 +258,10 @@ test("compact commander summary exposes canPublish/canApply and dry-run devLiveC cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, @@ -292,10 +292,10 @@ test("compact summary blocks publish on stale Cloud Web source dist", () => { cloudWebDistFreshness: { status: "blocked", mismatches: ["index.html"] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, @@ -326,9 +326,9 @@ test("compact summary separates Code Agent timeout and durable runtime blockers codeAgentTimeoutReadiness: { status: "blocked", frontendTimeoutMs: 4500, - backendTimeoutMs: 150000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: false, summary: "Code Agent browser timeout is too short." }, @@ -373,10 +373,10 @@ test("compact summary blocks publish when Code Agent transport timeout does not cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "blocked", - frontendTimeoutMs: 150000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 600000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: false, summary: "Code Agent timeout layering is unsafe." }, @@ -405,10 +405,10 @@ test("compact summary keeps provider-secret blockers out of publish but blocks a cloudWebDistFreshness: { status: "pass", mismatches: [] }, codeAgentTimeoutReadiness: { status: "pass", - frontendTimeoutMs: 180000, - backendTimeoutMs: 150000, + frontendTimeoutMs: 660000, + backendTimeoutMs: 600000, requiredFrontendTimeoutMs: 35000, - requiredBackendTimeoutMs: 150000, + requiredBackendTimeoutMs: 600000, transportWaitsForBackend: true, summary: "ready" }, diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index 2fa7fb06..9da6101e 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -333,9 +333,9 @@ const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder"; const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || ""; const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10); const cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || ""; -const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 180000, { +const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 660000, { min: 1000, - max: 300000 + max: 900000 }); const authUsername = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_USERNAME", "admin"); const authPassword = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_PASSWORD", "hwlab2026"); @@ -649,6 +649,10 @@ async function proxyCloudApi(request, response, url) { try { await requestUpstream(target, request, response, request.method === "GET" ? "" : body, url); } catch (error) { + if (response.headersSent || response.writableEnded) { + if (!response.writableEnded) response.destroy(error); + return; + } const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message); const code = timedOut ? "cloud_api_proxy_timeout" : "cloud_api_proxy_failed"; const category = timedOut ? "timeout" : "proxy"; diff --git a/scripts/src/artifact-runtime-readiness-guard.mjs b/scripts/src/artifact-runtime-readiness-guard.mjs index 698e579b..4ba43df7 100644 --- a/scripts/src/artifact-runtime-readiness-guard.mjs +++ b/scripts/src/artifact-runtime-readiness-guard.mjs @@ -22,8 +22,8 @@ const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`; const digestPattern = /^sha256:[a-f0-9]{64}$/u; const commitPattern = /^[a-f0-9]{7,40}$/u; const minCodeAgentFrontendTimeoutMs = 35000; -const defaultCodeAgentBackendTimeoutMs = 150000; -const minCodeAgentBackendTimeoutMs = 150000; +const defaultCodeAgentBackendTimeoutMs = 600000; +const minCodeAgentBackendTimeoutMs = 600000; export function normalizeCommit(value) { if (typeof value !== "string") return "unknown"; diff --git a/scripts/src/deploy-contract-plan.mjs b/scripts/src/deploy-contract-plan.mjs index fe2c6b43..cee6f6c5 100644 --- a/scripts/src/deploy-contract-plan.mjs +++ b/scripts/src/deploy-contract-plan.mjs @@ -13,8 +13,8 @@ import { const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const expectedPorts = Object.freeze({ frontend: 16666, api: 16667 }); -const codeAgentBackendTimeoutMs = "150000"; -const codeAgentTransportTimeoutMs = "180000"; +const codeAgentBackendTimeoutMs = "600000"; +const codeAgentTransportTimeoutMs = "660000"; const legacyPublicPorts = new Set([6666, 6667]); const duplicateMappingAllowed = new Set(["hwlab-box-simu", "hwlab-gateway-simu"]); const tomlBareKeyPattern = /^[A-Za-z0-9_-]+$/u; diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index e2764307..eaf8a50b 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -23,7 +23,7 @@ const webRoot = path.join(repoRoot, "web/hwlab-cloud-web"); const defaultLiveUrl = "http://74.48.78.17:16666/"; const helpOwner = "codex_1779444232735_1"; const legacyFailureWindowMs = 4500; -const codeAgentLongTimeoutMs = 180000; +const codeAgentLongTimeoutMs = 600000; const localAgentFixtureDelayMs = 5200; const codeAgentE2ePrompts = Object.freeze([ { @@ -2631,8 +2631,10 @@ function hasCodeAgentLongTimeoutContract(files) { const app = files.app; return ( /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) && - /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) && - /async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) && + /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) && + /DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/u.test(app) && + /async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) && + /shortConnection:\s*true/u.test(functionBody(app, "sendAgentMessage")) && /timeoutMs\s*=\s*API_TIMEOUT_MS/u.test(functionBody(app, "fetchJson")) && /timeoutName\s*=\s*path/u.test(functionBody(app, "fetchJson")) && /HWLAB_CLOUD_WEB_CONFIG/u.test(app) && @@ -2719,8 +2721,8 @@ function hasCodeAgentConversationUxStates({ app, styles }) { !/function\s+messageAttributionPanel\s*\(/u.test(app) && !/codeAgentAttributionFromMessage/u.test(app) && /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) && - /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) && - /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) && + /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) && + /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) && /HWLAB_CLOUD_WEB_CONFIG/u.test(app) && /正在处理这次 Code Agent 请求/u.test(app) && /旧 4500ms/u.test(app) && diff --git a/tools/hwlab-gateway-shell.mjs b/tools/hwlab-gateway-shell.mjs index 197001a2..45234f97 100644 --- a/tools/hwlab-gateway-shell.mjs +++ b/tools/hwlab-gateway-shell.mjs @@ -2,16 +2,19 @@ import http from "node:http"; import https from "node:https"; import { randomUUID } from "node:crypto"; +import { readFileSync } from "node:fs"; const DEFAULT_API_BASE_URL = "http://127.0.0.1:6667"; const DEFAULT_PROJECT_ID = "prj_mvp_topology"; const DEFAULT_GATEWAY_SESSION_ID = "gws_DESKTOP-1MHOD9I"; const DEFAULT_RESOURCE_ID = "res_windows_host"; const DEFAULT_CAPABILITY_ID = "cap_windows_cmd_exec"; +const POWERSHELL_PROLOGUE = "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [Console]::OutputEncoding;"; async function main() { const args = parseArgs(process.argv.slice(2)); - if (args.help || !args.command) { + const command = await resolveCommand(args); + if (args.help || !command) { printHelp(); process.exit(args.help ? 0 : 2); } @@ -36,7 +39,7 @@ async function main() { resourceId: args.resourceId || DEFAULT_RESOURCE_ID, capabilityId: args.capabilityId || DEFAULT_CAPABILITY_ID, input: { - command: args.command, + command, cwd: args.cwd || undefined, timeoutMs } @@ -58,19 +61,22 @@ async function main() { function parseArgs(argv) { const args = {}; + const booleanFlags = new Set(["json", "help", "h", "powershellStdin", "psStdin"]); for (let index = 0; index < argv.length; index += 1) { const item = argv[index]; - if (item === "--help" || item === "-h") { + if ((item === "--help") || (item === "-h")) { args.help = true; - } else if (item === "--json") { - args.json = true; } else if (item.startsWith("--") && item.includes("=")) { const [key, ...rest] = item.slice(2).split("="); args[toCamel(key)] = rest.join("="); } else if (item.startsWith("--")) { const key = toCamel(item.slice(2)); - args[key] = argv[index + 1] ?? ""; - index += 1; + if (booleanFlags.has(key)) { + args[key] = true; + } else { + args[key] = argv[index + 1] ?? ""; + index += 1; + } } } return args; @@ -83,10 +89,16 @@ function toCamel(value) { function printHelp() { process.stdout.write([ "Usage: node tools/hwlab-gateway-shell.mjs --command \"cmd /c ...\" [--json]", + " node tools/hwlab-gateway-shell.mjs --powershell-command \"Get-ChildItem ...\" [--json]", + " node tools/hwlab-gateway-shell.mjs --powershell-stdin [--json] < script.ps1", "", "Options:", " --api-base-url URL Cloud API/edge base URL, default http://127.0.0.1:6667", " --command CMD Windows cmd command to execute through the registered PC gateway", + " --powershell-command PS1 Encode PS1 as UTF-16LE and run powershell.exe -EncodedCommand", + " --powershell-file PATH Read a local PS1 file, encode it, and run through the gateway", + " --powershell-stdin Read PS1 from stdin, encode it, and run through the gateway", + " --powershell-exe NAME Default powershell.exe", " --timeout-ms N Short dispatch timeout in milliseconds", " --project-id ID Default prj_mvp_topology", " --gateway-session-id ID Default gws_DESKTOP-1MHOD9I", @@ -96,6 +108,41 @@ function printHelp() { ].join("\n") + "\n"); } +async function resolveCommand(args) { + if (args.powershellStdin || args.psStdin) { + return powershellEncodedCommand(await readStdin(), args); + } + if (args.powershellFile || args.psFile) { + return powershellEncodedCommand(readFileSync(args.powershellFile || args.psFile, "utf8"), args); + } + if (args.powershellCommand || args.psCommand || args.powershell) { + return powershellEncodedCommand(args.powershellCommand || args.psCommand || args.powershell, args); + } + return args.command || ""; +} + +function powershellEncodedCommand(script, args = {}) { + const source = String(script || "").trim(); + if (!source) return ""; + const shell = args.powershellExe || "powershell.exe"; + const prologue = args.noPowershellPrologue ? "" : `${POWERSHELL_PROLOGUE}\n`; + const encoded = Buffer.from(`${prologue}${source}`, "utf16le").toString("base64"); + return `${shell} -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`; +} + +function readStdin() { + return new Promise((resolve, reject) => { + let text = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => { + text += chunk; + }); + process.stdin.on("end", () => resolve(text)); + process.stdin.on("error", reject); + process.stdin.resume(); + }); +} + function requestJson(url, { method, headers, body, timeoutMs }) { return new Promise((resolve, reject) => { const target = new URL(url); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 48faee47..27ee5839 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -18,11 +18,14 @@ import { } from "./code-agent-m3-evidence.mjs"; const DEFAULT_API_TIMEOUT_MS = 4500; -const DEFAULT_CODE_AGENT_TIMEOUT_MS = 180000; +const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000; +const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000; +const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); -const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 }); +let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs(); +const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: 5000, max: 120000 }); const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; const rpcReadMethods = Object.freeze([ "system.health", @@ -113,8 +116,6 @@ const el = { liveDetail: byId("live-detail"), liveBuildSummary: byId("live-build-summary"), liveBuildToggle: byId("live-build-toggle"), - liveBuildPopover: byId("live-build-popover"), - liveBuildClose: byId("live-build-close"), liveBuildLatest: byId("live-build-latest"), liveBuildList: byId("live-build-list"), agentChatStatus: byId("agent-chat-status"), @@ -125,7 +126,6 @@ const el = { codeAgentSummaryTrace: byId("code-agent-summary-trace"), codeAgentSummaryDetail: byId("code-agent-summary-detail"), conversationList: byId("conversation-list"), - taskList: byId("task-list"), gateSourceStatus: byId("gate-source-status"), gateStatusFilter: byId("gate-status-filter"), gateSearch: byId("gate-search"), @@ -133,6 +133,7 @@ const el = { gateReviewBody: byId("gate-review-body"), commandForm: byId("command-form"), commandInput: byId("command-input"), + codeAgentTimeout: byId("code-agent-timeout"), commandSend: byId("command-send"), commandClear: byId("command-clear"), hardwareSourceStatus: byId("hardware-source-status"), @@ -159,6 +160,8 @@ const el = { helpStatus: byId("help-status") }; +let activeWorkbenchDialog = null; + await ensureWorkbenchAuth({ loginShell: el.loginShell, appShell: el.shell @@ -207,6 +210,7 @@ initRightSidebarResize(); initSideTabs(); initHardwareTabs(); initQuickActions(); +initCodeAgentTimeoutControl(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); @@ -241,33 +245,125 @@ function resolveTimeoutMs(name, fallback, { min, max }) { return Math.min(Math.max(Math.trunc(configured), min), max); } -function initLiveBuildOverlay() { - syncLiveBuildOverlayState(); - el.liveBuildSummary.addEventListener("toggle", syncLiveBuildOverlayState); - el.liveBuildClose.addEventListener("click", () => closeLiveBuildOverlay({ restoreFocus: true })); - document.addEventListener("keydown", (event) => { - if (event.key === "Escape" && el.liveBuildSummary.open) { - event.preventDefault(); - closeLiveBuildOverlay({ restoreFocus: true }); +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() { + let stored = null; + try { + stored = window.localStorage?.getItem(CODE_AGENT_TIMEOUT_STORAGE_KEY) ?? null; + } catch { + stored = null; + } + return clampTimeoutMs(stored, { min: 30000, max: 1200000 }) + ?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 30000, max: 1200000 }); +} + +function initCodeAgentTimeoutControl() { + syncCodeAgentTimeoutControl(); + el.codeAgentTimeout.addEventListener("change", () => { + const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: 1200000 }) ?? 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. } - }); - document.addEventListener("click", (event) => { - if (!el.liveBuildSummary.open || el.liveBuildSummary.contains(event.target)) return; - closeLiveBuildOverlay(); + syncCodeAgentTimeoutControl(); + renderCodeAgentSummary(); + renderConversation(); }); } -function syncLiveBuildOverlayState() { - const expanded = el.liveBuildSummary.open === true; - el.liveBuildToggle.setAttribute("aria-expanded", expanded ? "true" : "false"); - el.liveBuildPopover.setAttribute("aria-hidden", expanded ? "false" : "true"); +function syncCodeAgentTimeoutControl() { + el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS); + el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`; } -function closeLiveBuildOverlay(options = {}) { - if (!el.liveBuildSummary.open) return; +function initLiveBuildOverlay() { el.liveBuildSummary.open = false; - syncLiveBuildOverlayState(); - if (options.restoreFocus === true) el.liveBuildToggle.focus(); + 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() { @@ -711,7 +807,7 @@ async function submitAgentMessage(value, options = {}) { id: nextProtocolId("msg"), role: "agent", title: options.retryOf ? "Code Agent 重试中" : "Code Agent 处理中", - text: `正在处理这次 Code Agent 请求;复杂问题可能需要 1-2 分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`, + text: `正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`, status: "running", traceId, conversationId: activeConversationId, @@ -1306,7 +1402,6 @@ function renderStaticWorkbench() { el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。"; renderConversation(); - renderTaskList(); renderHardwareStatus(null); renderDrafts(); renderM3ControlStatus(); @@ -1335,7 +1430,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI "Prefer": "respond-async", "X-HWLAB-Short-Connection": "1" }, - timeoutMs: Math.min(API_TIMEOUT_MS, 5000), + timeoutMs: CODE_AGENT_SUBMIT_TIMEOUT_MS, timeoutName: "Code Agent", body: JSON.stringify({ message, @@ -1343,6 +1438,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI sessionId, threadId, traceId, + shortConnection: true, projectId: gateSummary.topology.projectId }) }); @@ -1888,27 +1984,6 @@ function renderConversation() { replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); } -function renderTaskList() { - const rows = [ - { - title: "1. 描述目标", - detail: "例如要检查哪个 BOX-SIMU、哪个端口、期望看到什么信号。", - tone: "source" - }, - { - title: "2. 核对接线", - detail: "打开右侧“接线”表,确认 res_boxsimu_1 与 res_boxsimu_2 的 DO/DI 对应关系,以及接线盘和轨迹/证据摘要。", - tone: "source" - }, - { - title: "3. 留存记录", - detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。", - tone: "source" - } - ]; - replaceChildren(el.taskList, ...rows.map(infoCard)); -} - function renderHardwareStatus(m3Status) { syncHardwareTabs(); const status = m3Status ?? sourceFallbackM3Status(); @@ -3744,11 +3819,6 @@ function messageSessionContinuityPanel(message) { ["missing", continuity.missing?.join("、")], ["changed", continuity.changed?.join("、")] ]; - const section = document.createElement("section"); - section.className = `message-session-context tone-border-${toneClass(continuity.tone)}`; - const header = document.createElement("div"); - header.className = "message-session-head"; - header.append(badge(continuity.label, continuity.tone), textSpan(continuity.summary, "message-session-summary")); const list = document.createElement("div"); list.className = "message-session-grid"; for (const [label, value] of fields) { @@ -3761,8 +3831,15 @@ function messageSessionContinuityPanel(message) { } list.append(item); } - section.append(header, list); - return section; + return messageCompactDetails({ + className: "message-session-context", + tone: continuity.tone, + badgeLabel: continuity.label, + badgeTone: continuity.tone, + summary: continuity.summary, + dialogTitle: "会话明细", + body: list + }); } function messagePendingContextPanel(message) { @@ -3781,7 +3858,7 @@ function messagePendingContextPanel(message) { ["thread", message.threadId ?? message.runnerTrace?.threadId ?? "等待后端分配"], ["sessionStatus", message.runnerTrace?.sessionStatus], ["lifecycle", message.runnerTrace?.sessionLifecycleStatus], - ["activityTimeout", `${CODE_AGENT_TIMEOUT_MS}ms 无新事件`] + ["activityTimeout", `${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 无新事件`] ]; const section = document.createElement("section"); section.className = "message-pending-context tone-border-pending"; @@ -3823,15 +3900,6 @@ function messageRuntimePathPanel(message) { if (message.role !== "agent") return null; const runtimePath = codeAgentRuntimePathFromMessage(message); if (!runtimePath) return null; - const section = document.createElement("section"); - section.className = `message-runtime-path tone-border-${toneClass(runtimePath.tone)}`; - section.dataset.runtimePathKind = runtimePath.kind; - const header = document.createElement("div"); - header.className = "message-runtime-head"; - header.append( - badge(runtimePath.label, runtimePath.tone), - textSpan(runtimePath.summary, "message-runtime-summary") - ); const rows = document.createElement("div"); rows.className = "message-runtime-grid"; for (const row of runtimePath.rows) { @@ -3852,7 +3920,38 @@ function messageRuntimePathPanel(message) { ); rows.append(item); } - section.append(header, rows); + const details = messageCompactDetails({ + className: "message-runtime-path", + tone: runtimePath.tone, + badgeLabel: runtimePath.label, + badgeTone: runtimePath.tone, + summary: runtimePath.summary, + dialogTitle: "运行路径明细", + body: rows + }); + details.dataset.runtimePathKind = runtimePath.kind; + return details; +} + +function messageCompactDetails({ className, tone, badgeLabel, badgeTone, summary, dialogTitle, body }) { + const section = document.createElement("section"); + section.className = `${className} message-compact-details tone-border-${toneClass(tone)}`; + const trigger = document.createElement("button"); + trigger.type = "button"; + trigger.className = "message-compact-summary"; + trigger.setAttribute("aria-haspopup", "dialog"); + trigger.append( + badge(badgeLabel, badgeTone), + textSpan(summary, "message-compact-summary-text") + ); + trigger.addEventListener("click", () => { + openWorkbenchDialog({ + title: dialogTitle, + body, + restoreFocus: trigger + }); + }); + section.append(trigger); return section; } @@ -3987,14 +4086,76 @@ function renderTraceEventList(list, rows) { function traceDisplayRows(trace, events) { const rows = []; + let noisyRun = []; + const flushNoisyRun = () => { + if (noisyRun.length === 0) return; + const row = traceNoiseSummaryRow(trace, noisyRun); + if (row) rows.push(row); + noisyRun = []; + }; for (const event of events) { + if (isNoisyTraceEvent(event)) { + noisyRun.push(event); + continue; + } + flushNoisyRun(); const row = traceDisplayRow(trace, event); if (!row) continue; rows.push(row); } + flushNoisyRun(); return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean); } +function traceNoiseSummaryRow(trace, events) { + const visibleEvents = events.filter(Boolean); + if (visibleEvents.length === 0) return null; + const last = visibleEvents.at(-1); + const clock = traceClock(last.createdAt); + const total = formatTraceDuration(traceRelativeMs(trace, last)); + const assistantChunks = visibleEvents.filter((event) => isAssistantChunkTraceEvent(event)); + const label = assistantChunks.length === visibleEvents.length + ? `assistant stream x${visibleEvents.length}` + : `trace noise x${visibleEvents.length}`; + return { + seq: last.seq ?? null, + tone: "source", + header: `${clock} total=${total} stream ${label}`, + body: traceNoiseSummaryBody(visibleEvents, assistantChunks) + }; +} + +function traceNoiseSummaryBody(events, assistantChunks) { + if (assistantChunks.length > 0) { + const text = assistantChunks + .map((event) => cleanTraceText(event.chunk)) + .join("") + .trim(); + const waitingFor = events.at(-1)?.waitingFor ? `waiting=${events.at(-1).waitingFor}` : null; + const recent = text ? compactTraceTextTail(text, 900) : null; + return [ + `compressed=${assistantChunks.length} assistant chunks`, + waitingFor, + recent ? `recent=${recent}` : null + ].filter(Boolean).join("\n"); + } + const labels = new Map(); + for (const event of events) { + const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`); + labels.set(label, (labels.get(label) ?? 0) + 1); + } + return [...labels.entries()] + .slice(0, 6) + .map(([label, count]) => `${readableTraceLabel({ label })} x${count}`) + .join("\n"); +} + +function compactTraceTextTail(text, maxLength) { + const clean = cleanTraceText(text); + if (clean.length <= maxLength) return clean; + return `...${clean.slice(-maxLength)}`; +} + function traceDisplayRow(trace, event, options = {}) { if (!options.includeNoise && isNoisyTraceEvent(event)) return null; const clock = traceClock(event.createdAt); @@ -4019,13 +4180,19 @@ function traceDisplayRow(trace, event, options = {}) { function isNoisyTraceEvent(event) { const label = String(event?.label ?? ""); - if (/assistant:chunk|token_count|outputDelta:chunk/iu.test(label)) return true; - if (event?.type === "assistant_message" && event?.status === "chunk") return true; + if (isAssistantChunkTraceEvent(event)) return true; + if (/token_count|outputDelta:chunk/iu.test(label)) return true; if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true; if (event?.label === "request:accepted" && !event.message) return true; return false; } +function isAssistantChunkTraceEvent(event) { + const label = String(event?.label ?? ""); + return /assistant:chunk/iu.test(label) || + (event?.type === "assistant_message" && event?.status === "chunk"); +} + function readableTraceLabel(event) { const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`); if (label === "request:accepted-short-connection") return "submit short-connection"; @@ -4033,6 +4200,7 @@ function readableTraceLabel(event) { if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start"; if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume"; if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output"; + if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token"; if (label.startsWith("turn:completed")) return "turn completed"; if (label.startsWith("result:completed")) return "result ready"; if (label.startsWith("result:failed")) return "result failed"; diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 92a03072..c3db2311 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -72,17 +72,11 @@ 等待验证 正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。
-
+ @@ -109,16 +103,6 @@
-
-
-
-

工作清单

-

下一步任务草稿

-
- 浏览器本地 -
-
-
@@ -217,6 +201,16 @@ data-prompt-text="通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。" >HWLAB API:DO1=false 复核 DI1 +
hwlab =\s*CODE_AGENT_TIMEOUT_MS/); +assert.match(html, /id="code-agent-timeout"/); +assert.match(styles, /\.agent-timeout-control\s*{/); assert.match(app, /activityRef:\s*\(\)\s*=>\s*state\.currentRequest/); assert.match(functionBody(app, "fetchJson"), /timeoutMs\s*=\s*API_TIMEOUT_MS/); assert.match(functionBody(app, "fetchJson"), /无新事件/); @@ -443,7 +453,6 @@ for (const workbenchElement of [ "code-agent-summary-label", "code-agent-summary-capability", "code-agent-summary-trace", - "task-list", "right-sidebar", "right-sidebar-resize", "command-form", @@ -794,6 +803,12 @@ assert.match(app, /下载 trace/); assert.match(app, /function renderTraceEventList/); assert.match(app, /function traceDisplayRows/); assert.match(app, /function traceDisplayRow/); +assert.match(app, /function traceNoiseSummaryRow/); +assert.match(app, /assistant stream x\$\{visibleEvents\.length\}/); +assert.match(app, /compressed=\$\{assistantChunks\.length\} assistant chunks/); +assert.match(app, /waiting first assistant token/); +assert.match(app, /function compactTraceTextTail/); +assert.match(app, /function isAssistantChunkTraceEvent/); assert.match(app, /function downloadTraceJson/); assert.match(styles, /\.message-trace-toolbar\s*\{/); assert.match(styles, /\.message-trace-count\s*\{/); @@ -898,6 +913,11 @@ assert.match(styles, /\.message-user\s*{/); assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/); assert.match(styles, /\.message-pending-context\s*{/); assert.match(styles, /\.message-session-context\s*{/); +assert.match(styles, /\.message-compact-details\s*{/); +assert.match(styles, /\.workbench-dialog-layer\s*{/); +assert.match(styles, /\.workbench-dialog\s*{/); +assert.match(app, /function openWorkbenchDialog/); +assert.match(app, /document\.body\.append\(layer\)/); assert.match(styles, /\.message-m3-evidence\s*{/); assert.match(styles, /\.message-m3-rows\s*{/); assert.match(styles, /\.message-m3-row\s*{/); @@ -949,9 +969,11 @@ assert.match(app, /createdAt/); assert.match(app, /updatedAt/); assert.match(app, /error\?\.message/); assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/); -assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/); +assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/); +assert.match(app, /CODE_AGENT_SUBMIT_TIMEOUT_MS/); assert.match(app, /CODE_AGENT_TIMEOUT_MS/); -assert.match(app, /timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/); +assert.match(functionBody(app, "sendAgentMessage"), /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/); +assert.match(functionBody(app, "waitForAgentMessageResult"), /CODE_AGENT_TIMEOUT_MS/); assert.match(app, /HWLAB_CLOUD_WEB_CONFIG/); assert.match(app, /sourceKind:\s*"PENDING"/); assert.match(app, /旧 4500ms/); @@ -980,7 +1002,10 @@ 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, /\.(?:compact-list|conversation-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); +assert.match(styles, /\.(?:compact-list|conversation-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); +assert.doesNotMatch(html, /task-panel|task-list|下一步任务草稿/); +assert.doesNotMatch(app, /taskList|renderTaskList/); +assert.doesNotMatch(styles, /\.task-panel|\.task-list/); 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]*?\.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;/); @@ -990,12 +1015,10 @@ assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/); assert.match(app, /fetchJson\("\/v1\/live-builds"\)/); assert.match(html, /id="live-build-latest"/); assert.match(html, /id="live-build-list"/); -assert.match(html, /id="live-build-toggle"[^>]*aria-controls="live-build-popover"[^>]*aria-expanded="false"/); -assert.match(html, /id="live-build-popover"[^>]*role="dialog"[^>]*aria-modal="false"/); -assert.match(html, /id="live-build-close"/); +assert.match(html, /id="live-build-toggle"[^>]*aria-haspopup="dialog"[^>]*aria-expanded="false"/); assert.match(styles, /\.live-build-summary\s*{/); assert.match(app, /initLiveBuildOverlay/); -assert.match(app, /closeLiveBuildOverlay/); +assert.match(app, /openWorkbenchDialog/); assert.match(app, /aria-expanded/); assert.match(app, /formatBeijingTime/); assert.match(app, /Asia\/Shanghai/); @@ -1009,11 +1032,8 @@ assert.match(app, /外部镜像或非 HWLAB 构建产物/); assert.doesNotMatch(app, /artifact report|artifact-report/); assert.match(app, /live health/); assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s); -assert.match(styles, /\.live-build-popover\s*{[^}]*position:\s*absolute;[^}]*max-height:\s*min\(420px,\s*58dvh\);[^}]*overflow:\s*hidden;/s); -assert.match(styles, /\.live-build-summary\[open\]\s+\.live-build-popover\s*{[^}]*display:\s*grid;/s); assert.match(styles, /\.live-build-list\s*{[^}]*min-width:\s*0;[^}]*max-height:\s*min\(352px,\s*calc\(58dvh - 54px\)\);[^}]*overflow:\s*auto;/s); assert.match(styles, /\.live-build-row-head\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*0\.8fr\)\s*minmax\(0,\s*1\.2fr\);/s); -assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-popover\s*{[\s\S]*?width:\s*100%;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*{[\s\S]*?grid-template-columns:\s*1fr;/); assert.match(styles, /\.live-build-meta span\s*{[^}]*overflow-wrap:\s*anywhere;/s); assert.match(app, /fetchJson\("\/health\/live"\)/); @@ -1034,15 +1054,39 @@ assert.match(cloudWebProxy, /text\\\/event-stream/); assert.match(cloudWebProxy, /x-accel-buffering/); assert.match(cloudWebProxy, /flushHeaders/); assert.match(cloudWebProxy, /response\.write\(chunk\)/); +assert.match(cloudWebProxy, /response\.headersSent\)[\s\S]*?response\.destroy\(error\);[\s\S]*?settle\(resolve\);[\s\S]*?return;/); +assert.match(artifactPublisher, /response\.headersSent \|\| response\.writableEnded/); +assert.match(artifactPublisher, /if \(!response\.writableEnded\) response\.destroy\(error\);/); assert.doesNotMatch(artifactPublisher, /await fetch\(target/); assert.match(artifactPublisher, /cloudWebProxyRoutePolicy\(request\.method,\s*url\.pathname\)/); assert.match(cloudWebRoutes, /"\/v1\/agent\/chat"/); assert.match(cloudWebRoutes, /"\/v1\/agent\/chat\/cancel"/); assert.match(cloudWebRoutes, /"\/v1\/m3\/io"/); assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat"/); +assert.match(cloudWebRoutes, /isPublicCodeAgentPollRoute/); +assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/result\//); +assert.match(cloudWebRoutes, /\/v1\/agent\/chat\/trace\//); assert.match(cloudWebRoutes, /const GET_PROXY_PREFIXES = Object\.freeze\(\["\/v1\/"\]\)/); assert.match(cloudWebRoutes, /const GET_PROXY_ROUTES = new Set\(\["\/v1"\]\)/); assert.match(cloudWebRoutes, /authRequired: proxy && !publicRoute/); +assert.match(codexStdioSession, /turn:completed:transport_closed_after_assistant/); +assert.match(codexStdioSession, /turn:completed:idle_timeout_after_assistant/); +assert.match(codexStdioSession, /firstNonEmpty\(finalResponse,\s*assistantText\)/); +assert.match(codexStdioSession, /CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS/); +assert.match(codexStdioSession, /turn:waiting:first_assistant_token/); +assert.match(codexStdioSession, /progressOnly:\s*true/); +assert.match(codexStdioSession, /\["assistant-message", "turn\/completed"\]\.includes\(activity\.waitingFor\)/); +assert.match(codexStdioSession, /does not reset the backend idle timer/); +assert.match(codexStdioSession, /--powershell-stdin/); +assert.match(codexStdioSession, /-EncodedCommand/); +assert.match(codexStdioSession, /Windows filesystem inventory/); +assert.match(codexStdioSession, /Select-Object -First/); +assert.match(codexStdioSession, /ConvertTo-Json -Compress/); +assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\keil/); +assert.match(gatewayShellTool, /function powershellEncodedCommand/); +assert.match(gatewayShellTool, /Buffer\.from\(`\$\{prologue\}\$\{source\}`,\s*"utf16le"\)\.toString\("base64"\)/); +assert.match(gatewayShellTool, /--powershell-stdin/); +assert.match(gatewayShellTool, /powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand/); assert.match(artifactPublisher, /"system\.health"/); assert.match(artifactPublisher, /"cloud\.adapter\.describe"/); assert.match(artifactPublisher, /"audit\.event\.query"/); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index fc27e947..779a1bee 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -461,7 +461,6 @@ h3 { .compact-list, .conversation-list, -.task-list, .hardware-list { min-height: 0; overflow: auto; @@ -564,66 +563,6 @@ h3 { overflow-wrap: anywhere; } -.live-build-popover { - position: absolute; - z-index: 30; - top: calc(100% + 8px); - right: 0; - width: min(100%, 560px); - max-width: 100%; - max-height: min(420px, 58dvh); - display: none; - grid-template-rows: auto minmax(0, 1fr); - gap: 8px; - padding: 10px; - background: rgba(20, 22, 20, 0.99); - border: 1px solid var(--line-strong); - box-shadow: 0 18px 42px rgba(0, 0, 0, 0.42); - overflow: hidden; -} - -.live-build-summary[open] .live-build-popover { - display: grid; -} - -.live-build-popover-head { - min-width: 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.live-build-popover-head strong { - min-width: 0; - color: var(--text); - font-family: var(--mono); - font-size: 11px; - line-height: 1.2; - text-transform: none; - overflow-wrap: anywhere; -} - -.live-build-close { - flex: 0 0 auto; - min-height: 26px; - padding: 3px 8px; - border: 1px solid var(--line); - background: var(--surface-2); - color: var(--muted); - font-family: var(--mono); - font-size: 10px; - font-weight: 760; - cursor: pointer; -} - -.live-build-close:hover, -.live-build-close:focus-visible { - border-color: var(--line-strong); - color: var(--text); - outline: 0; -} - .live-build-list { min-width: 0; max-height: min(352px, calc(58dvh - 54px)); @@ -633,6 +572,10 @@ h3 { overscroll-behavior: contain; } +.live-build-list-template { + display: none; +} + .live-build-row { min-width: 0; max-width: 100%; @@ -692,6 +635,89 @@ h3 { overflow-wrap: anywhere; } +.workbench-dialog-layer { + position: fixed; + z-index: 1000; + inset: 0; + display: grid; + place-items: center; + padding: 18px; +} + +.workbench-dialog-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(0, 0, 0, 0.58); + cursor: pointer; +} + +.workbench-dialog { + position: relative; + z-index: 1; + width: min(820px, calc(100vw - 36px)); + max-height: min(720px, calc(100dvh - 36px)); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 10px; + padding: 12px; + background: rgba(20, 22, 20, 0.99); + border: 1px solid var(--line-strong); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.54); + overflow: hidden; +} + +.workbench-dialog-head { + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.workbench-dialog-head strong { + min-width: 0; + color: var(--text); + font-family: var(--mono); + font-size: 11px; + line-height: 1.2; + overflow-wrap: anywhere; +} + +.workbench-dialog-close { + flex: 0 0 auto; + min-height: 28px; + padding: 3px 9px; + border: 1px solid var(--line); + background: var(--surface-2); + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + cursor: pointer; +} + +.workbench-dialog-close:hover, +.workbench-dialog-close:focus-visible { + border-color: var(--line-strong); + color: var(--text); + outline: 0; +} + +.workbench-dialog-content { + min-width: 0; + min-height: 0; + overflow: auto; + overscroll-behavior: contain; +} + +.workbench-dialog-content .live-build-list, +.workbench-dialog-content .message-runtime-grid, +.workbench-dialog-content .message-session-grid { + max-height: none; + overflow: visible; +} + .view { min-height: 0; overflow: auto; @@ -714,7 +740,7 @@ h3 { .conversation-column { min-height: 100%; display: grid; - grid-template-rows: minmax(280px, 1fr) minmax(170px, 0.45fr); + grid-template-rows: minmax(0, 1fr); gap: 10px; } @@ -730,10 +756,6 @@ h3 { grid-template-rows: auto auto minmax(0, 1fr); } -.task-panel { - grid-template-rows: auto minmax(0, 1fr); -} - .help-panel { height: 100%; grid-template-rows: auto minmax(0, 1fr); @@ -741,8 +763,7 @@ h3 { } .conversation-list, -.compact-list, -.task-list { +.compact-list { display: grid; align-content: start; gap: 8px; @@ -993,6 +1014,44 @@ h3 { border-left-width: 3px; } +.message-compact-details { + display: block; + padding: 0; +} + +.message-compact-summary { + width: 100%; + min-width: 0; + min-height: 30px; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 7px; + align-items: center; + padding: 6px 8px; + border: 0; + background: transparent; + color: var(--muted); + text-align: left; + cursor: pointer; +} + +.message-compact-summary-text { + min-width: 0; + color: var(--muted); + font-size: 10px; + line-height: 1.35; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-compact-summary:hover, +.message-compact-summary:focus-visible { + background: rgba(255, 255, 255, 0.03); + color: var(--text); + outline: 0; +} + .message-trace { background: rgba(20, 24, 23, 0.76); border-left-width: 3px; @@ -1514,6 +1573,34 @@ h3 { outline: 0; } +.agent-timeout-control { + grid-column: 1 / -1; + justify-self: start; + display: inline-grid; + grid-template-columns: auto auto; + align-items: center; + gap: 8px; + min-height: 30px; + padding: 4px 8px; + border: 1px solid rgba(94, 142, 231, 0.38); + background: rgba(25, 31, 39, 0.78); + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + font-weight: 760; + line-height: 1.2; +} + +.agent-timeout-control select { + min-width: 88px; + min-height: 24px; + border: 1px solid var(--line); + background: var(--surface); + color: var(--text); + font: inherit; + cursor: pointer; +} + .input-shell { min-width: 0; display: grid; @@ -2399,7 +2486,7 @@ tbody tr:last-child td { } .conversation-column { - grid-template-rows: minmax(210px, 1fr) minmax(150px, 0.45fr); + grid-template-rows: minmax(0, 1fr); gap: 8px; } @@ -2418,6 +2505,11 @@ tbody tr:last-child td { min-height: 30px; } + .agent-timeout-control { + width: 100%; + justify-content: start; + } + .input-shell { padding: 0 8px; } @@ -2441,16 +2533,8 @@ tbody tr:last-child td { gap: 2px; } - .live-build-popover { - right: 0; - left: auto; - width: 100%; - max-width: 100%; - max-height: min(420px, 56dvh); - } - .live-build-list { - max-height: min(352px, calc(56dvh - 54px)); + max-height: min(352px, calc(70dvh - 54px)); } .live-build-time { @@ -2552,6 +2636,15 @@ tbody tr:last-child td { flex-basis: 100%; } + .agent-timeout-control { + grid-template-columns: minmax(0, 1fr); + justify-items: start; + } + + .agent-timeout-control select { + width: 100%; + } + .prompt-mark { display: none; }