feat: implement daemon connectivity updates and relay protocol versioning
This commit is contained in:
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user