mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user