From d3d60f55ab849a108dfb8905d9f6a18454ddf5f7 Mon Sep 17 00:00:00 2001 From: Dzianis Vauchok <2119348+dzianisv@users.noreply.github.com> Date: Tue, 26 May 2026 07:38:32 +0000 Subject: [PATCH 1/3] fix: reload sessions on tab focus instead of only on client change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sessions screen was using useEffect with [client] to load sessions, which only fired when the client object reference changed. If the app was backgrounded and later foregrounded (common scenario on mobile), the session list was never refreshed, showing stale data. Replaced with useFocusEffect from expo-router so sessions are reloaded every time the sessions tab is focused — including: - Initial mount - Coming back from background - Switching tabs and returning --- app/(tabs)/index.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index eefa6e1..3fd5cf2 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useState, useRef } from "react" +import { useCallback, useState, useRef } from "react" import { View, Text, @@ -14,7 +14,7 @@ import { KeyboardAvoidingView, Platform, } from "react-native" -import { router } from "expo-router" +import { router, useFocusEffect } from "expo-router" import { Ionicons } from "@expo/vector-icons" import { useSessions } from "../../src/stores/sessions" import { useConnections } from "../../src/stores/connections" @@ -135,12 +135,14 @@ export default function SessionsScreen() { [switchDirectory, loadSessions, refreshProject], ) - useEffect(() => { - if (client) { - loadSessions() - refreshProject() - } - }, [client]) + useFocusEffect( + useCallback(() => { + if (client) { + loadSessions() + refreshProject() + } + }, [client, loadSessions, refreshProject]), + ) const onRefresh = useCallback(async () => { setRefreshing(true) From ce598e2a52b6787a711c4a1e165805127f2a8ff5 Mon Sep 17 00:00:00 2001 From: Dzianis Vauchok <2119348+dzianisv@users.noreply.github.com> Date: Tue, 26 May 2026 07:53:13 +0000 Subject: [PATCH 2/3] fix: use server home path for session listing to show all projects When no project directory is explicitly selected, the session list was filtered to the server's CWD, hiding sessions from other projects. Fix by using the server's home path (fetched from /path) as the x-opencode-directory header when listing sessions without an explicit project directory. This ensures recent sessions across all projects appear in the list. --- src/stores/sessions.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index 7db656b..cf6e162 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -73,15 +73,22 @@ export const useSessions = create((set, get) => ({ error: null, loadSessions: async () => { - const client = useConnections.getState().client - if (!client) { + const connState = useConnections.getState() + if (!connState.client) { set({ error: "No active connection" }) return } + // When no project directory is explicitly selected, use the server home path so the + // session list shows all recent sessions across all projects — not just the server's CWD. + const listClient = + !connState.activeConnection?.directory && connState.serverHome + ? connState.clientForDirectory(connState.serverHome) + : connState.client + try { set({ isLoading: true, error: null }) - const sessions = await client.session.list({ roots: true, limit: 50 }) + const sessions = await (listClient || connState.client).session.list({ roots: true, limit: 50 }) set({ sessions, isLoading: false }) } catch (error) { set({ error: "Failed to load sessions", isLoading: false }) From 0ef3dd37b65087d683e398a7f257bf9baeb39aa0 Mon Sep 17 00:00:00 2001 From: Dzianis Vauchok <2119348+dzianisv@users.noreply.github.com> Date: Tue, 26 May 2026 08:22:56 +0000 Subject: [PATCH 3/3] fix: use directory-aware client for delete, rename, command, permissions, questions - sessions.ts: deleteSession looks up session.directory and uses clientFor() instead of bare client, so cross-project deletes hit the right server path - index.tsx: submitRename uses clientForDirectory(session.directory) so renames work for sessions not owned by the active project directory - index.tsx: handleSwitchDirectory calls catalog.load() after a directory switch so agents/commands/providers refresh for the new project - session/[id].tsx: introduce sessionClient (directory-aware) and use it in handlePermissionReply, handleQuestionReply, handleQuestionReject, slash-command send, and the refreshPending bootstrap call; also add 'directory' to useEffect deps - catalog.ts: remove debug console.log --- app/(tabs)/index.tsx | 13 +++++++++---- app/session/[id].tsx | 30 +++++++++++++++++++----------- src/stores/catalog.ts | 10 ---------- src/stores/sessions.ts | 3 ++- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 3fd5cf2..91e8e0f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -18,6 +18,7 @@ import { router, useFocusEffect } from "expo-router" import { Ionicons } from "@expo/vector-icons" import { useSessions } from "../../src/stores/sessions" import { useConnections } from "../../src/stores/connections" +import { useCatalog } from "../../src/stores/catalog" import type BottomSheet from "@gorhom/bottom-sheet" import type { Session } from "../../src/lib/sdk" import { DirectorySwitcher } from "../../src/components/chat" @@ -123,6 +124,7 @@ export default function SessionsScreen() { addRecentDirectory, recentDirectories, } = useConnections() + const loadCatalog = useCatalog((s) => s.load) const dirSheetRef = useRef(null) const [refreshing, setRefreshing] = useState(false) @@ -131,8 +133,9 @@ export default function SessionsScreen() { await switchDirectory(dir) loadSessions() refreshProject() + loadCatalog() }, - [switchDirectory, loadSessions, refreshProject], + [switchDirectory, loadSessions, refreshProject, loadCatalog], ) useFocusEffect( @@ -157,12 +160,14 @@ export default function SessionsScreen() { const submitRename = useCallback(async () => { const title = renameText.trim() - if (!title || !renaming || !client) return - await client.session.update(renaming.id, { title }) + if (!title || !renaming) return + const renameClient = renaming.directory ? (clientForDirectory(renaming.directory) ?? client) : client + if (!renameClient) return + await renameClient.session.update(renaming.id, { title }) setRenaming(null) setRenameText("") loadSessions() - }, [renaming, renameText, client, loadSessions]) + }, [renaming, renameText, client, clientForDirectory, loadSessions]) const handleDelete = useCallback( (session: Session) => { diff --git a/app/session/[id].tsx b/app/session/[id].tsx index 118c388..9fde691 100644 --- a/app/session/[id].tsx +++ b/app/session/[id].tsx @@ -107,7 +107,13 @@ export default function SessionScreen() { const isSending = useSessions((s) => !!(currentSession && s.sending[currentSession.id])) const { authenticateForMessage } = useAuth() - const { client } = useConnections() + const { client, clientForDirectory } = useConnections() + + // Use directory-aware client for sessions that belong to a project other than the active one + const sessionClient = useMemo( + () => (currentSession?.directory ? (clientForDirectory(currentSession.directory) ?? client) : client), + [currentSession?.directory, clientForDirectory, client], + ) // Catalog const catalog = useCatalog() @@ -170,9 +176,11 @@ export default function SessionScreen() { selectSession(id, directory).then(() => { // Re-fetch pending permissions/questions from the server to recover from // missed SSE events or failed optimistic removals - if (client) refreshPending(client, id) + const connState = useConnections.getState() + const c = directory ? (connState.clientForDirectory(directory) ?? connState.client) : connState.client + if (c) refreshPending(c, id) }) - }, [id]) + }, [id, directory]) // Sync model chip from latest assistant message useEffect(() => { @@ -320,8 +328,8 @@ export default function SessionScreen() { const [cmdName, ...args] = text.split(" ") const name = cmdName.slice(1) const match = serverCommands.find((c) => c.name === name) - if (match && client && currentSession) { - client.session + if (match && sessionClient && currentSession) { + sessionClient.session .command(currentSession.id, { command: name, arguments: args.join(" "), @@ -359,7 +367,7 @@ export default function SessionScreen() { }, [loadingMore]) const handlePermissionReply = async (requestID: string, reply: "once" | "always" | "reject") => { - if (!client || !sessionID) return + if (!sessionClient || !sessionID) return // Snapshot for rollback const snapshot = useEvents.getState().permissions[sessionID] || [] // Optimistically remove from UI @@ -370,7 +378,7 @@ export default function SessionScreen() { }, })) try { - await client.permission.reply(requestID, reply) + await sessionClient.permission.reply(requestID, reply) } catch (err) { console.error("Permission reply failed:", err) // Restore the prompt so the user can retry @@ -382,7 +390,7 @@ export default function SessionScreen() { } const handleQuestionReply = async (requestID: string, answers: string[][]) => { - if (!client || !sessionID) return + if (!sessionClient || !sessionID) return const snapshot = useEvents.getState().questions[sessionID] || [] useEvents.setState((state) => ({ questions: { @@ -391,7 +399,7 @@ export default function SessionScreen() { }, })) try { - await client.question.reply(requestID, answers) + await sessionClient.question.reply(requestID, answers) } catch (err) { console.error("Question reply failed:", err) useEvents.setState((state) => ({ @@ -402,7 +410,7 @@ export default function SessionScreen() { } const handleQuestionReject = async (requestID: string) => { - if (!client || !sessionID) return + if (!sessionClient || !sessionID) return const snapshot = useEvents.getState().questions[sessionID] || [] useEvents.setState((state) => ({ questions: { @@ -411,7 +419,7 @@ export default function SessionScreen() { }, })) try { - await client.question.reject(requestID) + await sessionClient.question.reject(requestID) } catch (err) { console.error("Question reject failed:", err) useEvents.setState((state) => ({ diff --git a/src/stores/catalog.ts b/src/stores/catalog.ts index e926d64..aaf5594 100644 --- a/src/stores/catalog.ts +++ b/src/stores/catalog.ts @@ -85,16 +85,6 @@ export const useCatalog = create((set, get) => ({ .filter((p) => p.models.length > 0) : [] - console.log( - "[catalog] loaded:", - agents.length, - "agents,", - commands.length, - "commands,", - providers.length, - "providers (" + providers.reduce((n, p) => n + p.models.length, 0) + " models)", - ) - // Filter out hidden agents const visible = agents.filter((a) => !a.hidden) diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index cf6e162..b324d3d 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -193,7 +193,8 @@ export const useSessions = create((set, get) => ({ }, deleteSession: async (sessionID) => { - const client = useConnections.getState().client + const session = get().sessions.find((s) => s.id === sessionID) + const client = clientFor(session?.directory) if (!client) { set({ error: "No active connection" }) return