|
|
|
|
@@ -63,6 +63,9 @@ const listeners = new Set<() => void>();
|
|
|
|
|
const historyLoadInFlight = new Map<string, Promise<void>>();
|
|
|
|
|
const lastHistoryLoadAtBySession = new Map<string, number>();
|
|
|
|
|
const chatEventDedupe = new Map<string, number>();
|
|
|
|
|
let pendingStreamingDelta = '';
|
|
|
|
|
let pendingStreamingRunId: string | null = null;
|
|
|
|
|
let streamingFlushHandle: number | null = null;
|
|
|
|
|
|
|
|
|
|
let gatewaySubscribed = false;
|
|
|
|
|
let loadSessionsInFlight: Promise<void> | null = null;
|
|
|
|
|
@@ -98,6 +101,79 @@ function patchState(patch: Partial<ChatStoreState>): ChatStoreState {
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requestFrame(callback: () => void): number {
|
|
|
|
|
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
|
|
|
return window.requestAnimationFrame(() => callback());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return window.setTimeout(callback, 16);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelFrame(handle: number): void {
|
|
|
|
|
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') {
|
|
|
|
|
window.cancelAnimationFrame(handle);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.clearTimeout(handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyPendingStreamingDelta(): void {
|
|
|
|
|
if (!pendingStreamingDelta) return;
|
|
|
|
|
|
|
|
|
|
const previousContent = state.streamingMessage ? extractText(state.streamingMessage) : '';
|
|
|
|
|
const nextContent = previousContent + pendingStreamingDelta;
|
|
|
|
|
const nextRunId = pendingStreamingRunId;
|
|
|
|
|
|
|
|
|
|
pendingStreamingDelta = '';
|
|
|
|
|
pendingStreamingRunId = null;
|
|
|
|
|
|
|
|
|
|
patchState({
|
|
|
|
|
sending: true,
|
|
|
|
|
error: null,
|
|
|
|
|
activeRunId: nextRunId ?? state.activeRunId,
|
|
|
|
|
streamingMessage: {
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
content: nextContent,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
id: state.streamingMessage?.id || `stream-${nextRunId || Date.now()}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function flushPendingStreamingDelta(): void {
|
|
|
|
|
if (streamingFlushHandle !== null) {
|
|
|
|
|
cancelFrame(streamingFlushHandle);
|
|
|
|
|
streamingFlushHandle = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyPendingStreamingDelta();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetPendingStreamingDelta(): void {
|
|
|
|
|
if (streamingFlushHandle !== null) {
|
|
|
|
|
cancelFrame(streamingFlushHandle);
|
|
|
|
|
streamingFlushHandle = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pendingStreamingDelta = '';
|
|
|
|
|
pendingStreamingRunId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function queueStreamingDelta(delta: string, runId?: string): void {
|
|
|
|
|
if (!delta) return;
|
|
|
|
|
|
|
|
|
|
pendingStreamingDelta += delta;
|
|
|
|
|
pendingStreamingRunId = runId ?? pendingStreamingRunId;
|
|
|
|
|
|
|
|
|
|
if (streamingFlushHandle !== null) return;
|
|
|
|
|
|
|
|
|
|
streamingFlushHandle = requestFrame(() => {
|
|
|
|
|
streamingFlushHandle = null;
|
|
|
|
|
applyPendingStreamingDelta();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAgentIdFromSessionKey(sessionKey: string): string {
|
|
|
|
|
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
|
|
|
|
|
if (parsed.isAgentSession) return parsed.agentId;
|
|
|
|
|
@@ -527,6 +603,8 @@ async function loadHistory(sessionKey = state.currentSessionKey, quiet = false):
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function newSession(agentId = state.currentAgentId): Promise<void> {
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
|
|
|
|
|
const leavingEmpty =
|
|
|
|
|
!state.currentSessionKey.endsWith(':main') &&
|
|
|
|
|
state.messages.length === 0 &&
|
|
|
|
|
@@ -560,6 +638,7 @@ async function newSession(agentId = state.currentAgentId): Promise<void> {
|
|
|
|
|
|
|
|
|
|
function switchSession(sessionKey: string): void {
|
|
|
|
|
if (sessionKey === state.currentSessionKey) return;
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
patchState(buildSessionSwitchPatch(state, sessionKey));
|
|
|
|
|
void loadHistory(sessionKey);
|
|
|
|
|
}
|
|
|
|
|
@@ -575,6 +654,10 @@ function selectAgent(agentId: string): void {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteSession(sessionKey: string): Promise<void> {
|
|
|
|
|
if (sessionKey === state.currentSessionKey) {
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await gatewayRpc('session.delete', { sessionKey });
|
|
|
|
|
} catch {
|
|
|
|
|
@@ -724,6 +807,7 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
|
|
|
|
|
|
|
|
|
|
async function abortRun(): Promise<void> {
|
|
|
|
|
const sessionKey = state.currentSessionKey;
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
|
|
|
|
|
patchState({
|
|
|
|
|
sending: false,
|
|
|
|
|
@@ -749,21 +833,12 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
|
|
|
|
|
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case 'chat:delta': {
|
|
|
|
|
const previousContent = state.streamingMessage ? extractText(state.streamingMessage) : '';
|
|
|
|
|
patchState({
|
|
|
|
|
sending: true,
|
|
|
|
|
error: null,
|
|
|
|
|
activeRunId: typeof event.runId === 'string' ? event.runId : state.activeRunId,
|
|
|
|
|
streamingMessage: {
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
content: previousContent + event.delta,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
id: state.streamingMessage?.id || `stream-${event.runId || Date.now()}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
queueStreamingDelta(event.delta, typeof event.runId === 'string' ? event.runId : undefined);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'chat:final': {
|
|
|
|
|
flushPendingStreamingDelta();
|
|
|
|
|
|
|
|
|
|
const composedMessage = state.streamingMessage && typeof event.message.content === 'string'
|
|
|
|
|
? {
|
|
|
|
|
...event.message,
|
|
|
|
|
@@ -806,6 +881,8 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'chat:error': {
|
|
|
|
|
flushPendingStreamingDelta();
|
|
|
|
|
|
|
|
|
|
if (state.streamingMessage && !state.messages.some((message) => message.id === state.streamingMessage?.id)) {
|
|
|
|
|
patchState({
|
|
|
|
|
messages: [
|
|
|
|
|
@@ -827,9 +904,11 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
|
|
|
|
|
pendingFinal: false,
|
|
|
|
|
lastUserMessageAt: null,
|
|
|
|
|
});
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'chat:aborted': {
|
|
|
|
|
resetPendingStreamingDelta();
|
|
|
|
|
patchState({
|
|
|
|
|
sending: false,
|
|
|
|
|
activeRunId: null,
|
|
|
|
|
@@ -902,6 +981,7 @@ export const chatStore = {
|
|
|
|
|
stageAttachmentFiles,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function useChatStore(): ChatStoreState {
|
|
|
|
|
return useSyncExternalStore(chatStore.subscribe, chatStore.getSnapshot, chatStore.getSnapshot);
|
|
|
|
|
export function useChatStore<T = ChatStoreState>(selector?: (state: ChatStoreState) => T): T {
|
|
|
|
|
const select = selector ?? ((current: ChatStoreState) => current as unknown as T);
|
|
|
|
|
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
|
|
|
|
|
}
|
|
|
|
|
|