Merge pull request #913 from pikasTech/fix/issue912-workspace-first-screen

fix: speed up v0.2 workspace first screen
This commit is contained in:
Lyon
2026-06-05 14:25:33 +08:00
committed by GitHub
3 changed files with 54 additions and 22 deletions
@@ -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 };
+48 -16
View File
@@ -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<number | null>(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<DevicePodStatusResponse> | null, ApiResult<DevicePodEventsResponse> | 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);
+4 -4
View File
@@ -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; }