perf: 小优化组 + 恢复浏览器页签

1. MCP 空闲睡眠 50ms -> 10ms:#187 后单轮 drain 约 0.3ms,
   100 次/s 空闲迭代约 3% 单核,请求与远控输入拾取延迟
   从平均 25ms 降到 5ms。
2. stream 轮询 50ms -> 20ms。
3. stream payload 加 url/title;面板 stream 处理合并 url/
   title,5 秒 state 轮询只在未观看时跑(观看时 stream 已
   全量覆盖,轮询纯冗余;不观看时保留轮询,避免会话开始
   后页眉 stale)。
4. 恢复浏览器页签:#176(ccb5a86)改写 TaskDetailPanel
   页签时顺带删了 BrowserPanel 的挂载,导致线上控制台
   自 #176 起就没有浏览器页签(面板文件一直在,只是不
   被 import,构建产物因此不变)。本次把 import、页签
   与 Card 原样挂回;构建产物含“开始观看”标记可证。
console.html 由 npm run build:console 重打(342484 字节)。
console-redesign/ 为构建脚本审阅产物,不进仓库。
This commit is contained in:
host committed 2026-10-30 14:09:02 +08:00
1 parent 5e562d8413
commit cbf5e65a7a
7 files changed
+79 -40

No files matched your search

@@ -5,6 +5,7 @@ import { shortId, fmt } from "../api/client";
import { STATUS_EXPLAIN } from "../api/types";
import { firstChar, durationOf, tokenOf, fmtCostUsd } from "../api/taskMeta";
import DetailActions from "./detail/DetailActions";
import BrowserPanel from "./detail/BrowserPanel";
import ReviewPanel from "./detail/ReviewPanel";
import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "./detail/DetailLists";
@@ -17,11 +18,14 @@ import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "
// 协作:协调子任务 / 子代理链路 / 给其他任务发消息
// 评审与审批:评审队列 / 运行时审批队列
// 线程:线程管理
// 浏览器:浏览器远控(#176 改写页签时顺带掉了本页签,
// 本次恢复;面板本身一直在 detail/BrowserPanel.tsx,
// 只是没被挂进页签)。
// Data, actions and sub-sections are unchanged; only grouping
// and visibility changed.
type DetailTab = "overview" | "collab" | "review" | "threads";
type DetailTab = "overview" | "collab" | "review" | "threads" | "browser";
const DETAIL_TABS: Array<[DetailTab, string]> = [
["overview", "概览"], ["collab", "协作"], ["review", "评审与审批"], ["threads", "线程"],
["overview", "概览"], ["collab", "协作"], ["review", "评审与审批"], ["threads", "线程"], ["browser", "浏览器"],
];
export default function TaskDetailPanel() {
const ctx = useConsole();
@@ -113,6 +117,8 @@ export default function TaskDetailPanel() {
</>) : tab === "review" ? (<>
<Card title="评审队列"><ReviewPanel /></Card>
<Card title="运行时审批队列"><ApprovalPanel /></Card>
</>) : tab === "browser" ? (<>
<Card title="浏览器远控"><BrowserPanel /></Card>
</>) : (<>
<Card title="线程"><ThreadPanel /></Card>
</>)}
@@ -52,13 +52,22 @@ export default function BrowserPanel() {
abortRef.current?.abort();
if (!t) return;
void refreshState();
const timer = setInterval(() => void refreshState(), 5000);
return () => {
clearInterval(timer);
abortRef.current?.abort();
};
}, [t, refreshState]);
// While watching, the stream payload already carries holder/url/
// title at frame rate (plus a 5s heartbeat), so the 5s state
// poll would be pure redundancy -- run it only while not
// watching, so the header still goes fresh when a session
// starts before the user opens the stream.
useEffect(() => {
if (!t || watching) return;
const timer = setInterval(() => void refreshState(), 5000);
return () => clearInterval(timer);
}, [t, watching, refreshState]);
const startWatch = useCallback(async () => {
if (!t) return;
abortRef.current?.abort();
@@ -94,12 +103,18 @@ export default function BrowserPanel() {
const payload = JSON.parse(line.slice(6)) as {
image?: string;
holder?: string;
url?: string;
title?: string;
};
if (payload.image) {
setImg("data:image/jpeg;base64," + payload.image);
}
if (payload.holder) {
setState((s) => ({ ...(s || {}), holder: payload.holder }));
const patch: BrowserState = {};
if (payload.holder) patch.holder = payload.holder;
if (payload.url) patch.url = payload.url;
if (payload.title !== undefined) patch.title = payload.title;
if (Object.keys(patch).length > 0) {
setState((s) => ({ ...(s || {}), ...patch }));
}
} catch {
// ignore a torn frame line