Skip to content
Open
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
25 changes: 20 additions & 5 deletions app/session/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export default function SessionScreen() {
const flatListRef = useRef<FlatList>(null)
const modelSheetRef = useRef<BottomSheet>(null)
const variantSheetRef = useRef<BottomSheet>(null)
const [input, setInput] = useState("")
const [input, setInputState] = useState(() => (id ? useSessions.getState().drafts[id] || "" : ""))
const inputRef = useRef(input)
inputRef.current = input
const [attachments, setAttachments] = useState<Attachment[]>([])
const [showInfo, setShowInfo] = useState(false)

Expand All @@ -94,13 +96,25 @@ export default function SessionScreen() {
loadingMore,
hasMore,
selectSession,
setDraft,
clearDraft,
sendMessage,
abortSession,
loadOlderMessages,
revertToMessage,
unrevertSession,
} = useSessions()

const setInput = useCallback(
(value: string | ((current: string) => string)) => {
const next = typeof value === "function" ? value(inputRef.current) : value
inputRef.current = next
setInputState(next)
if (id) setDraft(id, next)
},
[id, setDraft],
)

// Derive sending state for this specific session
const isSending = useSessions((s) => !!(currentSession && s.sending[currentSession.id]))

Expand Down Expand Up @@ -194,9 +208,6 @@ export default function SessionScreen() {
// handleMessageLongPress's deps — kept as a plain ref assignment (not
// state) so the callback below stays referentially stable across
// keystrokes for MessageBubble's custom memo comparator.
const inputRef = useRef(input)
inputRef.current = input

const applyRevertResult = useCallback((result: Awaited<ReturnType<typeof revertToMessage>>) => {
if (!result.ok) {
if (result.reason === "unsupported") {
Expand Down Expand Up @@ -265,14 +276,17 @@ export default function SessionScreen() {
useFocusEffect(
useCallback(() => {
if (!id) return
const draft = useSessions.getState().drafts[id] || ""
inputRef.current = draft
setInputState(draft)
selectSession(id, directory).then(() => {
// Re-fetch pending permissions/questions from the server to recover from
// missed SSE events or failed optimistic removals
const connState = useConnections.getState()
const c = directory ? (connState.clientForDirectory(directory) ?? connState.client) : connState.client
if (c) refreshPending(c, id)
})
}, [id, directory]),
}, [id, directory, selectSession]),
)

// Sync model chip from latest assistant message
Expand Down Expand Up @@ -417,6 +431,7 @@ export default function SessionScreen() {
const text = input.trim()
const files = [...attachments]
setInput("")
if (id) clearDraft(id)
setAttachments([])

// Server slash commands (no attachments for commands)
Expand Down
17 changes: 17 additions & 0 deletions src/stores/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ interface SessionsState {
isLoading: boolean
// Per-session optimistic sending flag — bridging gap between user tap and SSE busy
sending: Record<string, boolean>
// Unsent composer text, isolated by session ID.
drafts: Record<string, string>
loadingMore: boolean
hasMore: boolean
error: string | null

// Actions
loadSessions: () => Promise<void>
selectSession: (sessionID: string, directory?: string) => Promise<void>
setDraft: (sessionID: string, text: string) => void
clearDraft: (sessionID: string) => void
loadOlderMessages: () => Promise<void>
createSession: (title?: string) => Promise<Session | null>
deleteSession: (sessionID: string) => Promise<void>
Expand Down Expand Up @@ -93,10 +97,22 @@ export const useSessions = create<SessionsState>((set, get) => ({
parts: {},
isLoading: false,
sending: {},
drafts: {},
loadingMore: false,
hasMore: false,
error: null,

setDraft: (sessionID, text) =>
set((state) => ({ drafts: { ...state.drafts, [sessionID]: text } })),

clearDraft: (sessionID) =>
set((state) => {
if (!(sessionID in state.drafts)) return state
const drafts = { ...state.drafts }
delete drafts[sessionID]
return { drafts }
}),

loadSessions: async () => {
const connState = useConnections.getState()
// Use a directory-less client so the server returns sessions from ALL projects,
Expand Down Expand Up @@ -245,6 +261,7 @@ export const useSessions = create<SessionsState>((set, get) => ({
await client.session.delete(sessionID)
set((state) => ({
sessions: state.sessions.filter((s) => s.id !== sessionID),
drafts: Object.fromEntries(Object.entries(state.drafts).filter(([id]) => id !== sessionID)),
currentSession: state.currentSession?.id === sessionID ? null : state.currentSession,
messages: state.currentSession?.id === sessionID ? [] : state.messages,
parts: state.currentSession?.id === sessionID ? {} : state.parts,
Expand Down