From 71184221708fd5b703106a1214e002f30a46eeb6 Mon Sep 17 00:00:00 2001 From: snehasishcodes Date: Sat, 29 Aug 2026 11:47:09 +0530 Subject: [PATCH] feat(mobile): handle agent permission requests in chat Surface permission requests (e.g. external directory access) that are not tied to a message tool part as a dedicated panel above the chat input, so users can accept/deny and unblock the stuck agent response. Also includes project directory display tweaks in the stats card. Co-authored-by: muse-spark-1.2 --- .../project/[projectId]/[sessionId]/index.tsx | 47 +++++++++++++++++++ apps/mobile/components/OpencodeStatsCard.tsx | 24 ++++++++-- apps/mobile/components/hooks/event-stream.ts | 9 +++- apps/mobile/store/opencode-stats.store.ts | 5 +- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx b/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx index fd03426..d779c88 100644 --- a/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx +++ b/apps/mobile/app/project/[projectId]/[sessionId]/index.tsx @@ -33,6 +33,7 @@ import { QuestionRequest } from "@/store/questions.store" import { usePermissions } from "@/store/permissions.store" import { getPendingPermissions, replyToPermission } from "@/lib/permissions" import { PermissionRequest } from "@/store/permissions.store" +import { PermissionBlock } from "@/components/permission-block" import { getAuthHeader } from "@/lib/utils" import { notifyAgentStatus, setActiveChatScreen, clearActiveChatScreen } from "@/lib/notifications" @@ -675,6 +676,39 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi const handleScrollBeginDrag = useCallback(() => Keyboard.dismiss(), []) + const matchedPermissionIds = useMemo(() => { + const ids = new Set() + for (const msg of rawMessages) { + for (const part of msg.parts ?? []) { + if (part.type === "tool-invocation") { + for (const p of pendingPermissions) { + if ( + p.tool?.messageID === msg.id && + p.tool?.callID === part.toolInvocation.toolCallId + ) { + ids.add(p.id) + } + } + } else if (part.type === "tool") { + for (const p of pendingPermissions) { + if (p.tool?.messageID === msg.id && p.tool?.callID === part.callID) { + ids.add(p.id) + } + } + } + } + } + return ids + }, [rawMessages, pendingPermissions]) + + // Permissions that are not tied to a specific message tool part (e.g. external + // directory access requests) cannot be rendered inline, so surface them as a + // dedicated panel so the user can accept/deny and unblock the agent. + const orphanPermissions = useMemo( + () => pendingPermissions.filter((p) => !matchedPermissionIds.has(p.id)), + [pendingPermissions, matchedPermissionIds] + ) + const renderItem = useCallback( ({ item }: { item: Message }) => { const hasQuestionTool = item.parts?.some( @@ -926,6 +960,19 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi )} + {orphanPermissions.length > 0 && ( + + {orphanPermissions.map((perm) => ( + + ))} + + )} + - - {project.projectName} - + + + {project.directory + ? (project.directory.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? project.directory) + : project.projectName} + + {project.directory && ( + + {project.directory} + + )} + handleResetProject(project.projectId, project.projectName)} + onPress={() => + handleResetProject( + project.projectId, + project.directory + ? (project.directory.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? project.directory) + : project.projectName + ) + } className="p-1" > diff --git a/apps/mobile/components/hooks/event-stream.ts b/apps/mobile/components/hooks/event-stream.ts index 0c18b0c..3b5a277 100644 --- a/apps/mobile/components/hooks/event-stream.ts +++ b/apps/mobile/components/hooks/event-stream.ts @@ -240,12 +240,17 @@ export function useEventStream(url?: string, sessionId?: string, token?: string, if (!info.error && projectId && !countedMessageIds.has(info.id)) { countedMessageIds.add(info.id) const assistant = info as AssistantMessage + const project = + useProjects.getState().projects.find((p) => p.id === projectId) + const directory = project?.directory ?? "" const projectName = - useProjects.getState().projects.find((p) => p.id === projectId)?.name ?? - "Unknown project" + directory + ? (directory.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? directory) + : (project?.name ?? "Unknown project") useOpencodeStats.getState().incrementProjectStats( projectId, projectName, + directory, assistant.tokens?.input ?? 0, assistant.tokens?.output ?? 0, assistant.cost ?? 0 diff --git a/apps/mobile/store/opencode-stats.store.ts b/apps/mobile/store/opencode-stats.store.ts index e3401e4..5891005 100644 --- a/apps/mobile/store/opencode-stats.store.ts +++ b/apps/mobile/store/opencode-stats.store.ts @@ -14,6 +14,7 @@ export const DAILY_HISTORY_LIMIT = 30 export interface ProjectStats { projectId: string projectName: string + directory?: string responseCount: number totalInputTokens: number totalOutputTokens: number @@ -43,6 +44,7 @@ type OpencodeStatsStore = { incrementProjectStats: ( projectId: string, projectName: string, + directory: string, inputTokens: number, outputTokens: number, cost: number @@ -58,7 +60,7 @@ export const useOpencodeStats = create()( (set, get) => ({ projects: {}, - incrementProjectStats: (projectId, projectName, inputTokens, outputTokens, cost) => { + incrementProjectStats: (projectId, projectName, directory, inputTokens, outputTokens, cost) => { set((state) => { const existing = state.projects[projectId] const now = new Date().toISOString() @@ -83,6 +85,7 @@ export const useOpencodeStats = create()( const updated: ProjectStats = { projectId, projectName, + directory: directory || existing?.directory, responseCount: (existing?.responseCount ?? 0) + 1, totalInputTokens: (existing?.totalInputTokens ?? 0) + inputTokens, totalOutputTokens: (existing?.totalOutputTokens ?? 0) + outputTokens,