160 lines
3.7 KiB
TypeScript
160 lines
3.7 KiB
TypeScript
import { useRef } from "react";
|
|
import type { Agent } from "@/contexts/session-context";
|
|
|
|
export type AgentScreenMissingState =
|
|
| { kind: "idle" }
|
|
| { kind: "resolving" }
|
|
| { kind: "not_found"; message: string }
|
|
| { kind: "error"; message: string };
|
|
|
|
export interface AgentScreenMachineInput {
|
|
agent: Agent | null;
|
|
placeholderAgent: Agent | null;
|
|
missingAgentState: AgentScreenMissingState;
|
|
isConnected: boolean;
|
|
isArchivingCurrentAgent: boolean;
|
|
isHistorySyncing: boolean;
|
|
needsAuthoritativeSync: boolean;
|
|
shouldUseOptimisticStream: boolean;
|
|
}
|
|
|
|
export interface AgentScreenMachineMemory {
|
|
hasRenderedReady: boolean;
|
|
lastReadyAgent: Agent | null;
|
|
}
|
|
|
|
export type AgentScreenSyncStatus =
|
|
| "idle"
|
|
| "catching_up"
|
|
| "reconnecting"
|
|
| "sync_error";
|
|
|
|
export type AgentScreenViewState =
|
|
| {
|
|
tag: "boot";
|
|
reason: "loading" | "resolving";
|
|
source: "none";
|
|
}
|
|
| {
|
|
tag: "not_found";
|
|
message: string;
|
|
}
|
|
| {
|
|
tag: "error";
|
|
message: string;
|
|
}
|
|
| {
|
|
tag: "ready";
|
|
agent: Agent;
|
|
source: "authoritative" | "optimistic" | "stale";
|
|
syncStatus: AgentScreenSyncStatus;
|
|
isArchiving: boolean;
|
|
};
|
|
|
|
export function deriveAgentScreenViewState({
|
|
input,
|
|
memory,
|
|
}: {
|
|
input: AgentScreenMachineInput;
|
|
memory: AgentScreenMachineMemory;
|
|
}): { state: AgentScreenViewState; memory: AgentScreenMachineMemory } {
|
|
const nextMemory: AgentScreenMachineMemory = {
|
|
hasRenderedReady: memory.hasRenderedReady,
|
|
lastReadyAgent: memory.lastReadyAgent,
|
|
};
|
|
|
|
const candidateAgent = input.agent ?? input.placeholderAgent;
|
|
if (candidateAgent) {
|
|
nextMemory.hasRenderedReady = true;
|
|
nextMemory.lastReadyAgent = candidateAgent;
|
|
}
|
|
|
|
if (input.missingAgentState.kind === "not_found") {
|
|
return {
|
|
state: {
|
|
tag: "not_found",
|
|
message: input.missingAgentState.message,
|
|
},
|
|
memory: nextMemory,
|
|
};
|
|
}
|
|
|
|
if (input.missingAgentState.kind === "error" && !nextMemory.hasRenderedReady) {
|
|
return {
|
|
state: {
|
|
tag: "error",
|
|
message: input.missingAgentState.message,
|
|
},
|
|
memory: nextMemory,
|
|
};
|
|
}
|
|
|
|
const displayAgent =
|
|
candidateAgent ?? (nextMemory.hasRenderedReady ? nextMemory.lastReadyAgent : null);
|
|
if (!displayAgent) {
|
|
return {
|
|
state: {
|
|
tag: "boot",
|
|
reason: input.missingAgentState.kind === "resolving" ? "resolving" : "loading",
|
|
source: "none",
|
|
},
|
|
memory: nextMemory,
|
|
};
|
|
}
|
|
|
|
const source: "authoritative" | "optimistic" | "stale" = input.agent
|
|
? "authoritative"
|
|
: input.shouldUseOptimisticStream
|
|
? "optimistic"
|
|
: "stale";
|
|
|
|
let syncStatus: AgentScreenSyncStatus = "idle";
|
|
if (!input.isConnected) {
|
|
syncStatus = "reconnecting";
|
|
} else if (input.missingAgentState.kind === "error") {
|
|
syncStatus = "sync_error";
|
|
} else if (input.needsAuthoritativeSync || input.isHistorySyncing) {
|
|
syncStatus = "catching_up";
|
|
}
|
|
|
|
return {
|
|
state: {
|
|
tag: "ready",
|
|
agent: displayAgent,
|
|
source,
|
|
syncStatus,
|
|
isArchiving: input.isArchivingCurrentAgent,
|
|
},
|
|
memory: nextMemory,
|
|
};
|
|
}
|
|
|
|
export function useAgentScreenStateMachine({
|
|
routeKey,
|
|
input,
|
|
}: {
|
|
routeKey: string;
|
|
input: AgentScreenMachineInput;
|
|
}): AgentScreenViewState {
|
|
const routeKeyRef = useRef(routeKey);
|
|
const memoryRef = useRef<AgentScreenMachineMemory>({
|
|
hasRenderedReady: false,
|
|
lastReadyAgent: null,
|
|
});
|
|
|
|
if (routeKeyRef.current !== routeKey) {
|
|
routeKeyRef.current = routeKey;
|
|
memoryRef.current = {
|
|
hasRenderedReady: false,
|
|
lastReadyAgent: null,
|
|
};
|
|
}
|
|
|
|
const result = deriveAgentScreenViewState({
|
|
input,
|
|
memory: memoryRef.current,
|
|
});
|
|
memoryRef.current = result.memory;
|
|
return result.state;
|
|
}
|