diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index 870646c84..f38976848 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -1,10 +1,8 @@ -import { View, Text, Pressable, SectionList, Modal, RefreshControl, type SectionListRenderItem } from "react-native"; +import { View, Text, Pressable, FlatList, Modal, RefreshControl, type ListRenderItem } from "react-native"; import { useCallback, useState, useMemo } from "react"; import { router } from "expo-router"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import { Check } from "lucide-react-native"; import { formatTimeAgo } from "@/utils/time"; -import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status"; import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest"; import { type AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { useSessionStore } from "@/stores/session-store"; @@ -16,37 +14,18 @@ interface AgentListProps { onRefresh?: () => void; } -interface AgentSection { - title: string; - data: AggregatedAgent[]; -} - export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) { const { theme } = useUnistyles(); const [actionAgent, setActionAgent] = useState(null); - // Group agents by attention status - const sections = useMemo(() => { - const requiresAttention: AggregatedAgent[] = []; - const normal: AggregatedAgent[] = []; - - for (const agent of agents) { - if (agent.requiresAttention) { - requiresAttention.push(agent); - } else { - normal.push(agent); - } - } - - const result: AgentSection[] = []; - if (requiresAttention.length > 0) { - result.push({ title: "Requires Attention", data: requiresAttention }); - } - if (normal.length > 0) { - result.push({ title: "All Agents", data: normal }); - } - - return result; + // Sort agents with requires attention at the top + const sortedAgents = useMemo(() => { + return [...agents].sort((a, b) => { + // Requires attention first + if (a.requiresAttention && !b.requiresAttention) return -1; + if (!a.requiresAttention && b.requiresAttention) return 1; + return 0; + }); }, [agents]); // Get the methods for the specific server @@ -103,30 +82,11 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList setActionAgent(null); }, [actionAgent, deleteAgent]); - const handleClearAllAttention = useCallback((agentsInSection: AggregatedAgent[]) => { - // Group agents by serverId for batch clearing - const agentsByServer = new Map(); - for (const agent of agentsInSection) { - const agentIds = agentsByServer.get(agent.serverId) || []; - agentIds.push(agent.id); - agentsByServer.set(agent.serverId, agentIds); - } - - // Send one batch request per server - for (const [serverId, agentIds] of agentsByServer) { - const session = useSessionStore.getState().sessions[serverId]; - if (session?.ws) { - session.ws.clearAgentAttention(agentIds); - } - } - }, []); - - const renderAgentItem = useCallback>( + const renderAgentItem = useCallback>( ({ item: agent }) => { - const statusColor = getAgentStatusColor(agent.status); - const statusLabel = getAgentStatusLabel(agent.status); const timeAgo = formatTimeAgo(agent.lastActivityAt); const providerLabel = getAgentProviderDefinition(agent.provider).label; + const isRunning = agent.status === "running"; return ( - - - - {statusLabel} - - + {agent.requiresAttention && ( + + + Needs attention + + + )} + + {isRunning && ( + + + + Running + + + )} @@ -190,7 +158,7 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList ); }, - [handleAgentLongPress, handleAgentPress, theme.colors.muted, theme.colors.mutedForeground] + [handleAgentLongPress, handleAgentPress, theme.colors.muted, theme.colors.mutedForeground, theme.colors.primary, theme.colors.primaryForeground] ); const keyExtractor = useCallback( @@ -198,38 +166,16 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList [] ); - const renderSectionHeader = useCallback( - ({ section }: { section: AgentSection }) => ( - - {section.title} - {section.title === "Requires Attention" && ( - [ - styles.clearAllButton, - pressed && styles.clearAllButtonPressed, - ]} - onPress={() => handleClearAllAttention(section.data)} - > - - - )} - - ), - [handleClearAllAttention, theme.colors.mutedForeground] - ); - return ( <> - ({ paddingTop: theme.spacing[4], paddingBottom: theme.spacing[4], }, - sectionHeader: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingTop: theme.spacing[2], - paddingBottom: theme.spacing[2], - paddingHorizontal: theme.spacing[2], - }, - sectionTitle: { - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.semibold, - color: theme.colors.mutedForeground, - textTransform: "uppercase", - }, - clearAllButton: { - padding: theme.spacing[1], - borderRadius: theme.borderRadius.md, - }, - clearAllButtonPressed: { - opacity: 0.5, - backgroundColor: theme.colors.muted, - }, agentItem: { paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], @@ -392,6 +316,15 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, + attentionBadge: { + borderRadius: theme.borderRadius.full, + paddingHorizontal: theme.spacing[2], + paddingVertical: 2, + }, + attentionText: { + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.semibold, + }, timeAgo: { fontSize: theme.fontSize.xs, color: theme.colors.mutedForeground, diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index 19d799628..94769dcc6 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -470,6 +470,7 @@ function AgentFlowModal({ mode: false, model: false, workingDir: false, + serverId: false, }); const prevVisibilityRef = useRef(isVisible); const dictationRequestIdRef = useRef(null); @@ -730,6 +731,7 @@ function AgentFlowModal({ provider?: AgentProvider; mode?: string; model?: string; + serverId?: string; }; if ( parsed.provider && @@ -756,6 +758,12 @@ function AgentFlowModal({ ) { setSelectedModel(parsed.model); } + if ( + typeof parsed.serverId === "string" && + !userEditedPreferencesRef.current.serverId + ) { + setSelectedServerId(parsed.serverId); + } } catch (error) { console.error( "[CreateAgentModal] Failed to hydrate form preferences:", @@ -786,6 +794,7 @@ function AgentFlowModal({ provider: selectedProvider, mode: selectedMode, model: selectedModel, + serverId: selectedServerId, }) ); } catch (error) { @@ -796,7 +805,7 @@ function AgentFlowModal({ } }; void persist(); - }, [selectedMode, selectedProvider, workingDir]); + }, [selectedMode, selectedProvider, workingDir, selectedModel, selectedServerId]); const providerFilterOptions = useMemo( () => [ @@ -1250,6 +1259,7 @@ function AgentFlowModal({ }, [router, selectedDaemonId]); const handleSelectServer = useCallback((serverId: string) => { + userEditedPreferencesRef.current.serverId = true; setSelectedServerId(serverId); }, []); diff --git a/packages/server/src/server/agent/providers/codex-agent.ts b/packages/server/src/server/agent/providers/codex-agent.ts index d2dcfcd92..2b914d5d3 100644 --- a/packages/server/src/server/agent/providers/codex-agent.ts +++ b/packages/server/src/server/agent/providers/codex-agent.ts @@ -277,9 +277,29 @@ function resolveAgentControlMcpUrl(config: CodexAgentConfig): string | null { : baseUrl; } +function resolveAgentControlBearerToken( + config: CodexAgentConfig +): string | null { + const header = config.agentControlMcp?.headers?.Authorization; + if (!header || typeof header !== "string") { + return null; + } + const trimmed = header.trim(); + if (trimmed.toLowerCase().startsWith("bearer ")) { + const token = trimmed.slice("bearer ".length).trim(); + return token.length > 0 ? token : null; + } + if (trimmed.toLowerCase().startsWith("basic ")) { + const token = trimmed.slice("basic ".length).trim(); + return token.length > 0 ? token : null; + } + return null; +} + async function createCodexWrapperScript( realCodexPath: string, mcpUrl: string, + mcpBearerToken: string | null, developerInstructions: string ): Promise { const templatePath = await resolveCodexWrapperTemplatePath(); @@ -289,6 +309,10 @@ async function createCodexWrapperScript( const content = template .replace("__REAL_CODEX__", escapeBashValue(realCodexPath)) .replace("__MCP_URL__", escapeBashValue(mcpUrl)) + .replace( + "__MCP_BEARER__", + escapeBashValue(mcpBearerToken ?? "") + ) .replace( "__DEV_INSTRUCTIONS__", escapeBashTomlValue(encodeTomlBasicString(developerInstructions)) @@ -459,6 +483,7 @@ export class CodexAgentClient implements AgentClient { config: CodexAgentConfig ): Promise { const mcpUrl = resolveAgentControlMcpUrl(config); + const mcpBearerToken = resolveAgentControlBearerToken(config); const developerInstructions = resolveDeveloperInstructions(config); if ((!mcpUrl && !developerInstructions) || !this.baseCodexPath) { if ((mcpUrl || developerInstructions) && !this.baseCodexPath) { @@ -470,6 +495,7 @@ export class CodexAgentClient implements AgentClient { const wrapper = await createCodexWrapperScript( this.baseCodexPath, mcpUrl ?? "", + mcpBearerToken, developerInstructions ?? "" ); const env = { diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index a7759811b..dfb65c2fd 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -28,17 +28,34 @@ const BASIC_AUTH_USERS = { mo: "bo" } as const; function createServer() { const app = express(); + const [agentMcpUser, agentMcpPassword] = + Object.entries(BASIC_AUTH_USERS)[0] ?? []; + const agentMcpBearerToken = + agentMcpUser && agentMcpPassword + ? Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64") + : undefined; + // Serve static files from public directory (no auth required for APK downloads) app.use("/public", express.static("public")); // Basic authentication (skip for /public routes) - app.use( - basicAuth({ - users: BASIC_AUTH_USERS, - challenge: true, - realm: "Voice Assistant", - }) - ); + const basicAuthMiddleware = basicAuth({ + users: BASIC_AUTH_USERS, + challenge: true, + realm: "Voice Assistant", + }); + app.use((req, res, next) => { + if (agentMcpBearerToken && req.path.startsWith("/mcp/agents")) { + const authHeader = req.header("authorization") ?? ""; + if (authHeader.startsWith("Bearer ")) { + const token = authHeader.slice("Bearer ".length).trim(); + if (token === agentMcpBearerToken) { + return next(); + } + } + } + return basicAuthMiddleware(req, res, next); + }); // Middleware app.use(express.json()); @@ -155,6 +172,15 @@ async function main() { req, res ) => { + if (process.env.MCP_DEBUG === "1") { + console.log("[Agent MCP] request", { + method: req.method, + url: req.originalUrl, + sessionId: req.header("mcp-session-id"), + authorization: req.header("authorization"), + body: req.body, + }); + } try { const sessionId = req.header("mcp-session-id"); let transport = sessionId