Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 19:09:25 +07:00
parent 05c150dab8
commit 598a1823ae
2 changed files with 191 additions and 2 deletions

View File

@@ -13,7 +13,7 @@ import { View, ActivityIndicator, Text } from "react-native";
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { darkTheme } from "@/styles/theme";
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { DaemonConnectionsProvider } from "@/contexts/daemon-connections-context";
import { DaemonConnectionsProvider, useDaemonConnections } from "@/contexts/daemon-connections-context";
import { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
import { QueryClientProvider } from "@tanstack/react-query";
import { useState, useEffect, type ReactNode, useMemo, useRef } from "react";
@@ -253,8 +253,53 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
function ProvidersWrapper({ children }: { children: ReactNode }) {
const { settings, isLoading: settingsLoading } = useAppSettings();
const { daemons, isLoading: registryLoading, upsertDaemonFromOfferUrl } = useDaemonRegistry();
const { connectionStates } = useDaemonConnections();
const isLoading = settingsLoading || registryLoading;
const isInitialConnectionPending = useMemo(() => {
if (daemons.length === 0) return false;
return daemons.some((daemon) => {
const record = connectionStates.get(daemon.serverId);
if (!record) return true;
if (record.hasEverReceivedAgentList) return false;
return (
record.status === "idle" ||
record.status === "connecting" ||
(record.status === "online" && !record.agentListReady)
);
});
}, [daemons, connectionStates]);
const [connectionTimedOut, setConnectionTimedOut] = useState(false);
useEffect(() => {
if (!isInitialConnectionPending) {
setConnectionTimedOut(false);
return;
}
const timer = setTimeout(() => setConnectionTimedOut(true), 5000);
return () => clearTimeout(timer);
}, [isInitialConnectionPending]);
const connectingMessage = useMemo(() => {
if (!isInitialConnectionPending) return null;
const pending = daemons.filter((daemon) => {
const record = connectionStates.get(daemon.serverId);
if (!record) return true;
if (record.hasEverReceivedAgentList) return false;
return (
record.status === "idle" ||
record.status === "connecting" ||
(record.status === "online" && !record.agentListReady)
);
});
if (pending.length === 1) {
const label = pending[0].label?.trim();
return label ? `Connecting to ${label}...` : "Connecting...";
}
return "Connecting...";
}, [isInitialConnectionPending, daemons, connectionStates]);
// Apply theme setting on mount and when it changes
useEffect(() => {
if (isLoading) return;
@@ -270,6 +315,10 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
return <LoadingView />;
}
if (connectingMessage && !connectionTimedOut) {
return <LoadingView message={connectingMessage} />;
}
return (
<VoiceProvider>
<OfferLinkListener upsertDaemonFromOfferUrl={upsertDaemonFromOfferUrl} />
@@ -339,7 +388,7 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
);
}
function LoadingView() {
function LoadingView({ message }: { message?: string } = {}) {
return (
<View
style={{
@@ -350,6 +399,17 @@ function LoadingView() {
}}
>
<ActivityIndicator size="large" color={darkTheme.colors.foreground} />
{message ? (
<Text
style={{
color: darkTheme.colors.foregroundMuted,
marginTop: 16,
fontSize: 14,
}}
>
{message}
</Text>
) : null}
</View>
);
}

View File

@@ -23,6 +23,7 @@ import type {
AgentUsage,
ListModelsOptions,
ListPersistedAgentsOptions,
McpServerConfig,
PersistedAgentDescriptor,
} from "../agent-sdk-types.js";
@@ -45,6 +46,58 @@ const DEFAULT_MODES: AgentMode[] = [
type OpenCodeAgentConfig = AgentSessionConfig & { provider: "opencode" };
type OpenCodeMcpConfig =
| {
type: "local";
command: string[];
environment?: Record<string, string>;
enabled?: boolean;
}
| {
type: "remote";
url: string;
headers?: Record<string, string>;
enabled?: boolean;
};
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const;
function toOpenCodeMcpConfig(config: McpServerConfig): OpenCodeMcpConfig {
if (config.type === "stdio") {
return {
type: "local",
command: [config.command, ...(config.args ?? [])],
...(config.env ? { environment: config.env } : {}),
enabled: true,
};
}
return {
type: "remote",
url: config.url,
...(config.headers ? { headers: config.headers } : {}),
enabled: true,
};
}
function stringifyUnknownError(error: unknown): string {
if (typeof error === "string") {
return error;
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
function isAlreadyPresentMcpError(error: unknown): boolean {
const normalized = stringifyUnknownError(error).toLowerCase();
return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) =>
normalized.includes(token)
);
}
async function findAvailablePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
@@ -354,6 +407,8 @@ class OpenCodeAgentSession implements AgentSession {
private pendingPermissions = new Map<string, AgentPermissionRequest>();
private abortController: AbortController | null = null;
private accumulatedUsage: AgentUsage = {};
private mcpConfigured = false;
private mcpSetupPromise: Promise<void> | null = null;
/** Tracks the role of each message by ID to distinguish user from assistant messages */
private messageRoles = new Map<string, "user" | "assistant">();
@@ -426,6 +481,7 @@ class OpenCodeAgentSession implements AgentSession {
_options?: AgentRunOptions
): AsyncGenerator<AgentStreamEvent> {
this.abortController = new AbortController();
await this.ensureMcpServersConfigured();
const parts = this.buildPromptParts(prompt);
const model = this.parseModel(this.config.model);
@@ -438,6 +494,7 @@ class OpenCodeAgentSession implements AgentSession {
sessionID: this.sessionId,
directory: this.config.cwd,
parts,
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
...(model ? { model } : {}),
...(effectiveVariant ? { variant: effectiveVariant } : {}),
});
@@ -648,6 +705,78 @@ class OpenCodeAgentSession implements AgentSession {
return { providerID: "opencode", modelID: model };
}
private async ensureMcpServersConfigured(): Promise<void> {
if (this.mcpConfigured) {
return;
}
const mcpServers = this.config.mcpServers;
if (!mcpServers || Object.keys(mcpServers).length === 0) {
this.mcpConfigured = true;
return;
}
if (!this.mcpSetupPromise) {
this.mcpSetupPromise = this.configureMcpServers(mcpServers);
}
try {
await this.mcpSetupPromise;
this.mcpConfigured = true;
} catch (error) {
this.mcpSetupPromise = null;
throw error;
}
}
private async configureMcpServers(
mcpServers: Record<string, McpServerConfig>
): Promise<void> {
for (const [name, serverConfig] of Object.entries(mcpServers)) {
const mappedConfig = toOpenCodeMcpConfig(serverConfig);
await this.registerMcpServer(name, mappedConfig);
}
}
private async registerMcpServer(
name: string,
config: OpenCodeMcpConfig
): Promise<void> {
await this.runMcpOperation("add", name, () =>
this.client.mcp.add({
directory: this.config.cwd,
name,
config,
})
);
await this.runMcpOperation("connect", name, () =>
this.client.mcp.connect({
directory: this.config.cwd,
name,
})
);
}
private async runMcpOperation(
operation: "add" | "connect",
name: string,
run: () => Promise<{ error?: unknown }>
): Promise<void> {
const response = await run();
const error = response.error;
if (!error) {
return;
}
if (isAlreadyPresentMcpError(error)) {
return;
}
throw new Error(
`Failed to ${operation} OpenCode MCP server '${name}': ${stringifyUnknownError(error)}`
);
}
private translateEvent(event: unknown): AgentStreamEvent[] {
const events: AgentStreamEvent[] = [];