Update files
This commit is contained in:
@@ -13,7 +13,7 @@ import { View, ActivityIndicator, Text } from "react-native";
|
|||||||
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { darkTheme } from "@/styles/theme";
|
import { darkTheme } from "@/styles/theme";
|
||||||
import { DaemonRegistryProvider, useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
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 { MultiDaemonSessionHost } from "@/components/multi-daemon-session-host";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState, useEffect, type ReactNode, useMemo, useRef } from "react";
|
import { useState, useEffect, type ReactNode, useMemo, useRef } from "react";
|
||||||
@@ -253,8 +253,53 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
function ProvidersWrapper({ children }: { children: ReactNode }) {
|
function ProvidersWrapper({ children }: { children: ReactNode }) {
|
||||||
const { settings, isLoading: settingsLoading } = useAppSettings();
|
const { settings, isLoading: settingsLoading } = useAppSettings();
|
||||||
const { daemons, isLoading: registryLoading, upsertDaemonFromOfferUrl } = useDaemonRegistry();
|
const { daemons, isLoading: registryLoading, upsertDaemonFromOfferUrl } = useDaemonRegistry();
|
||||||
|
const { connectionStates } = useDaemonConnections();
|
||||||
const isLoading = settingsLoading || registryLoading;
|
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
|
// Apply theme setting on mount and when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
@@ -270,6 +315,10 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
|
|||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connectingMessage && !connectionTimedOut) {
|
||||||
|
return <LoadingView message={connectingMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VoiceProvider>
|
<VoiceProvider>
|
||||||
<OfferLinkListener upsertDaemonFromOfferUrl={upsertDaemonFromOfferUrl} />
|
<OfferLinkListener upsertDaemonFromOfferUrl={upsertDaemonFromOfferUrl} />
|
||||||
@@ -339,7 +388,7 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingView() {
|
function LoadingView({ message }: { message?: string } = {}) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -350,6 +399,17 @@ function LoadingView() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActivityIndicator size="large" color={darkTheme.colors.foreground} />
|
<ActivityIndicator size="large" color={darkTheme.colors.foreground} />
|
||||||
|
{message ? (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: darkTheme.colors.foregroundMuted,
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type {
|
|||||||
AgentUsage,
|
AgentUsage,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
|
McpServerConfig,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
|
|
||||||
@@ -45,6 +46,58 @@ const DEFAULT_MODES: AgentMode[] = [
|
|||||||
|
|
||||||
type OpenCodeAgentConfig = AgentSessionConfig & { provider: "opencode" };
|
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> {
|
async function findAvailablePort(): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
@@ -354,6 +407,8 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
||||||
private abortController: AbortController | null = null;
|
private abortController: AbortController | null = null;
|
||||||
private accumulatedUsage: AgentUsage = {};
|
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 */
|
/** Tracks the role of each message by ID to distinguish user from assistant messages */
|
||||||
private messageRoles = new Map<string, "user" | "assistant">();
|
private messageRoles = new Map<string, "user" | "assistant">();
|
||||||
|
|
||||||
@@ -426,6 +481,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
_options?: AgentRunOptions
|
_options?: AgentRunOptions
|
||||||
): AsyncGenerator<AgentStreamEvent> {
|
): AsyncGenerator<AgentStreamEvent> {
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
|
await this.ensureMcpServersConfigured();
|
||||||
|
|
||||||
const parts = this.buildPromptParts(prompt);
|
const parts = this.buildPromptParts(prompt);
|
||||||
const model = this.parseModel(this.config.model);
|
const model = this.parseModel(this.config.model);
|
||||||
@@ -438,6 +494,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
sessionID: this.sessionId,
|
sessionID: this.sessionId,
|
||||||
directory: this.config.cwd,
|
directory: this.config.cwd,
|
||||||
parts,
|
parts,
|
||||||
|
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
|
||||||
...(model ? { model } : {}),
|
...(model ? { model } : {}),
|
||||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
||||||
});
|
});
|
||||||
@@ -648,6 +705,78 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
return { providerID: "opencode", modelID: model };
|
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[] {
|
private translateEvent(event: unknown): AgentStreamEvent[] {
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user