fix: request notification permission; resync session on focus; notif + reply bugs (#121)
Five correctness bugs from an adversarial review of the notification and permission/approval paths (each verified against the code): 1. Notifications never worked for most users (HIGH): OS permission was only requested when a user manually toggled a Settings switch off→on. Since categories default on, that path never fired, permission stayed 'undetermined', and send() silently no-op'd every notification. Now request it once on first live connection (in-context). (app/_layout.tsx) 2. Wrong-session data after back-navigation (HIGH): session screen reads a global store and its resync ran only on mount; the native stack keeps screens mounted underneath a pushed one, so returning to a session could show another session's messages and permission prompts — approving the wrong session's tool call. Re-select on focus via useFocusEffect. (app/session/[id].tsx) 3. 'Task completed' fired on aborted/errored runs (misleading, and a duplicate push alongside 'Session error'). Gate the notify by !aborted && !errored. (src/stores/events.ts) 4. Tapping a connection-drop notification (no sessionId) navigated to an empty '/session/' dead-end. Route to home instead. (app/_layout.tsx) 5. Double-tap on a single-select question sent two replies; the second hit an already-resolved request and popped a spurious 'Reply failed' alert. One-shot guard on reply/reject. (src/components/chat/QuestionPrompt.tsx) Verified but intentionally NOT changed: 'completed' notifications default off (a defensible anti-spam choice — the app still notifies when the agent needs input). typecheck clean, 187/187 tests. Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6 Co-authored-by: engineer <engineer@macbookpro.lan> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { useRef, useState } from "react"
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet } from "react-native"
|
||||
import { Ionicons } from "@expo/vector-icons"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -33,6 +33,22 @@ export function QuestionPrompt({ request, isDark, onReply, onReject }: Props) {
|
||||
const [showCustom, setShowCustom] = useState(false)
|
||||
const [current, setCurrent] = useState(0)
|
||||
|
||||
// A question is answered exactly once. Without this guard, a double-tap on a
|
||||
// single-select option schedules two `onReply` timers; the second reply hits
|
||||
// an already-resolved request server-side and surfaces a spurious
|
||||
// "Reply failed" alert even though the answer went through.
|
||||
const replied = useRef(false)
|
||||
const reply = (a: string[][]) => {
|
||||
if (replied.current) return
|
||||
replied.current = true
|
||||
onReply(a)
|
||||
}
|
||||
const reject = () => {
|
||||
if (replied.current) return
|
||||
replied.current = true
|
||||
onReject()
|
||||
}
|
||||
|
||||
const q = request.questions[current]
|
||||
if (!q) return null
|
||||
|
||||
@@ -45,7 +61,7 @@ export function QuestionPrompt({ request, isDark, onReply, onReject }: Props) {
|
||||
} else {
|
||||
copy[current] = [label]
|
||||
if (request.questions.length === 1) {
|
||||
setTimeout(() => onReply(copy), 100)
|
||||
setTimeout(() => reply(copy), 100)
|
||||
}
|
||||
}
|
||||
return copy
|
||||
@@ -60,7 +76,7 @@ export function QuestionPrompt({ request, isDark, onReply, onReject }: Props) {
|
||||
setCustom("")
|
||||
setShowCustom(false)
|
||||
if (request.questions.length === 1) {
|
||||
onReply(copy)
|
||||
reply(copy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +132,7 @@ export function QuestionPrompt({ request, isDark, onReply, onReject }: Props) {
|
||||
</View>
|
||||
|
||||
<View style={s.footer}>
|
||||
<TouchableOpacity onPress={onReject}>
|
||||
<TouchableOpacity onPress={reject}>
|
||||
<Text style={[s.dismiss, isDark && s.metaDark]}>{t("chat.questionPrompt.dismiss")}</Text>
|
||||
</TouchableOpacity>
|
||||
{(request.questions.length > 1 || q.multiple) && (
|
||||
@@ -126,7 +142,7 @@ export function QuestionPrompt({ request, isDark, onReply, onReject }: Props) {
|
||||
if (current < request.questions.length - 1) {
|
||||
setCurrent(current + 1)
|
||||
} else {
|
||||
onReply(answers)
|
||||
reply(answers)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user