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 =
|
export type Action =
|
||||||
| { type: "hydrate:start" }
|
| { 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: "workspace:sync"; workspace: WorkbenchState["workspace"] }
|
||||||
| { type: "hydrate:error"; error: string }
|
| { type: "hydrate:error"; error: string }
|
||||||
| { type: "live:set"; live: WorkbenchState["live"]; availability: CodeAgentAvailability | null; selectedDevicePodId: 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 {
|
export function workbenchReducer(state: WorkbenchState, action: Action): WorkbenchState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "hydrate:start": return { ...state, loading: true, error: null };
|
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 "workspace:sync": return { ...state, workspace: action.workspace ?? state.workspace };
|
||||||
case "hydrate:error": return { ...state, loading: false, error: action.error };
|
case "hydrate:error": return { ...state, loading: false, error: action.error };
|
||||||
case "live:set": return { ...state, live: action.live, codeAgentAvailability: action.availability, selectedDevicePodId: action.selectedDevicePodId };
|
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 {
|
import type {
|
||||||
AgentChatResponse,
|
AgentChatResponse,
|
||||||
AgentChatResultResponse,
|
AgentChatResultResponse,
|
||||||
ApiResult,
|
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
CodeAgentAvailability,
|
CodeAgentAvailability,
|
||||||
DevicePodEventsResponse,
|
|
||||||
ConversationRecord,
|
ConversationRecord,
|
||||||
DevicePodItem,
|
DevicePodItem,
|
||||||
DevicePodListResponse,
|
DevicePodListResponse,
|
||||||
DevicePodStatusResponse,
|
|
||||||
LiveBuildsPayload,
|
LiveBuildsPayload,
|
||||||
LiveSurface,
|
LiveSurface,
|
||||||
ProviderProfile,
|
ProviderProfile,
|
||||||
@@ -52,6 +49,7 @@ import {
|
|||||||
const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq";
|
const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq";
|
||||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||||
|
const FIRST_SCREEN_EVENT_DELAY_MS = 1_600;
|
||||||
const initialState: WorkbenchState = {
|
const initialState: WorkbenchState = {
|
||||||
workspace: null,
|
workspace: null,
|
||||||
conversations: [],
|
conversations: [],
|
||||||
@@ -103,6 +101,10 @@ export interface WorkbenchStore {
|
|||||||
|
|
||||||
export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
||||||
const [state, dispatch] = useReducer(workbenchReducer, initialState);
|
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
|
// Single shared activity clock so submit, trace polling, and user-typing
|
||||||
// all reset the same inactivity window. Kept in a ref (not state) so
|
// all reset the same inactivity window. Kept in a ref (not state) so
|
||||||
// updates never trigger re-renders.
|
// updates never trigger re-renders.
|
||||||
@@ -136,28 +138,35 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
|
|
||||||
const composer = useMemo(() => composerFromState(state, activeConversationId), [activeConversationId, state]);
|
const composer = useMemo(() => composerFromState(state, activeConversationId), [activeConversationId, state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedDevicePodIdRef.current = state.selectedDevicePodId;
|
||||||
|
}, [state.selectedDevicePodId]);
|
||||||
|
|
||||||
const hydrate = useCallback(async () => {
|
const hydrate = useCallback(async () => {
|
||||||
dispatch({ type: "hydrate:start" });
|
dispatch({ type: "hydrate:start" });
|
||||||
const [workspaceResult, conversationsResult] = await Promise.all([
|
const conversationsPromise = api.conversations(WORKBENCH_PROJECT_ID);
|
||||||
api.workspace(WORKBENCH_PROJECT_ID),
|
const workspaceResult = await api.workspace(WORKBENCH_PROJECT_ID);
|
||||||
api.conversations(WORKBENCH_PROJECT_ID)
|
|
||||||
]);
|
|
||||||
if (!workspaceResult.ok) {
|
if (!workspaceResult.ok) {
|
||||||
dispatch({ type: "hydrate:error", error: workspaceResult.error ?? "workspace unavailable" });
|
dispatch({ type: "hydrate:error", error: workspaceResult.error ?? "workspace unavailable" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = workspaceResult.data?.workspace ?? null;
|
const workspace = workspaceResult.data?.workspace ?? null;
|
||||||
const conversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "hydrate:done",
|
type: "hydrate:done",
|
||||||
workspace,
|
workspace,
|
||||||
conversations,
|
|
||||||
messages: messagesFromWorkspace(workspace)
|
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 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([
|
const [healthLive, health, restIndex, adapter, devicePods, liveBuilds] = await Promise.all([
|
||||||
api.healthLive(),
|
api.healthLive(),
|
||||||
api.health(),
|
api.health(),
|
||||||
@@ -168,12 +177,27 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
]);
|
]);
|
||||||
const visibleDevicePods = devicePodsFromResult(devicePods);
|
const visibleDevicePods = devicePodsFromResult(devicePods);
|
||||||
const selected = selectVisibleDevicePodId(devicePods.data, visibleDevicePods, requestedDevicePodId);
|
const selected = selectVisibleDevicePodId(devicePods.data, visibleDevicePods, requestedDevicePodId);
|
||||||
const [devicePodStatus, devicePodEvents] = selected
|
const devicePodStatus = selected ? await api.devicePodStatus(selected) : null;
|
||||||
? await Promise.all([api.devicePodStatus(selected), api.devicePodEvents(selected)])
|
const previousEvents = selected && devicePodEventsRef.current?.devicePodId === selected ? devicePodEventsRef.current.events : null;
|
||||||
: [null, null] satisfies [ApiResult<DevicePodStatusResponse> | null, ApiResult<DevicePodEventsResponse> | null];
|
const live: LiveSurface = { healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents: previousEvents, liveBuilds, loadedAt: new Date().toISOString() };
|
||||||
const live: LiveSurface = { healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents, liveBuilds, loadedAt: new Date().toISOString() };
|
if (requestSeq !== liveRequestSeqRef.current) return;
|
||||||
|
selectedDevicePodIdRef.current = selected;
|
||||||
dispatch({ type: "live:set", live, availability: availabilityFromLive(live), selectedDevicePodId: 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(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
@@ -184,7 +208,14 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
void refreshLive();
|
void refreshLive();
|
||||||
const timer = window.setInterval(() => void refreshLive(), 30_000);
|
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]);
|
}, [enabled, refreshLive]);
|
||||||
|
|
||||||
// HWLAB #802: re-attach to a running trace on hydrate.
|
// 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 });
|
dispatch({ type: "gateway-timeout:set", gatewayShellTimeoutMs: value });
|
||||||
}, []);
|
}, []);
|
||||||
const selectDevicePod = useCallback((devicePodId: string) => {
|
const selectDevicePod = useCallback((devicePodId: string) => {
|
||||||
|
selectedDevicePodIdRef.current = devicePodId;
|
||||||
window.localStorage.setItem("hwlab.workbench.devicePodId.v1", devicePodId);
|
window.localStorage.setItem("hwlab.workbench.devicePodId.v1", devicePodId);
|
||||||
dispatch({ type: "device:selected", devicePodId });
|
dispatch({ type: "device:selected", devicePodId });
|
||||||
void refreshLive(devicePodId);
|
void refreshLive(devicePodId);
|
||||||
|
|||||||
@@ -288,16 +288,16 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|||||||
.is-right-sidebar-collapsed .right-sidebar { display: none; }
|
.is-right-sidebar-collapsed .right-sidebar { display: none; }
|
||||||
.device-pod-picker { display: grid; gap: 6px; font-weight: 800; }
|
.device-pod-picker { display: grid; gap: 6px; font-weight: 800; }
|
||||||
.device-pod-status { display: grid; gap: 6px; }
|
.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; }
|
.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: 52px; display: grid; gap: 2px; text-align: left; border: 1px solid var(--line); border-radius: 6px; background: #fff; padding: 7px 8px; }
|
.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 span { color: var(--muted); font-size: 12px; }
|
||||||
.summary-tile strong { overflow-wrap: anywhere; font-size: 12px; line-height: 1.2; }
|
.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 { 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 div { border: 1px solid var(--line); border-radius: 6px; padding: 6px; background: #fff; }
|
||||||
.device-pod-meta dt { font-weight: 800; }
|
.device-pod-meta dt { font-weight: 800; }
|
||||||
.device-pod-meta dd { margin: 3px 0 0; overflow-wrap: anywhere; }
|
.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-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: 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-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-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-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; }
|
#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