@@ -55,6 +55,11 @@ open class DraftStore private constructor(
55 /** The full in-memory snapshot of all drafts, updated when prefs finish loading. */
55 /** The full in-memory snapshot of all drafts, updated when prefs finish loading. */
56 open val drafts: StateFlow<Map<String, String>> = _drafts.asStateFlow()
56 open val drafts: StateFlow<Map<String, String>> = _drafts.asStateFlow()
58+ /** Queued follow-up messages per session, persisted under a namespaced key so a
59+ * follow-up queued mid-run survives process death (the in-memory-only StateFlow in
60+ * ChatViewModel would otherwise lose it, along with the cleared draft). */
61+ private val _followUps = MutableStateFlow<Map<String, String>>(emptyMap())
58 private val _ready = MutableStateFlow(false)
63 private val _ready = MutableStateFlow(false)
59 /** True once the background prefs load has completed. */
64 /** True once the background prefs load has completed. */
60 open val ready: StateFlow<Boolean> = _ready.asStateFlow()
65 open val ready: StateFlow<Boolean> = _ready.asStateFlow()
@@ -65,16 +70,23 @@ open class DraftStore private constructor(
65 // so callers can observe readiness without blocking.
70 // so callers can observe readiness without blocking.
66 if (scope != null && appContext != null) {
71 if (scope != null && appContext != null) {
67 scope.launch(Dispatchers.IO) {
72 scope.launch(Dispatchers.IO) {
68− val snapshot = runCatching {
73+ val all = runCatching {
69 prefs.all.mapValues { it.value as? String ?: "" }
74 prefs.all.mapValues { it.value as? String ?: "" }
70 }.getOrDefault(emptyMap())
75 }.getOrDefault(emptyMap())
76+ // Partition persisted keys: follow-up keys carry a prefix, everything else
77+ // is a plain per-session draft.
78+ val drafts = mutableMapOf<String, String>()
79+ val followUps = mutableMapOf<String, String>()
80+ all.forEach { (k, v) ->
81+ if (k.startsWith(FOLLOWUP_PREFIX)) followUps[k.removePrefix(FOLLOWUP_PREFIX)] = v
71 // Merge instead of overwriting: any in-memory writes that arrived before
84 // Merge instead of overwriting: any in-memory writes that arrived before
72 // the background load completed (e.g. a share-intent draft injected via
85 // the background load completed (e.g. a share-intent draft injected via
73 // setImmediate) take priority over the persisted snapshot so they aren't
86 // setImmediate) take priority over the persisted snapshot so they aren't
74 // clobbered.
87 // clobbered.
75− _drafts.update { current ->
88+ _drafts.update { current -> drafts.apply { putAll(current) } }
76− snapshot.toMutableMap().apply { putAll(current) }
89+ _followUps.update { current -> followUps.apply { putAll(current) } }
78 _ready.value = true
90 _ready.value = true
@@ -85,6 +97,35 @@ open class DraftStore private constructor(
85 /** Returns the draft for [sessionId], or "" if not found or still loading. */
97 /** Returns the draft for [sessionId], or "" if not found or still loading. */
86 open fun get(sessionId: String): String = _drafts.value[sessionId].orEmpty()
98 open fun get(sessionId: String): String = _drafts.value[sessionId].orEmpty()
100+ /** Returns the queued follow-up for [sessionId], or "" if none / still loading. */
101+ open fun getFollowUp(sessionId: String): String = _followUps.value[sessionId].orEmpty()
103+ /** Persist a queued follow-up immediately (fire-and-forget, main-thread-safe).
104+ * Mirrors [flushDraft]: async apply() when prefs are open, else a background thread,
105+ * so a follow-up queued just before process death is flushed before the process exits.
106+ * A blank [text] clears the stored follow-up. */
107+ open fun flushFollowUp(sessionId: String, text: String) {
108+ _followUps.update { current ->
109+ val updated = current.toMutableMap()
110+ if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
113+ writeAsync(FOLLOWUP_PREFIX + sessionId, text)
116+ /** Persist [key]=[text] (or remove it when blank) without blocking the main thread on
117+ * first-time disk I/O — see [flushDraft] for the prefs-not-yet-open rationale. */
118+ private fun writeAsync(key: String, text: String) {
121+ prefs.edit().apply {
122+ if (text.isBlank()) remove(key) else putString(key, text)
124+ }.onFailure { Log.w("DraftStore", "Failed to persist $key", it) }
126+ if (prefsInitialized.get()) write() else flushExecutor.execute { write() }
88 /** Update the in-memory draft immediately without persisting to disk.
129 /** Update the in-memory draft immediately without persisting to disk.
89 * Used when navigation must see the new value synchronously (e.g. share-intent
130 * Used when navigation must see the new value synchronously (e.g. share-intent
90 * injection before ChatScreen composes). Call [set] afterwards to persist. */
131 * injection before ChatScreen composes). Call [set] afterwards to persist. */
@@ -111,16 +152,22 @@ open class DraftStore private constructor(
114− /** Remove the draft for a session (call on session deletion to avoid orphaned entries). */
155+ /** Remove the draft for a session (call on session deletion to avoid orphaned entries).
156+ * Also clears any queued follow-up so a deleted session leaves nothing behind. */
115 open suspend fun remove(sessionId: String) {
157 open suspend fun remove(sessionId: String) {
116 _drafts.update { current ->
158 _drafts.update { current ->
117 val updated = current.toMutableMap()
159 val updated = current.toMutableMap()
118 updated.remove(sessionId)
160 updated.remove(sessionId)
163+ _followUps.update { current ->
164+ val updated = current.toMutableMap()
165+ updated.remove(sessionId)
121 withContext(Dispatchers.IO) {
168 withContext(Dispatchers.IO) {
122 runCatching {
169 runCatching {
123− prefs.edit().remove(sessionId).apply()
170+ prefs.edit().remove(sessionId).remove(FOLLOWUP_PREFIX + sessionId).apply()
124 }.onFailure { Log.w("DraftStore", "Failed to remove draft for $sessionId", it) }
171 }.onFailure { Log.w("DraftStore", "Failed to remove draft for $sessionId", it) }
@@ -166,6 +213,12 @@ open class DraftStore private constructor(
216+ private companion object {
217+ /** Key prefix distinguishing persisted follow-ups from plain drafts in the shared
218+ * prefs file. Uses a NUL so it can't collide with a real session id. */
219+ const val FOLLOWUP_PREFIX = "