From 896489a1c285c4583e150b5ec7dd5be1ce654dcf Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 14:23:32 +0800 Subject: [PATCH] fix: speed up v02 workspace first screen --- .../src/state/workbench-reducer.ts | 4 +- web/hwlab-cloud-web/src/state/workbench.ts | 64 ++++++++++++++----- web/hwlab-cloud-web/src/styles/workbench.css | 8 +-- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/web/hwlab-cloud-web/src/state/workbench-reducer.ts b/web/hwlab-cloud-web/src/state/workbench-reducer.ts index d68d1410..6fab9574 100644 --- a/web/hwlab-cloud-web/src/state/workbench-reducer.ts +++ b/web/hwlab-cloud-web/src/state/workbench-reducer.ts @@ -5,7 +5,7 @@ import type { ComposerState, WorkbenchState } from "./workbench-state"; export type Action = | { type: "hydrate:start" } - | { type: "hydrate:done"; workspace: WorkbenchState["workspace"]; conversations: ConversationRecord[]; messages: ChatMessage[] } + | { type: "hydrate:done"; workspace: WorkbenchState["workspace"]; conversations?: ConversationRecord[]; messages: ChatMessage[] } | { type: "workspace:sync"; workspace: WorkbenchState["workspace"] } | { type: "hydrate:error"; error: string } | { type: "live:set"; live: WorkbenchState["live"]; availability: CodeAgentAvailability | null; selectedDevicePodId: string } @@ -27,7 +27,7 @@ export type Action = export function workbenchReducer(state: WorkbenchState, action: Action): WorkbenchState { switch (action.type) { case "hydrate:start": return { ...state, loading: true, error: null }; - case "hydrate:done": return { ...state, loading: false, workspace: action.workspace, conversations: action.conversations, messages: action.messages, error: null }; + case "hydrate:done": return { ...state, loading: false, workspace: action.workspace, conversations: action.conversations ?? state.conversations, messages: action.messages, error: null }; case "workspace:sync": return { ...state, workspace: action.workspace ?? state.workspace }; case "hydrate:error": return { ...state, loading: false, error: action.error }; case "live:set": return { ...state, live: action.live, codeAgentAvailability: action.availability, selectedDevicePodId: action.selectedDevicePodId }; diff --git a/web/hwlab-cloud-web/src/state/workbench.ts b/web/hwlab-cloud-web/src/state/workbench.ts index 2a928050..4ace2b31 100644 --- a/web/hwlab-cloud-web/src/state/workbench.ts +++ b/web/hwlab-cloud-web/src/state/workbench.ts @@ -4,14 +4,11 @@ import { api } from "../services/api/client"; import type { AgentChatResponse, AgentChatResultResponse, - ApiResult, ChatMessage, CodeAgentAvailability, - DevicePodEventsResponse, ConversationRecord, DevicePodItem, DevicePodListResponse, - DevicePodStatusResponse, LiveBuildsPayload, LiveSurface, ProviderProfile, @@ -52,6 +49,7 @@ import { const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq"; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000; const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000; +const FIRST_SCREEN_EVENT_DELAY_MS = 1_600; const initialState: WorkbenchState = { workspace: null, conversations: [], @@ -103,6 +101,10 @@ export interface WorkbenchStore { export function useWorkbenchStore(enabled: boolean): WorkbenchStore { const [state, dispatch] = useReducer(workbenchReducer, initialState); + const selectedDevicePodIdRef = useRef(initialState.selectedDevicePodId); + const liveRequestSeqRef = useRef(0); + const deferredEventTimerRef = useRef(null); + const devicePodEventsRef = useRef<{ devicePodId: string; events: LiveSurface["devicePodEvents"] } | null>(null); // Single shared activity clock so submit, trace polling, and user-typing // all reset the same inactivity window. Kept in a ref (not state) so // updates never trigger re-renders. @@ -136,28 +138,35 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore { const composer = useMemo(() => composerFromState(state, activeConversationId), [activeConversationId, state]); + useEffect(() => { + selectedDevicePodIdRef.current = state.selectedDevicePodId; + }, [state.selectedDevicePodId]); + const hydrate = useCallback(async () => { dispatch({ type: "hydrate:start" }); - const [workspaceResult, conversationsResult] = await Promise.all([ - api.workspace(WORKBENCH_PROJECT_ID), - api.conversations(WORKBENCH_PROJECT_ID) - ]); + const conversationsPromise = api.conversations(WORKBENCH_PROJECT_ID); + const workspaceResult = await api.workspace(WORKBENCH_PROJECT_ID); if (!workspaceResult.ok) { dispatch({ type: "hydrate:error", error: workspaceResult.error ?? "workspace unavailable" }); return; } const workspace = workspaceResult.data?.workspace ?? null; - const conversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : []; dispatch({ type: "hydrate:done", workspace, - conversations, messages: messagesFromWorkspace(workspace) }); + const conversationsResult = await conversationsPromise; + if (conversationsResult.ok) dispatch({ type: "conversation:list", conversations: conversationsResult.data?.conversations ?? [] }); }, []); const refreshLive = useCallback(async (devicePodId?: string) => { - const requestedDevicePodId = devicePodId ?? state.selectedDevicePodId; + const requestSeq = ++liveRequestSeqRef.current; + const requestedDevicePodId = devicePodId ?? selectedDevicePodIdRef.current; + if (deferredEventTimerRef.current !== null) { + window.clearTimeout(deferredEventTimerRef.current); + deferredEventTimerRef.current = null; + } const [healthLive, health, restIndex, adapter, devicePods, liveBuilds] = await Promise.all([ api.healthLive(), api.health(), @@ -168,12 +177,27 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore { ]); const visibleDevicePods = devicePodsFromResult(devicePods); const selected = selectVisibleDevicePodId(devicePods.data, visibleDevicePods, requestedDevicePodId); - const [devicePodStatus, devicePodEvents] = selected - ? await Promise.all([api.devicePodStatus(selected), api.devicePodEvents(selected)]) - : [null, null] satisfies [ApiResult | null, ApiResult | null]; - const live: LiveSurface = { healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents, liveBuilds, loadedAt: new Date().toISOString() }; + const devicePodStatus = selected ? await api.devicePodStatus(selected) : null; + const previousEvents = selected && devicePodEventsRef.current?.devicePodId === selected ? devicePodEventsRef.current.events : null; + const live: LiveSurface = { healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents: previousEvents, liveBuilds, loadedAt: new Date().toISOString() }; + if (requestSeq !== liveRequestSeqRef.current) return; + selectedDevicePodIdRef.current = selected; dispatch({ type: "live:set", live, availability: availabilityFromLive(live), selectedDevicePodId: selected }); - }, [state.selectedDevicePodId]); + + if (!selected) return; + const loadEvents = async () => { + const devicePodEvents = await api.devicePodEvents(selected); + if (requestSeq !== liveRequestSeqRef.current || selectedDevicePodIdRef.current !== selected) return; + devicePodEventsRef.current = { devicePodId: selected, events: devicePodEvents }; + const nextLive: LiveSurface = { ...live, devicePodEvents, loadedAt: new Date().toISOString() }; + dispatch({ type: "live:set", live: nextLive, availability: availabilityFromLive(nextLive), selectedDevicePodId: selected }); + }; + const eventDelayMs = previousEvents || devicePodId ? 0 : FIRST_SCREEN_EVENT_DELAY_MS; + deferredEventTimerRef.current = window.setTimeout(() => { + deferredEventTimerRef.current = null; + void loadEvents(); + }, eventDelayMs); + }, []); useEffect(() => { if (!enabled) return; @@ -184,7 +208,14 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore { if (!enabled) return; void refreshLive(); const timer = window.setInterval(() => void refreshLive(), 30_000); - return () => window.clearInterval(timer); + return () => { + window.clearInterval(timer); + liveRequestSeqRef.current += 1; + if (deferredEventTimerRef.current !== null) { + window.clearTimeout(deferredEventTimerRef.current); + deferredEventTimerRef.current = null; + } + }; }, [enabled, refreshLive]); // HWLAB #802: re-attach to a running trace on hydrate. @@ -380,6 +411,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore { dispatch({ type: "gateway-timeout:set", gatewayShellTimeoutMs: value }); }, []); const selectDevicePod = useCallback((devicePodId: string) => { + selectedDevicePodIdRef.current = devicePodId; window.localStorage.setItem("hwlab.workbench.devicePodId.v1", devicePodId); dispatch({ type: "device:selected", devicePodId }); void refreshLive(devicePodId); diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index 904ddc9e..0121eacf 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -288,16 +288,16 @@ button:disabled { cursor: not-allowed; opacity: 0.55; } .is-right-sidebar-collapsed .right-sidebar { display: none; } .device-pod-picker { display: grid; gap: 6px; font-weight: 800; } .device-pod-status { display: grid; gap: 6px; } -.device-pod-summary, .device-pod-interfaces { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; } -.summary-tile { min-width: 0; min-height: 52px; display: grid; gap: 2px; text-align: left; border: 1px solid var(--line); border-radius: 6px; background: #fff; padding: 7px 8px; } +.device-pod-summary, .device-pod-interfaces { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: minmax(58px, auto); gap: 6px; } +.summary-tile { min-width: 0; min-height: 58px; display: grid; align-content: start; gap: 2px; text-align: left; border: 1px solid var(--line); border-radius: 6px; background: #fff; padding: 7px 8px; } .summary-tile span { color: var(--muted); font-size: 12px; } .summary-tile strong { overflow-wrap: anywhere; font-size: 12px; line-height: 1.2; } .device-pod-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin: 0; } .device-pod-meta div { border: 1px solid var(--line); border-radius: 6px; padding: 6px; background: #fff; } .device-pod-meta dt { font-weight: 800; } .device-pod-meta dd { margin: 3px 0 0; overflow-wrap: anywhere; } -.device-pod-workspace { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 8px; overflow: hidden; } -.device-event-panel { min-height: 0; max-height: min(360px, 38dvh); display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; } +.device-pod-workspace { min-height: 0; display: grid; grid-template-rows: minmax(58px, auto) minmax(240px, 1fr); gap: 8px; overflow: hidden; } +.device-event-panel { min-height: 240px; max-height: min(360px, 38dvh); display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; } .device-event-panel header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px; border-bottom: 1px solid var(--line); font-weight: 800; } .device-event-scroll { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 10px; background: #17201c; color: #e9f5e8; } #device-event-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }