feat: implement daemon connectivity updates and relay protocol versioning

This commit is contained in:
Mohamed Boudra
2026-02-20 19:27:56 +07:00
parent 6dad3212da
commit 0350f00840
49 changed files with 4146 additions and 1351 deletions
@@ -29,6 +29,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { Shortcut } from '@/components/ui/shortcut'
import { Autocomplete } from '@/components/ui/autocomplete'
import { useAgentAutocomplete } from '@/hooks/use-agent-autocomplete'
import { useHostRuntimeSession } from '@/runtime/host-runtime'
type QueuedMessage = {
id: string
@@ -77,10 +78,14 @@ export function AgentInputArea({
(s) => s.clearMessageInputActionRequest
)
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null)
const { client, isConnected, snapshot } = useHostRuntimeSession(serverId)
const toast = useToast()
const voice = useVoiceOptional()
const isConnected = client?.isConnected ?? false
const isDictationReady =
isConnected &&
(snapshot?.agentDirectoryStatus === 'ready' ||
snapshot?.agentDirectoryStatus === 'revalidating' ||
snapshot?.agentDirectoryStatus === 'error_after_ready')
const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId))
@@ -672,6 +677,7 @@ export function AgentInputArea({
onAddImages={addImages}
onRemoveImage={handleRemoveImage}
client={client}
isReadyForDictation={isDictationReady}
placeholder="Message agent..."
autoFocus={autoFocus}
disabled={isSubmitLoading}
+14 -12
View File
@@ -14,7 +14,7 @@ import { router, usePathname } from "expo-router";
import { usePanelStore } from "@/stores/panel-store";
import { SidebarAgentList } from "./sidebar-agent-list";
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
import { useSidebarAgentsGrouped } from "@/hooks/use-sidebar-agents-grouped";
import { useSidebarAgentsList } from "@/hooks/use-sidebar-agents-list";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
@@ -94,22 +94,24 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
const [selectedProjectKeys, setSelectedProjectKeys] = useState<string[]>([]);
const [selectedProjectFilterKeys, setSelectedProjectFilterKeys] = useState<
string[]
>([]);
const {
entries,
projectOptions,
projectFilterOptions,
hasMoreEntries,
isInitialLoad,
isRevalidating,
refreshAll,
} = useSidebarAgentsGrouped({
} = useSidebarAgentsList({
isOpen,
serverId: activeServerId,
selectedProjectKeys,
selectedProjectFilterKeys,
});
useEffect(() => {
setSelectedProjectKeys([]);
setSelectedProjectFilterKeys([]);
}, [activeServerId]);
const {
translateX,
@@ -373,9 +375,9 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
) : (
<SidebarAgentList
entries={entries}
projectOptions={projectOptions}
selectedProjectKeys={selectedProjectKeys}
onSelectedProjectKeysChange={setSelectedProjectKeys}
projectFilterOptions={projectFilterOptions}
selectedProjectFilterKeys={selectedProjectFilterKeys}
onSelectedProjectFilterKeysChange={setSelectedProjectFilterKeys}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
listFooterComponent={listFooterComponent}
@@ -499,9 +501,9 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
) : (
<SidebarAgentList
entries={entries}
projectOptions={projectOptions}
selectedProjectKeys={selectedProjectKeys}
onSelectedProjectKeysChange={setSelectedProjectKeys}
projectFilterOptions={projectFilterOptions}
selectedProjectFilterKeys={selectedProjectFilterKeys}
onSelectedProjectFilterKeysChange={setSelectedProjectFilterKeys}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
listFooterComponent={listFooterComponent}
@@ -55,6 +55,8 @@ export interface MessageInputProps {
onAddImages?: (images: ImageAttachment[]) => void
onRemoveImage?: (index: number) => void
client: DaemonClient | null
/** Dictation start gate from host runtime (socket connected + directory ready). */
isReadyForDictation?: boolean
placeholder?: string
autoFocus?: boolean
disabled?: boolean
@@ -121,6 +123,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
onAddImages,
onRemoveImage,
client,
isReadyForDictation,
placeholder = 'Message...',
autoFocus = false,
disabled = false,
@@ -262,14 +265,16 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
const canStartDictation = useCallback(() => {
const socketConnected = client?.isConnected ?? false
return socketConnected && !disabled && !dictationUnavailableMessage
}, [client, disabled, dictationUnavailableMessage])
const readyForDictation = isReadyForDictation ?? socketConnected
return socketConnected && readyForDictation && !disabled && !dictationUnavailableMessage
}, [client, disabled, dictationUnavailableMessage, isReadyForDictation])
const canConfirmDictation = useCallback(() => {
const socketConnected = client?.isConnected ?? false
return socketConnected
}, [client])
const isConnected = client?.isConnected ?? false
const isDictationStartEnabled = (isReadyForDictation ?? isConnected) && !disabled
const {
isRecording: isDictating,
@@ -719,7 +724,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={handleVoicePress}
disabled={!isConnected || disabled}
disabled={!isDictationStartEnabled}
accessibilityRole="button"
accessibilityLabel={
isRealtimeVoiceForCurrentAgent
@@ -732,7 +737,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
}
style={[
styles.voiceButton,
(!isConnected || disabled) && styles.buttonDisabled,
(!isDictationStartEnabled) && styles.buttonDisabled,
isDictating && styles.voiceButtonRecording,
]}
>
@@ -1,28 +1,13 @@
import { useEffect } from "react";
import { AppState } from "react-native";
import { SessionProvider } from "@/contexts/session-context";
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import {
getHostRuntimeStore,
useHostRuntimeSession,
} from "@/runtime/host-runtime";
import { toDaemonConnectionUpdateFromRuntime } from "@/runtime/host-runtime-bridge";
function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
const { snapshot, client } = useHostRuntimeSession(daemon.serverId);
const { connectionStates, updateConnectionStatus } = useDaemonConnections();
const hasConnectionRecord = connectionStates.has(daemon.serverId);
useEffect(() => {
if (!hasConnectionRecord) {
return;
}
updateConnectionStatus(
daemon.serverId,
toDaemonConnectionUpdateFromRuntime(snapshot)
);
}, [daemon.serverId, hasConnectionRecord, snapshot, updateConnectionStatus]);
const { client } = useHostRuntimeSession(daemon.serverId);
if (!client) {
return null;
@@ -47,20 +32,6 @@ export function MultiDaemonSessionHost() {
runtime.syncHosts(daemons);
}, [daemons]);
useEffect(() => {
const runtime = getHostRuntimeStore();
const subscription = AppState.addEventListener("change", (nextState) => {
if (nextState !== "active") {
return;
}
runtime.ensureConnectedAll();
void runtime.runProbeCycleNow();
});
return () => {
subscription.remove();
};
}, []);
if (daemons.length === 0) {
return null;
}
@@ -43,8 +43,8 @@ import {
import { projectIconQueryKey } from "@/hooks/use-project-icon-query";
import {
type SidebarAgentListEntry,
type SidebarProjectOption,
} from "@/hooks/use-sidebar-agents-grouped";
type SidebarProjectFilterOption,
} from "@/hooks/use-sidebar-agents-list";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { getIsTauri } from "@/constants/layout";
import { AgentStatusDot } from "@/components/agent-status-dot";
@@ -59,9 +59,9 @@ type EntryData = SidebarAgentListEntry;
interface SidebarAgentListProps {
entries: SidebarAgentListEntry[];
projectOptions: SidebarProjectOption[];
selectedProjectKeys: string[];
onSelectedProjectKeysChange: (keys: string[]) => void;
projectFilterOptions: SidebarProjectFilterOption[];
selectedProjectFilterKeys: string[];
onSelectedProjectFilterKeysChange: (keys: string[]) => void;
isRefreshing?: boolean;
onRefresh?: () => void;
selectedAgentId?: string;
@@ -72,7 +72,7 @@ interface SidebarAgentListProps {
}
interface ProjectFilterOptionRowProps {
option: SidebarProjectOption;
option: SidebarProjectFilterOption;
selected: boolean;
iconDataUri: string | null;
displayName: string;
@@ -381,15 +381,15 @@ function deriveShortcutIndexByAgentKey(sidebarShortcutAgentKeys: string[]) {
}
function resolveSelectedProjectLabel(input: {
selectedProjectKeys: string[];
projectOptions: SidebarProjectOption[];
selectedProjectFilterKeys: string[];
projectFilterOptions: SidebarProjectFilterOption[];
}): string {
if (input.selectedProjectKeys.length === 0) {
if (input.selectedProjectFilterKeys.length === 0) {
return "Project";
}
if (input.selectedProjectKeys.length === 1) {
const selected = input.projectOptions.find(
(option) => option.projectKey === input.selectedProjectKeys[0]
if (input.selectedProjectFilterKeys.length === 1) {
const selected = input.projectFilterOptions.find(
(option) => option.projectKey === input.selectedProjectFilterKeys[0]
);
if (selected) {
return deriveProjectDisplayName({
@@ -399,7 +399,7 @@ function resolveSelectedProjectLabel(input: {
});
}
return deriveProjectDisplayName({
projectKey: input.selectedProjectKeys[0] ?? "",
projectKey: input.selectedProjectFilterKeys[0] ?? "",
projectName: "",
remoteUrl: null,
});
@@ -444,9 +444,9 @@ function deriveProjectDisplayName(input: {
export function SidebarAgentList({
entries,
projectOptions,
selectedProjectKeys,
onSelectedProjectKeysChange,
projectFilterOptions,
selectedProjectFilterKeys,
onSelectedProjectFilterKeysChange,
isRefreshing = false,
onRefresh,
selectedAgentId,
@@ -479,23 +479,23 @@ export function SidebarAgentList({
);
const selectedProjectLabel = useMemo(
() => resolveSelectedProjectLabel({ selectedProjectKeys, projectOptions }),
[projectOptions, selectedProjectKeys]
() => resolveSelectedProjectLabel({ selectedProjectFilterKeys, projectFilterOptions }),
[projectFilterOptions, selectedProjectFilterKeys]
);
const selectedProjectOption = useMemo(() => {
if (selectedProjectKeys.length !== 1) {
if (selectedProjectFilterKeys.length !== 1) {
return null;
}
return (
projectOptions.find((option) => option.projectKey === selectedProjectKeys[0]) ?? null
projectFilterOptions.find((option) => option.projectKey === selectedProjectFilterKeys[0]) ?? null
);
}, [projectOptions, selectedProjectKeys]);
const showProjectFilters = projectOptions.length > 0;
}, [projectFilterOptions, selectedProjectFilterKeys]);
const showProjectFilters = projectFilterOptions.length > 0;
const projectIconRequests = useMemo(() => {
const unique = new Map<string, { serverId: string; cwd: string }>();
for (const option of projectOptions) {
for (const option of projectFilterOptions) {
if (!option.serverId || !option.workingDir) {
continue;
}
@@ -513,7 +513,7 @@ export function SidebarAgentList({
unique.set(`${serverId}:${cwd}`, { serverId, cwd });
}
return Array.from(unique.values());
}, [entries, projectOptions]);
}, [entries, projectFilterOptions]);
const projectIconQueries = useQueries({
queries: projectIconRequests.map((request) => ({
@@ -559,7 +559,7 @@ export function SidebarAgentList({
const projectIconByProjectKey = useMemo(() => {
const map = new Map<string, string | null>();
for (const option of projectOptions) {
for (const option of projectFilterOptions) {
map.set(
option.projectKey,
projectIconByQueryKey.get(`${option.serverId}:${option.workingDir}`) ?? null
@@ -577,7 +577,7 @@ export function SidebarAgentList({
);
}
return map;
}, [entries, projectIconByQueryKey, projectOptions]);
}, [entries, projectIconByQueryKey, projectFilterOptions]);
const selectedProjectIconUri = useMemo(() => {
if (!selectedProjectOption) {
@@ -588,20 +588,20 @@ export function SidebarAgentList({
const handleToggleProject = useCallback(
(projectKey: string) => {
const next = new Set(selectedProjectKeys);
const next = new Set(selectedProjectFilterKeys);
if (next.has(projectKey)) {
next.delete(projectKey);
} else {
next.add(projectKey);
}
onSelectedProjectKeysChange(Array.from(next));
onSelectedProjectFilterKeysChange(Array.from(next));
},
[onSelectedProjectKeysChange, selectedProjectKeys]
[onSelectedProjectFilterKeysChange, selectedProjectFilterKeys]
);
const handleClearProjectFilter = useCallback(() => {
onSelectedProjectKeysChange([]);
}, [onSelectedProjectKeysChange]);
onSelectedProjectFilterKeysChange([]);
}, [onSelectedProjectFilterKeysChange]);
const handleAgentPress = useCallback(
(entry: SidebarAgentListEntry) => {
@@ -755,7 +755,7 @@ export function SidebarAgentList({
ref={projectFilterAnchorRef}
style={({ hovered = false, pressed }) => [
styles.filterTrigger,
(selectedProjectKeys.length > 0 || hovered || pressed) &&
(selectedProjectFilterKeys.length > 0 || hovered || pressed) &&
styles.filterTriggerActive,
]}
onPress={() => setIsProjectFilterOpen(true)}
@@ -763,20 +763,20 @@ export function SidebarAgentList({
{({ hovered = false, pressed }) => {
const isInteracting = hovered || pressed;
const showActiveForeground =
selectedProjectKeys.length > 0 || isInteracting;
selectedProjectFilterKeys.length > 0 || isInteracting;
return (
<>
{selectedProjectKeys.length === 1 && selectedProjectIconUri ? (
{selectedProjectFilterKeys.length === 1 && selectedProjectIconUri ? (
<Image
source={{
uri: selectedProjectIconUri,
}}
style={styles.selectedProjectIcon}
/>
) : selectedProjectKeys.length > 1 ? (
) : selectedProjectFilterKeys.length > 1 ? (
<View style={styles.projectCountBadge}>
<Text style={styles.projectCountBadgeText}>
{selectedProjectKeys.length}
{selectedProjectFilterKeys.length}
</Text>
</View>
) : null}
@@ -802,7 +802,7 @@ export function SidebarAgentList({
}}
</Pressable>
{selectedProjectKeys.length > 0 ? (
{selectedProjectFilterKeys.length > 0 ? (
<Pressable style={styles.clearFilterButton} onPress={handleClearProjectFilter}>
<Text style={styles.clearFilterText}>Clear</Text>
</Pressable>
@@ -821,14 +821,14 @@ export function SidebarAgentList({
anchorRef={projectFilterAnchorRef}
>
<View style={styles.filterOptionsList}>
{projectOptions.length === 0 ? (
{projectFilterOptions.length === 0 ? (
<Text style={styles.filterEmptyText}>No projects</Text>
) : (
projectOptions.map((option) => (
projectFilterOptions.map((option) => (
<ProjectFilterOptionRow
key={option.projectKey}
option={option}
selected={selectedProjectKeys.includes(option.projectKey)}
selected={selectedProjectFilterKeys.includes(option.projectKey)}
iconDataUri={projectIconByProjectKey.get(option.projectKey) ?? null}
displayName={deriveProjectDisplayName({
projectKey: option.projectKey,