mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge. Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error.
168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { selectAgentTimelineState, useSessionStore } from "@/stores/session-store";
|
|
import {
|
|
createInitDeferred,
|
|
getInitDeferred,
|
|
getInitKey,
|
|
INIT_TIMEOUT_MS,
|
|
rejectInitDeferred,
|
|
refreshInitTimeout,
|
|
} from "@/utils/agent-initialization";
|
|
import { getHostRuntimeStore, type HostRuntimeStore } from "@/runtime/host-runtime";
|
|
import { planInitialAgentTimelineSync, planTimelineTailFetch } from "@/timeline/timeline-sync-plan";
|
|
import { i18n } from "@/i18n/i18next";
|
|
|
|
export type SetAgentInitializing = (agentId: string, initializing: boolean) => void;
|
|
|
|
export function createHistorySyncTimeoutError(): Error {
|
|
return new Error(`History sync timed out after ${Math.round(INIT_TIMEOUT_MS / 1000)}s`);
|
|
}
|
|
|
|
export function refreshAgentInitializationTimeout(input: {
|
|
key: string;
|
|
agentId: string;
|
|
setAgentInitializing: SetAgentInitializing;
|
|
}): void {
|
|
refreshInitTimeout({
|
|
key: input.key,
|
|
onTimeout: () => {
|
|
input.setAgentInitializing(input.agentId, false);
|
|
rejectInitDeferred(input.key, createHistorySyncTimeoutError());
|
|
},
|
|
});
|
|
}
|
|
|
|
export interface EnsureAgentIsInitializedInput {
|
|
serverId: string;
|
|
agentId: string;
|
|
client: Pick<DaemonClient, "fetchAgentTimeline"> | null;
|
|
runtime: Pick<HostRuntimeStore, "fetchAgentTimeline">;
|
|
setAgentInitializing: SetAgentInitializing;
|
|
hostDisconnectedMessage?: string;
|
|
}
|
|
|
|
export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput): Promise<void> {
|
|
const { serverId, agentId, client, setAgentInitializing } = input;
|
|
const key = getInitKey(serverId, agentId);
|
|
const existing = getInitDeferred(key);
|
|
if (existing) {
|
|
return existing.promise;
|
|
}
|
|
|
|
const session = useSessionStore.getState().sessions[serverId];
|
|
const timeline = selectAgentTimelineState(session, agentId);
|
|
const cursor = timeline.status === "synced" ? (timeline.range ?? undefined) : undefined;
|
|
const hasAuthoritativeHistory = timeline.status === "synced";
|
|
const timelineRequest = planInitialAgentTimelineSync({ cursor, hasAuthoritativeHistory });
|
|
|
|
const deferred = createInitDeferred(key, timelineRequest.direction);
|
|
refreshAgentInitializationTimeout({ key, agentId, setAgentInitializing });
|
|
|
|
setAgentInitializing(agentId, true);
|
|
|
|
if (!client) {
|
|
setAgentInitializing(agentId, false);
|
|
rejectInitDeferred(
|
|
key,
|
|
new Error(input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected")),
|
|
);
|
|
return deferred.promise;
|
|
}
|
|
|
|
input.runtime.fetchAgentTimeline(serverId, agentId, timelineRequest).catch((error) => {
|
|
setAgentInitializing(agentId, false);
|
|
rejectInitDeferred(key, error instanceof Error ? error : new Error(String(error)));
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
export interface RefreshAgentInput {
|
|
serverId: string;
|
|
agentId: string;
|
|
client: Pick<DaemonClient, "refreshAgent"> | null;
|
|
runtime: Pick<HostRuntimeStore, "fetchAgentTimeline">;
|
|
setAgentInitializing: SetAgentInitializing;
|
|
hostDisconnectedMessage?: string;
|
|
}
|
|
|
|
export async function refreshAgent(input: RefreshAgentInput): Promise<void> {
|
|
const { serverId, agentId, client, runtime, setAgentInitializing } = input;
|
|
if (!client) {
|
|
throw new Error(input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected"));
|
|
}
|
|
setAgentInitializing(agentId, true);
|
|
|
|
try {
|
|
await client.refreshAgent(agentId);
|
|
await runtime.fetchAgentTimeline(serverId, agentId, planTimelineTailFetch());
|
|
} catch (error) {
|
|
setAgentInitializing(agentId, false);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function createSetAgentInitializing(
|
|
serverId: string,
|
|
setInitializingAgents: ReturnType<typeof useSessionStore.getState>["setInitializingAgents"],
|
|
): SetAgentInitializing {
|
|
return (agentId, initializing) => {
|
|
setInitializingAgents(serverId, (prev) => {
|
|
if (prev.get(agentId) === initializing) {
|
|
return prev;
|
|
}
|
|
const next = new Map(prev);
|
|
next.set(agentId, initializing);
|
|
return next;
|
|
});
|
|
};
|
|
}
|
|
|
|
export function useAgentInitialization({
|
|
serverId,
|
|
client,
|
|
}: {
|
|
serverId: string;
|
|
client: DaemonClient | null;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
|
|
const setAgentInitializing = useMemo(
|
|
() => createSetAgentInitializing(serverId, setInitializingAgents),
|
|
[serverId, setInitializingAgents],
|
|
);
|
|
|
|
const ensureAgentIsInitializedCallback = useCallback(
|
|
(agentId: string): Promise<void> =>
|
|
ensureAgentIsInitialized({
|
|
serverId,
|
|
agentId,
|
|
client,
|
|
runtime: getHostRuntimeStore(),
|
|
setAgentInitializing,
|
|
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
|
}),
|
|
[client, serverId, setAgentInitializing, t],
|
|
);
|
|
|
|
const refreshAgentCallback = useCallback(
|
|
(agentId: string): Promise<void> =>
|
|
refreshAgent({
|
|
serverId,
|
|
agentId,
|
|
client,
|
|
runtime: getHostRuntimeStore(),
|
|
setAgentInitializing,
|
|
hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"),
|
|
}),
|
|
[client, serverId, setAgentInitializing, t],
|
|
);
|
|
|
|
return {
|
|
ensureAgentIsInitialized: ensureAgentIsInitializedCallback,
|
|
refreshAgent: refreshAgentCallback,
|
|
};
|
|
}
|