Switch to double quotes and reformat codebase with Biome

This commit is contained in:
Mohamed Boudra
2026-03-21 20:22:15 +07:00
parent ab40786882
commit f6579dbeb4
805 changed files with 38132 additions and 40175 deletions
@@ -1,11 +1,6 @@
import { createContext, useContext, useEffect, useRef, type ReactNode } from "react";
import { useWindowDimensions } from "react-native";
import {
useSharedValue,
withTiming,
Easing,
type SharedValue,
} from "react-native-reanimated";
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
import { type GestureType } from "react-native-gesture-handler";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
@@ -22,12 +17,13 @@ interface ExplorerSidebarAnimationContextValue {
closeGestureRef: React.MutableRefObject<GestureType | undefined>;
}
const ExplorerSidebarAnimationContext = createContext<ExplorerSidebarAnimationContextValue | null>(null);
const ExplorerSidebarAnimationContext = createContext<ExplorerSidebarAnimationContextValue | null>(
null,
);
export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
@@ -130,7 +126,9 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
export function useExplorerSidebarAnimation() {
const context = useContext(ExplorerSidebarAnimationContext);
if (!context) {
throw new Error("useExplorerSidebarAnimation must be used within ExplorerSidebarAnimationProvider");
throw new Error(
"useExplorerSidebarAnimation must be used within ExplorerSidebarAnimationProvider",
);
}
return context;
}
@@ -31,7 +31,7 @@ export function HorizontalScrollProvider({ children }: { children: ReactNode })
scrollOffsetsRef.current.set(id, offset);
updateIsAnyScrolled();
},
[updateIsAnyScrolled]
[updateIsAnyScrolled],
);
const unregisterScrollOffset = useCallback(
@@ -39,7 +39,7 @@ export function HorizontalScrollProvider({ children }: { children: ReactNode })
scrollOffsetsRef.current.delete(id);
updateIsAnyScrolled();
},
[updateIsAnyScrolled]
[updateIsAnyScrolled],
);
return (
+116 -247
View File
@@ -5,10 +5,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useClientActivity } from "@/hooks/use-client-activity";
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
import {
generateMessageId,
type StreamItem,
} from "@/types/stream";
import { generateMessageId, type StreamItem } from "@/types/stream";
import {
processTimelineResponse,
processAgentStreamEvent,
@@ -27,14 +24,8 @@ import {
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { DaemonClient } from "@server/client/daemon-client";
import { File } from "expo-file-system";
import {
getHostRuntimeStore,
useHostRuntimeIsConnected,
} from "@/runtime/host-runtime";
import {
useVoiceAudioEngineOptional,
useVoiceRuntimeOptional,
} from "@/contexts/voice-context";
import { getHostRuntimeStore, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useVoiceAudioEngineOptional, useVoiceRuntimeOptional } from "@/contexts/voice-context";
import type { AudioPlaybackSource } from "@/voice/audio-engine-types";
import {
useSessionStore,
@@ -54,10 +45,7 @@ import {
rejectInitDeferred,
} from "@/utils/agent-initialization";
import { encodeImages } from "@/utils/encode-images";
import {
derivePendingPermissionKey,
normalizeAgentSnapshot,
} from "@/utils/agent-snapshots";
import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import { resolveProjectPlacement } from "@/utils/project-placement";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import type { AttachmentMetadata } from "@/attachments/types";
@@ -78,10 +66,7 @@ export type {
const HISTORY_STALE_AFTER_MS = 60_000;
const AUTHORITATIVE_REVALIDATION_DEBOUNCE_MS = 300;
type AudioOutputPayload = Extract<
SessionOutboundMessage,
{ type: "audio_output" }
>["payload"];
type AudioOutputPayload = Extract<SessionOutboundMessage, { type: "audio_output" }>["payload"];
interface BufferedAudioChunk {
chunkIndex: number;
@@ -94,9 +79,7 @@ function decodeBase64Chunk(base64: string): Uint8Array {
return Buffer.from(base64, "base64");
}
function buildAudioPlaybackSource(
chunks: BufferedAudioChunk[]
): AudioPlaybackSource {
function buildAudioPlaybackSource(chunks: BufferedAudioChunk[]): AudioPlaybackSource {
const decodedChunks = chunks.map((chunk) => decodeBase64Chunk(chunk.audio));
const totalSize = decodedChunks.reduce((sum, chunk) => sum + chunk.length, 0);
const output = new Uint8Array(totalSize);
@@ -111,7 +94,7 @@ function buildAudioPlaybackSource(
format === "pcm"
? "audio/pcm;rate=24000;bits=16"
: format === "mp3"
? "audio/mpeg"
? "audio/mpeg"
: `audio/${format}`;
const bytes = output.slice();
@@ -119,10 +102,7 @@ function buildAudioPlaybackSource(
size: bytes.byteLength,
type: mimeType,
async arrayBuffer() {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength
);
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
},
};
}
@@ -139,7 +119,7 @@ const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
const getLatestPermissionRequest = (
session: SessionState | undefined,
agentId: string
agentId: string,
): NotificationPermissionRequest | null => {
if (!session) {
return null;
@@ -173,10 +153,7 @@ type FileDownloadTokenPayload = Extract<
{ type: "file_download_token_response" }
>["payload"];
type AgentUpdatePayload = Extract<
SessionOutboundMessage,
{ type: "agent_update" }
>["payload"];
type AgentUpdatePayload = Extract<SessionOutboundMessage, { type: "agent_update" }>["payload"];
type WorkspaceUpdatePayload = Extract<
SessionOutboundMessage,
{ type: "workspace_update" }
@@ -197,14 +174,14 @@ function pendingKey(serverId: string, agentId: string): string {
export function bufferPendingAgentUpdate(
serverId: string,
agentId: string,
update: AgentUpdatePayload
update: AgentUpdatePayload,
): void {
pendingAgentUpdates.set(pendingKey(serverId, agentId), update);
}
export function flushPendingAgentUpdate(
serverId: string,
agentId: string
agentId: string,
): AgentUpdatePayload | undefined {
const key = pendingKey(serverId, agentId);
const update = pendingAgentUpdates.get(key);
@@ -212,10 +189,7 @@ export function flushPendingAgentUpdate(
return update;
}
export function deletePendingAgentUpdate(
serverId: string,
agentId: string
): void {
export function deletePendingAgentUpdate(serverId: string, agentId: string): void {
pendingAgentUpdates.delete(pendingKey(serverId, agentId));
}
@@ -238,11 +212,7 @@ interface SessionProviderClientProps extends SessionProviderSharedProps {
export type SessionProviderProps = SessionProviderClientProps;
function SessionProviderWithClient({
children,
serverId,
client,
}: SessionProviderClientProps) {
function SessionProviderWithClient({ children, serverId, client }: SessionProviderClientProps) {
return (
<SessionProviderInternal serverId={serverId} client={client}>
{children}
@@ -255,11 +225,7 @@ export function SessionProvider(props: SessionProviderProps) {
return <SessionProviderWithClient {...props} />;
}
function SessionProviderInternal({
children,
serverId,
client,
}: SessionProviderClientProps) {
function SessionProviderInternal({ children, serverId, client }: SessionProviderClientProps) {
const voiceRuntime = useVoiceRuntimeOptional();
const voiceAudioEngine = useVoiceAudioEngineOptional();
const queryClient = useQueryClient();
@@ -270,55 +236,29 @@ function SessionProviderInternal({
const clearSession = useSessionStore((state) => state.clearSession);
const setIsPlayingAudio = useSessionStore((state) => state.setIsPlayingAudio);
const setMessages = useSessionStore((state) => state.setMessages);
const setCurrentAssistantMessage = useSessionStore(
(state) => state.setCurrentAssistantMessage
);
const setAgentStreamTail = useSessionStore(
(state) => state.setAgentStreamTail
);
const setAgentStreamHead = useSessionStore(
(state) => state.setAgentStreamHead
);
const setAgentStreamState = useSessionStore(
(state) => state.setAgentStreamState
);
const clearAgentStreamHead = useSessionStore(
(state) => state.clearAgentStreamHead
);
const setAgentTimelineCursor = useSessionStore(
(state) => state.setAgentTimelineCursor
);
const setInitializingAgents = useSessionStore(
(state) => state.setInitializingAgents
);
const bumpHistorySyncGeneration = useSessionStore(
(state) => state.bumpHistorySyncGeneration
);
const setCurrentAssistantMessage = useSessionStore((state) => state.setCurrentAssistantMessage);
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
const setAgentStreamHead = useSessionStore((state) => state.setAgentStreamHead);
const setAgentStreamState = useSessionStore((state) => state.setAgentStreamState);
const clearAgentStreamHead = useSessionStore((state) => state.clearAgentStreamHead);
const setAgentTimelineCursor = useSessionStore((state) => state.setAgentTimelineCursor);
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
const bumpHistorySyncGeneration = useSessionStore((state) => state.bumpHistorySyncGeneration);
const markAgentHistorySynchronized = useSessionStore(
(state) => state.markAgentHistorySynchronized
(state) => state.markAgentHistorySynchronized,
);
const setAgentAuthoritativeHistoryApplied = useSessionStore(
(state) => state.setAgentAuthoritativeHistoryApplied
);
const setHasHydratedAgents = useSessionStore(
(state) => state.setHasHydratedAgents
);
const setHasHydratedWorkspaces = useSessionStore(
(state) => state.setHasHydratedWorkspaces
(state) => state.setAgentAuthoritativeHistoryApplied,
);
const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents);
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
const setAgents = useSessionStore((state) => state.setAgents);
const setWorkspaces = useSessionStore((state) => state.setWorkspaces);
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const removeWorkspace = useSessionStore((state) => state.removeWorkspace);
const setAgentLastActivity = useSessionStore(
(state) => state.setAgentLastActivity
);
const flushAgentLastActivity = useSessionStore(
(state) => state.flushAgentLastActivity
);
const setPendingPermissions = useSessionStore(
(state) => state.setPendingPermissions
);
const setAgentLastActivity = useSessionStore((state) => state.setAgentLastActivity);
const flushAgentLastActivity = useSessionStore((state) => state.flushAgentLastActivity);
const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions);
const clearDraftInput = useDraftStore((state) => state.clearDraftInput);
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
const updateSessionClient = useSessionStore((state) => state.updateSessionClient);
@@ -326,37 +266,22 @@ function SessionProviderInternal({
// Track focused agent for heartbeat
const focusedAgentId = useSessionStore(
(state) => state.sessions[serverId]?.focusedAgentId ?? null
);
const sessionAgents = useSessionStore(
(state) => state.sessions[serverId]?.agents
(state) => state.sessions[serverId]?.focusedAgentId ?? null,
);
const sessionAgents = useSessionStore((state) => state.sessions[serverId]?.agents);
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(
new Map()
);
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
const sendAgentMessageRef = useRef<
| ((
agentId: string,
message: string,
images?: AttachmentMetadata[]
) => Promise<void>)
| null
((agentId: string, message: string, images?: AttachmentMetadata[]) => Promise<void>) | null
>(null);
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const attentionNotifiedRef = useRef<Map<string, number>>(new Map());
const appStateRef = useRef(AppState.currentState);
const revalidationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const revalidationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const revalidationInFlightRef = useRef<Promise<void> | null>(null);
const revalidationQueuedRef = useRef(false);
const wasConnectedRef = useRef(isConnected);
const audioOutputBuffersRef = useRef<Map<string, BufferedAudioChunk[]>>(
new Map()
);
const audioOutputBuffersRef = useRef<Map<string, BufferedAudioChunk[]>>(new Map());
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
useEffect(() => {
@@ -420,7 +345,7 @@ function SessionProviderInternal({
setWorkspaces(serverId, workspaces);
setHasHydratedWorkspaces(serverId, true);
},
[client, isConnected, serverId, setHasHydratedWorkspaces, setWorkspaces]
[client, isConnected, serverId, setHasHydratedWorkspaces, setWorkspaces],
);
const applyAuthoritativeAgentSnapshot = useCallback(
@@ -523,7 +448,7 @@ function SessionProviderInternal({
setAgents,
setPendingPermissions,
setQueuedMessages,
]
],
);
const runAuthoritativeRevalidation = useCallback(async () => {
@@ -596,7 +521,7 @@ function SessionProviderInternal({
}
bumpHistorySyncGeneration(serverId);
},
[bumpHistorySyncGeneration, scheduleAuthoritativeRevalidation, serverId]
[bumpHistorySyncGeneration, scheduleAuthoritativeRevalidation, serverId],
);
// Client activity tracking (heartbeat, push token registration)
@@ -617,8 +542,7 @@ function SessionProviderInternal({
return;
}
const isActivelyVisible = getIsAppActivelyVisible(appState);
const isAwayFromAgent =
!isActivelyVisible || focusedAgentId !== params.agentId;
const isAwayFromAgent = !isActivelyVisible || focusedAgentId !== params.agentId;
if (!isAwayFromAgent) {
return;
}
@@ -633,12 +557,8 @@ function SessionProviderInternal({
const head = session?.agentStreamHead.get(params.agentId) ?? [];
const tail = session?.agentStreamTail.get(params.agentId) ?? [];
const assistantMessage =
findLatestAssistantMessageText(head) ??
findLatestAssistantMessageText(tail);
const permissionRequest = getLatestPermissionRequest(
session,
params.agentId
);
findLatestAssistantMessageText(head) ?? findLatestAssistantMessageText(tail);
const permissionRequest = getLatestPermissionRequest(session, params.agentId);
const notification =
params.notification ??
@@ -647,10 +567,7 @@ function SessionProviderInternal({
serverId,
agentId: params.agentId,
assistantMessage: params.reason === "finished" ? assistantMessage : null,
permissionRequest:
params.reason === "permission"
? permissionRequest
: null,
permissionRequest: params.reason === "permission" ? permissionRequest : null,
});
void sendOsNotification({
@@ -659,7 +576,7 @@ function SessionProviderInternal({
data: notification.data,
});
},
[serverId]
[serverId],
);
// Initialize session in store
@@ -676,7 +593,7 @@ function SessionProviderInternal({
return;
}
return voiceRuntime.registerSession({
return voiceRuntime.registerSession({
serverId,
setVoiceMode: async (enabled, agentId) => {
if (!client) {
@@ -706,12 +623,7 @@ function SessionProviderInternal({
setIsPlayingAudio(serverId, isPlaying);
},
});
}, [
client,
serverId,
setIsPlayingAudio,
voiceRuntime,
]);
}, [client, serverId, setIsPlayingAudio, voiceRuntime]);
useEffect(() => {
voiceRuntime?.updateSessionConnection(serverId, isConnected);
@@ -818,7 +730,7 @@ function SessionProviderInternal({
setAgents,
setPendingPermissions,
setAgentTimelineCursor,
]
],
);
const requestCanonicalCatchUp = useCallback(
@@ -831,14 +743,10 @@ function SessionProviderInternal({
projection: "canonical",
})
.catch((error) => {
console.warn(
"[Session] failed to fetch canonical catch-up timeline",
agentId,
error
);
console.warn("[Session] failed to fetch canonical catch-up timeline", agentId, error);
});
},
[client]
[client],
);
const applyTimelineResponse = useCallback(
@@ -846,7 +754,7 @@ function SessionProviderInternal({
payload: Extract<
SessionOutboundMessage,
{ type: "fetch_agent_timeline_response" }
>["payload"]
>["payload"],
) => {
const agentId = payload.agentId;
const initKey = getInitKey(serverId, agentId);
@@ -992,7 +900,7 @@ function SessionProviderInternal({
setAgentStreamTail,
setAgentTimelineCursor,
setInitializingAgents,
]
],
);
useEffect(() => {
@@ -1027,8 +935,7 @@ function SessionProviderInternal({
const initKey = getInitKey(serverId, agentId);
const session = useSessionStore.getState().sessions[serverId];
const isSyncingHistory =
session?.initializingAgents.get(agentId) === true &&
Boolean(getInitDeferred(initKey));
session?.initializingAgents.get(agentId) === true && Boolean(getInitDeferred(initKey));
if (isSyncingHistory) {
bufferPendingAgentUpdate(serverId, agentId, update);
@@ -1160,13 +1067,10 @@ function SessionProviderInternal({
// on status changes, which is sufficient for sorting and display purposes.
});
const unsubAgentTimeline = client.on(
"fetch_agent_timeline_response",
(message) => {
if (message.type !== "fetch_agent_timeline_response") return;
applyTimelineResponse(message.payload);
}
);
const unsubAgentTimeline = client.on("fetch_agent_timeline_response", (message) => {
if (message.type !== "fetch_agent_timeline_response") return;
applyTimelineResponse(message.payload);
});
const unsubWorkspaceUpdate = client.on("workspace_update", (message) => {
if (message.type !== "workspace_update") return;
@@ -1174,9 +1078,7 @@ function SessionProviderInternal({
removeWorkspace(serverId, message.payload.id);
return;
}
mergeWorkspaces(serverId, [
normalizeWorkspaceDescriptor(message.payload.workspace),
]);
mergeWorkspaces(serverId, [normalizeWorkspaceDescriptor(message.payload.workspace)]);
});
const unsubStatus = client.on("status", (message) => {
@@ -1187,53 +1089,42 @@ function SessionProviderInternal({
serverId: serverInfo.serverId,
hostname: serverInfo.hostname,
version: serverInfo.version,
...(serverInfo.capabilities
? { capabilities: serverInfo.capabilities }
: {}),
...(serverInfo.capabilities ? { capabilities: serverInfo.capabilities } : {}),
});
return;
}
});
const unsubPermissionRequest = client.on(
"agent_permission_request",
(message) => {
if (message.type !== "agent_permission_request") return;
const { agentId, request } = message.payload;
const unsubPermissionRequest = client.on("agent_permission_request", (message) => {
if (message.type !== "agent_permission_request") return;
const { agentId, request } = message.payload;
setPendingPermissions(serverId, (prev) => {
const next = new Map(prev);
const key = derivePendingPermissionKey(agentId, request);
next.set(key, { key, agentId, request });
return next;
});
}
);
setPendingPermissions(serverId, (prev) => {
const next = new Map(prev);
const key = derivePendingPermissionKey(agentId, request);
next.set(key, { key, agentId, request });
return next;
});
});
const unsubPermissionResolved = client.on(
"agent_permission_resolved",
(message) => {
if (message.type !== "agent_permission_resolved") return;
const { requestId, agentId } = message.payload;
const unsubPermissionResolved = client.on("agent_permission_resolved", (message) => {
if (message.type !== "agent_permission_resolved") return;
const { requestId, agentId } = message.payload;
setPendingPermissions(serverId, (prev) => {
const next = new Map(prev);
const derivedKey = `${agentId}:${requestId}`;
if (!next.delete(derivedKey)) {
for (const [key, pending] of next.entries()) {
if (
pending.agentId === agentId &&
pending.request.id === requestId
) {
next.delete(key);
break;
}
setPendingPermissions(serverId, (prev) => {
const next = new Map(prev);
const derivedKey = `${agentId}:${requestId}`;
if (!next.delete(derivedKey)) {
for (const [key, pending] of next.entries()) {
if (pending.agentId === agentId && pending.request.id === requestId) {
next.delete(key);
break;
}
}
return next;
});
}
);
}
return next;
});
});
const unsubAudioOutput = client.on("audio_output", async (message) => {
if (message.type !== "audio_output") return;
@@ -1273,16 +1164,15 @@ function SessionProviderInternal({
bufferedChunks.sort((left, right) => left.chunkIndex - right.chunkIndex);
const chunkIds = bufferedChunks.map((chunk) => chunk.id);
const shouldPlay =
!payload.isVoiceMode ||
(voiceRuntime?.shouldPlayVoiceAudio(serverId) ?? false);
!payload.isVoiceMode || (voiceRuntime?.shouldPlayVoiceAudio(serverId) ?? false);
const audioBlob = buildAudioPlaybackSource(bufferedChunks);
const confirmAudioPlayed = async () => {
await Promise.all(
chunkIds.map((chunkId) =>
client.audioPlayed(chunkId).catch((error) => {
console.warn("[Session] Failed to confirm audio playback:", error);
})
)
}),
),
);
};
@@ -1352,17 +1242,13 @@ function SessionProviderInternal({
prev.map((msg) =>
msg.type === "tool_call" && msg.id === toolCallId
? { ...msg, result, status: "completed" as const }
: msg
)
: msg,
),
);
return;
}
if (
data.type === "error" &&
data.metadata &&
"toolCallId" in data.metadata
) {
if (data.type === "error" && data.metadata && "toolCallId" in data.metadata) {
const { toolCallId, error } = data.metadata as {
toolCallId: string;
error: unknown;
@@ -1372,8 +1258,8 @@ function SessionProviderInternal({
prev.map((msg) =>
msg.type === "tool_call" && msg.id === toolCallId
? { ...msg, error, status: "failed" as const }
: msg
)
: msg,
),
);
}
@@ -1422,10 +1308,7 @@ function SessionProviderInternal({
const unsubChunk = client.on("assistant_chunk", (message) => {
if (message.type !== "assistant_chunk") return;
setCurrentAssistantMessage(
serverId,
(prev) => prev + message.payload.chunk
);
setCurrentAssistantMessage(serverId, (prev) => prev + message.payload.chunk);
});
const unsubTranscription = client.on("transcription_result", (message) => {
@@ -1442,10 +1325,7 @@ function SessionProviderInternal({
const unsubVoiceInputState = client.on("voice_input_state", (message) => {
if (message.type !== "voice_input_state") return;
voiceRuntime?.onServerSpeechStateChanged(
serverId,
message.payload.isSpeaking
);
voiceRuntime?.onServerSpeechStateChanged(serverId, message.payload.isSpeaking);
});
const unsubAgentDeleted = client.on("agent_deleted", (message) => {
@@ -1589,11 +1469,7 @@ function SessionProviderInternal({
]);
const sendAgentMessage = useCallback(
async (
agentId: string,
message: string,
images?: AttachmentMetadata[]
) => {
async (agentId: string, message: string, images?: AttachmentMetadata[]) => {
const messageId = generateMessageId();
const userMessage: StreamItem = {
kind: "user_message",
@@ -1605,7 +1481,9 @@ function SessionProviderInternal({
// Append to head if streaming (keeps the user message with the current
// turn so late text_deltas still find the existing assistant_message).
// Otherwise append to tail.
const currentHead = useSessionStore.getState().sessions[serverId]?.agentStreamHead?.get(agentId);
const currentHead = useSessionStore
.getState()
.sessions[serverId]?.agentStreamHead?.get(agentId);
if (currentHead && currentHead.length > 0) {
setAgentStreamHead(serverId, (prev) => {
const head = prev.get(agentId) || [];
@@ -1630,15 +1508,13 @@ function SessionProviderInternal({
void client
.sendAgentMessage(agentId, message, {
messageId,
...(imagesData && imagesData.length > 0
? { images: imagesData }
: {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
})
.catch((error) => {
console.error("[Session] Failed to send agent message:", error);
});
},
[encodeImages, serverId, client, setAgentStreamTail, setAgentStreamHead]
[encodeImages, serverId, client, setAgentStreamTail, setAgentStreamHead],
);
// Keep the ref updated so the agent_update handler can call it
@@ -1654,7 +1530,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to cancel agent:", error);
});
},
[client]
[client],
);
const deleteAgent = useCallback(
@@ -1667,7 +1543,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to delete agent:", error);
});
},
[client]
[client],
);
const archiveAgent = useCallback(
@@ -1680,7 +1556,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to archive agent:", error);
});
},
[client]
[client],
);
const restartServer = useCallback(
@@ -1693,7 +1569,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to restart server:", error);
});
},
[client]
[client],
);
const createAgent = useCallback(
@@ -1721,10 +1597,7 @@ function SessionProviderInternal({
try {
imagesData = await encodeImages(images);
} catch (error) {
console.error(
"[Session] Failed to prepare images for agent creation:",
error
);
console.error("[Session] Failed to prepare images for agent creation:", error);
}
return client.createAgent({
config,
@@ -1735,7 +1608,7 @@ function SessionProviderInternal({
...(requestId ? { requestId } : {}),
});
},
[encodeImages, client]
[encodeImages, client],
);
const setAgentMode = useCallback(
@@ -1748,7 +1621,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to set agent mode:", error);
});
},
[client]
[client],
);
const setAgentModel = useCallback(
@@ -1761,7 +1634,7 @@ function SessionProviderInternal({
console.error("[Session] Failed to set agent model:", error);
});
},
[client]
[client],
);
const setAgentThinkingOption = useCallback(
@@ -1770,13 +1643,11 @@ function SessionProviderInternal({
console.warn("[Session] setAgentThinkingOption skipped: daemon unavailable");
return;
}
void client
.setAgentThinkingOption(agentId, thinkingOptionId)
.catch((error) => {
console.error("[Session] Failed to set agent thinking option:", error);
});
void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => {
console.error("[Session] Failed to set agent thinking option:", error);
});
},
[client]
[client],
);
const respondToPermission = useCallback(
@@ -1785,13 +1656,11 @@ function SessionProviderInternal({
console.warn("[Session] respondToPermission skipped: daemon unavailable");
return;
}
void client
.respondToPermission(agentId, requestId, response)
.catch((error) => {
console.error("[Session] Failed to respond to permission:", error);
});
void client.respondToPermission(agentId, requestId, response).catch((error) => {
console.error("[Session] Failed to respond to permission:", error);
});
},
[client]
[client],
);
// Cleanup on unmount
@@ -15,15 +15,9 @@ const turnFailedEvent: AgentStreamEventPayload = {
describe("session stream lifecycle helpers", () => {
it("derives optimistic terminal lifecycle only when current status is running", () => {
expect(deriveOptimisticLifecycleStatus("running", turnCompletedEvent)).toBe(
"idle"
);
expect(deriveOptimisticLifecycleStatus("running", turnFailedEvent)).toBe(
"error"
);
expect(deriveOptimisticLifecycleStatus("initializing", turnCompletedEvent)).toBe(
null
);
expect(deriveOptimisticLifecycleStatus("running", turnCompletedEvent)).toBe("idle");
expect(deriveOptimisticLifecycleStatus("running", turnFailedEvent)).toBe("error");
expect(deriveOptimisticLifecycleStatus("initializing", turnCompletedEvent)).toBe(null);
expect(deriveOptimisticLifecycleStatus("idle", turnFailedEvent)).toBe(null);
});
});
@@ -3,7 +3,7 @@ import type { AgentStreamEventPayload } from "@server/shared/messages";
export function deriveOptimisticLifecycleStatus(
currentStatus: AgentLifecycleStatus,
event: AgentStreamEventPayload
event: AgentStreamEventPayload,
): AgentLifecycleStatus | null {
if (currentStatus !== "running") {
return null;
@@ -13,11 +13,7 @@ import {
// Test helpers
// ---------------------------------------------------------------------------
function makeTimelineEntry(
seq: number,
text: string,
type: string = "assistant_message"
) {
function makeTimelineEntry(seq: number, text: string, type: string = "assistant_message") {
return {
seqStart: seq,
provider: "claude",
@@ -28,7 +24,7 @@ function makeTimelineEntry(
function makeTimelineEvent(
text: string,
type: string = "assistant_message"
type: string = "assistant_message",
): AgentStreamEventPayload {
return {
type: "timeline",
@@ -161,9 +157,7 @@ describe("processTimelineResponse", () => {
endSeq: 3,
});
expect(result.error).toBe(null);
expect(
result.sideEffects.some((e) => e.type === "flush_pending_updates")
).toBe(true);
expect(result.sideEffects.some((e) => e.type === "flush_pending_updates")).toBe(true);
});
it("sets cursor to null when reset=true but no cursors in payload", () => {
@@ -193,10 +187,7 @@ describe("processTimelineResponse", () => {
epoch: "epoch-1",
startCursor: { seq: 1 },
endCursor: { seq: 5 },
entries: [
makeTimelineEntry(1, "first"),
makeTimelineEntry(5, "last"),
],
entries: [makeTimelineEntry(1, "first"), makeTimelineEntry(5, "last")],
},
});
@@ -232,10 +223,7 @@ describe("processTimelineResponse", () => {
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [
makeTimelineEntry(4, "next-1"),
makeTimelineEntry(5, "next-2"),
],
entries: [makeTimelineEntry(4, "next-1"), makeTimelineEntry(5, "next-2")],
},
});
@@ -262,9 +250,7 @@ describe("processTimelineResponse", () => {
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [
makeTimelineEntry(10, "far ahead"),
],
entries: [makeTimelineEntry(10, "far ahead")],
},
});
@@ -290,10 +276,7 @@ describe("processTimelineResponse", () => {
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [
makeTimelineEntry(5, "old"),
makeTimelineEntry(7, "also old"),
],
entries: [makeTimelineEntry(5, "old"), makeTimelineEntry(7, "also old")],
},
});
@@ -315,9 +298,7 @@ describe("processTimelineResponse", () => {
payload: {
...baseTimelineInput.payload,
epoch: "epoch-2",
entries: [
makeTimelineEntry(6, "different epoch"),
],
entries: [makeTimelineEntry(6, "different epoch")],
},
});
@@ -386,9 +367,7 @@ describe("processTimelineResponse", () => {
},
});
expect(
result.sideEffects.some((e) => e.type === "flush_pending_updates")
).toBe(true);
expect(result.sideEffects.some((e) => e.type === "flush_pending_updates")).toBe(true);
});
it("initializes cursor when no existing cursor on first entries", () => {
@@ -398,10 +377,7 @@ describe("processTimelineResponse", () => {
payload: {
...baseTimelineInput.payload,
epoch: "epoch-1",
entries: [
makeTimelineEntry(1, "first"),
makeTimelineEntry(2, "second"),
],
entries: [makeTimelineEntry(1, "first"), makeTimelineEntry(2, "second")],
},
});
@@ -1,11 +1,7 @@
import type { AgentStreamEventPayload } from "@server/shared/messages";
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { StreamItem } from "@/types/stream";
import {
applyStreamEvent,
hydrateStreamState,
reduceStreamUpdate,
} from "@/types/stream";
import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
import {
classifySessionTimelineSeq,
type SessionTimelineSeqDecision,
@@ -84,7 +80,7 @@ export interface ProcessTimelineResponseOutput {
}
export function processTimelineResponse(
input: ProcessTimelineResponseInput
input: ProcessTimelineResponseInput,
): ProcessTimelineResponseOutput {
const {
payload,
@@ -126,7 +122,7 @@ export function processTimelineResponse(
}));
const toHydratedEvents = (
units: typeof timelineUnits
units: typeof timelineUnits,
): Array<{ event: AgentStreamEventPayload; timestamp: Date }> =>
units.map(({ event, timestamp }) => ({ event, timestamp }));
@@ -186,17 +182,13 @@ export function processTimelineResponse(
for (const unit of timelineUnits) {
const decision: SessionTimelineSeqDecision = classifySessionTimelineSeq({
cursor: cursor
? { epoch: cursor.epoch, endSeq: cursor.endSeq }
: null,
cursor: cursor ? { epoch: cursor.epoch, endSeq: cursor.endSeq } : null,
epoch: payload.epoch,
seq: unit.seq,
});
if (decision === "gap") {
gapCursor = cursor
? { epoch: cursor.epoch, endSeq: cursor.endSeq }
: null;
gapCursor = cursor ? { epoch: cursor.epoch, endSeq: cursor.endSeq } : null;
break;
}
if (decision === "drop_stale" || decision === "drop_epoch") {
@@ -227,7 +219,7 @@ export function processTimelineResponse(
reduceStreamUpdate(state, event, timestamp, {
source: "canonical",
}),
currentTail
currentTail,
);
}
@@ -262,12 +254,9 @@ export function processTimelineResponse(
responseDirection: payload.direction,
reset: payload.reset,
});
const clearInitializing =
shouldResolveDeferredInit || (isInitializing && !hasActiveInitDeferred);
const clearInitializing = shouldResolveDeferredInit || (isInitializing && !hasActiveInitDeferred);
const initResolution: "resolve" | "reject" | null = shouldResolveDeferredInit
? "resolve"
: null;
const initResolution: "resolve" | "reject" | null = shouldResolveDeferredInit ? "resolve" : null;
return {
tail: nextTail,
@@ -319,18 +308,10 @@ export interface ProcessAgentStreamEventOutput {
}
export function processAgentStreamEvent(
input: ProcessAgentStreamEventInput
input: ProcessAgentStreamEventInput,
): ProcessAgentStreamEventOutput {
const {
event,
seq,
epoch,
currentTail,
currentHead,
currentCursor,
currentAgent,
timestamp,
} = input;
const { event, seq, epoch, currentTail, currentHead, currentCursor, currentAgent, timestamp } =
input;
let shouldApplyStreamEvent = true;
let nextTimelineCursor: TimelineCursor | null = null;
@@ -340,15 +321,9 @@ export function processAgentStreamEvent(
// ------------------------------------------------------------------
// Timeline sequencing gate
// ------------------------------------------------------------------
if (
event.type === "timeline" &&
typeof seq === "number" &&
typeof epoch === "string"
) {
if (event.type === "timeline" && typeof seq === "number" && typeof epoch === "string") {
const decision = classifySessionTimelineSeq({
cursor: currentCursor
? { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq }
: null,
cursor: currentCursor ? { epoch: currentCursor.epoch, endSeq: currentCursor.endSeq } : null,
epoch,
seq,
});
@@ -410,18 +385,12 @@ export function processAgentStreamEvent(
event.type === "turn_canceled" ||
event.type === "turn_failed")
) {
const optimisticStatus = deriveOptimisticLifecycleStatus(
currentAgent.status,
event
);
const optimisticStatus = deriveOptimisticLifecycleStatus(currentAgent.status, event);
if (optimisticStatus) {
const nextUpdatedAtMs = Math.max(
currentAgent.updatedAt.getTime(),
timestamp.getTime()
);
const nextUpdatedAtMs = Math.max(currentAgent.updatedAt.getTime(), timestamp.getTime());
const nextLastActivityAtMs = Math.max(
currentAgent.lastActivityAt.getTime(),
timestamp.getTime()
timestamp.getTime(),
);
agentPatch = {
status: optimisticStatus,
@@ -78,7 +78,7 @@ describe("shouldResolveTimelineInit", () => {
initRequestDirection: "tail",
responseDirection: "tail",
reset: false,
})
}),
).toBe(true);
});
@@ -90,7 +90,7 @@ describe("shouldResolveTimelineInit", () => {
initRequestDirection: "tail",
responseDirection: "after",
reset: false,
})
}),
).toBe(false);
});
@@ -102,7 +102,7 @@ describe("shouldResolveTimelineInit", () => {
initRequestDirection: "after",
responseDirection: "after",
reset: false,
})
}),
).toBe(true);
});
});
@@ -62,8 +62,7 @@ export function deriveBootstrapTailTimelinePolicy({
return { replace: true, catchUpCursor: null };
}
const isBootstrapTailInit =
direction === "tail" && isInitializing && hasActiveInitDeferred;
const isBootstrapTailInit = direction === "tail" && isInitializing && hasActiveInitDeferred;
if (!isBootstrapTailInit) {
return { replace: false, catchUpCursor: null };
}
@@ -8,7 +8,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: { epoch: "epoch-1", endSeq: 4 },
epoch: "epoch-1",
seq: 5,
})
}),
).toBe("accept");
});
@@ -18,7 +18,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: { epoch: "epoch-1", endSeq: 8 },
epoch: "epoch-1",
seq: 7,
})
}),
).toBe("drop_stale");
});
@@ -28,7 +28,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: { epoch: "epoch-1", endSeq: 8 },
epoch: "epoch-1",
seq: 8,
})
}),
).toBe("drop_stale");
});
@@ -38,7 +38,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: { epoch: "epoch-1", endSeq: 4 },
epoch: "epoch-2",
seq: 5,
})
}),
).toBe("drop_epoch");
});
@@ -48,7 +48,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: null,
epoch: "epoch-1",
seq: 1,
})
}),
).toBe("init");
});
@@ -58,7 +58,7 @@ describe("classifySessionTimelineSeq", () => {
cursor: { epoch: "epoch-1", endSeq: 4 },
epoch: "epoch-1",
seq: 9,
})
}),
).toBe("gap");
});
});
@@ -6,12 +6,7 @@ export type SessionTimelineSeqCursor =
| null
| undefined;
export type SessionTimelineSeqDecision =
| "accept"
| "drop_stale"
| "drop_epoch"
| "gap"
| "init";
export type SessionTimelineSeqDecision = "accept" | "drop_stale" | "drop_epoch" | "gap" | "init";
export function classifySessionTimelineSeq({
cursor,
@@ -1,11 +1,14 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode } from "react";
import { useWindowDimensions } from "react-native";
import {
useSharedValue,
withTiming,
Easing,
type SharedValue,
} from "react-native-reanimated";
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
type ReactNode,
} from "react";
import { useWindowDimensions } from "react-native";
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
import { type GestureType } from "react-native-gesture-handler";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
@@ -27,8 +30,7 @@ const SidebarAnimationContext = createContext<SidebarAnimationContextValue | nul
export function SidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
@@ -112,13 +114,19 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
isGesturing,
closeGestureRef,
}),
[translateX, backdropOpacity, windowWidth, animateToOpen, animateToClose, isGesturing, closeGestureRef]
[
translateX,
backdropOpacity,
windowWidth,
animateToOpen,
animateToClose,
isGesturing,
closeGestureRef,
],
);
return (
<SidebarAnimationContext.Provider value={value}>
{children}
</SidebarAnimationContext.Provider>
<SidebarAnimationContext.Provider value={value}>{children}</SidebarAnimationContext.Provider>
);
}
+1 -5
View File
@@ -1,9 +1,5 @@
import { createContext, useContext, type ReactNode } from "react";
import {
ToastViewport,
useToastHost,
type ToastApi,
} from "@/components/toast-host";
import { ToastViewport, useToastHost, type ToastApi } from "@/components/toast-host";
const ToastContext = createContext<ToastApi | null>(null);
+3 -5
View File
@@ -59,7 +59,7 @@ export function useVoiceOptional(): VoiceContextValue | null {
const snapshot = useSyncExternalStore(
runtime ? runtime.subscribe : noopSubscribe,
runtime ? runtime.getSnapshot : getEmptySnapshot,
runtime ? runtime.getSnapshot : getEmptySnapshot
runtime ? runtime.getSnapshot : getEmptySnapshot,
);
if (!runtime) {
@@ -88,7 +88,7 @@ export function useVoiceTelemetryOptional(): VoiceRuntimeTelemetrySnapshot | nul
const snapshot = useSyncExternalStore(
runtime ? runtime.subscribeTelemetry : noopSubscribe,
runtime ? runtime.getTelemetrySnapshot : getEmptyTelemetry,
runtime ? runtime.getTelemetrySnapshot : getEmptyTelemetry
runtime ? runtime.getTelemetrySnapshot : getEmptyTelemetry,
);
return runtime ? snapshot : null;
@@ -153,9 +153,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
return (
<VoiceAudioEngineContext.Provider value={engine}>
<VoiceRuntimeContext.Provider value={runtime}>
{children}
</VoiceRuntimeContext.Provider>
<VoiceRuntimeContext.Provider value={runtime}>{children}</VoiceRuntimeContext.Provider>
</VoiceAudioEngineContext.Provider>
);
}