Merge pull request #913 from pikasTech/fix/issue912-workspace-first-screen
fix: speed up v0.2 workspace first screen
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user