Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions apps/mobile/app/project/[projectId]/[sessionId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -675,6 +676,39 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi

const handleScrollBeginDrag = useCallback(() => Keyboard.dismiss(), [])

const matchedPermissionIds = useMemo(() => {
const ids = new Set<string>()
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(
Expand Down Expand Up @@ -926,6 +960,19 @@ function SessionScreenInner({ projectId, sessionId }: { projectId: string; sessi
</View>
)}

{orphanPermissions.length > 0 && (
<View className="px-4 pb-2 gap-2">
{orphanPermissions.map((perm) => (
<PermissionBlock
key={perm.id}
request={perm}
theme={theme}
onReply={handlePermissionReply}
/>
))}
</View>
)}

<ChatInput
draft={draft}
setDraft={setDraft}
Expand Down
24 changes: 20 additions & 4 deletions apps/mobile/components/OpencodeStatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,27 @@ export function OpencodeStatsCard() {
className="bg-muted/20 rounded-lg p-3"
>
<View className="flex-row items-center justify-between mb-2">
<Text className="text-sm font-medium flex-1" numberOfLines={1}>
{project.projectName}
</Text>
<View className="flex-1 mr-2">
<Text className="text-sm font-medium" numberOfLines={1}>
{project.directory
? (project.directory.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? project.directory)
: project.projectName}
</Text>
{project.directory && (
<Text className="text-xs text-muted-foreground mt-0.5" numberOfLines={1}>
{project.directory}
</Text>
)}
</View>
<Pressable
onPress={() => 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"
>
<Trash2 size={14} color={THEME[theme].destructive} />
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/components/hooks/event-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/store/opencode-stats.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,6 +44,7 @@ type OpencodeStatsStore = {
incrementProjectStats: (
projectId: string,
projectName: string,
directory: string,
inputTokens: number,
outputTokens: number,
cost: number
Expand All @@ -58,7 +60,7 @@ export const useOpencodeStats = create<OpencodeStatsStore>()(
(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()
Expand All @@ -83,6 +85,7 @@ export const useOpencodeStats = create<OpencodeStatsStore>()(
const updated: ProjectStats = {
projectId,
projectName,
directory: directory || existing?.directory,
responseCount: (existing?.responseCount ?? 0) + 1,
totalInputTokens: (existing?.totalInputTokens ?? 0) + inputTokens,
totalOutputTokens: (existing?.totalOutputTokens ?? 0) + outputTokens,
Expand Down
Loading