@@ -64,6 +64,24 @@ open class DraftStore private constructor(
6464 /** True once the background prefs load has completed. */
6565 open val ready: StateFlow<Boolean> = _ready.asStateFlow()
6666
67+ /** Keys written in-memory (set / cleared / removed) *before* the async prefs load
68+ * finished. For these the in-memory state is authoritative — the load must not
69+ * resurrect a persisted value the user has since cleared, which a plain putAll(current)
70+ * merge cannot express (it can add/override, but not delete). Consulted once, in the
71+ * init merge. Separate sets because drafts and follow-ups share the session-id key space
72+ * but live under different prefs keys. */
73+ // ConcurrentHashMap-backed sets: weakly-consistent iteration so the init merge's forEach
74+ // can't throw ConcurrentModificationException if a mutator adds a key concurrently.
75+ private val touchedDraftKeys: MutableSet<String> = java.util.concurrent.ConcurrentHashMap.newKeySet()
76+ private val touchedFollowUpKeys: MutableSet<String> = java.util.concurrent.ConcurrentHashMap.newKeySet()
77+
78+ private fun markDraftTouched(sessionId: String) {
79+ if (!_ready.value) touchedDraftKeys.add(sessionId)
80+ }
81+ private fun markFollowUpTouched(sessionId: String) {
82+ if (!_ready.value) touchedFollowUpKeys.add(sessionId)
83+ }
84+
6785 init {
6886 // Eagerly load the prefs file on a background thread so the first main-thread
6987 // get() doesn't block on disk I/O. The loaded snapshot is published via [_drafts]
@@ -84,9 +102,21 @@ open class DraftStore private constructor(
84102 // Merge instead of overwriting: any in-memory writes that arrived before
85103 // the background load completed (e.g. a share-intent draft injected via
86104 // setImmediate) take priority over the persisted snapshot so they aren't
87− // clobbered.
88− _drafts.update { current -> drafts.apply { putAll(current) } }
89− _followUps.update { current -> followUps.apply { putAll(current) } }
105+ // clobbered. For keys the user explicitly touched before the load finished,
106+ // drop the persisted entry first so a value they *cleared* isn't resurrected
107+ // (the cleared key is absent from `current`, so putAll alone can't remove it).
108+ _drafts.update { current ->
109+ drafts.apply {
110+ touchedDraftKeys.forEach { remove(it) }
111+ putAll(current)
112+ }
113+ }
114+ _followUps.update { current ->
115+ followUps.apply {
116+ touchedFollowUpKeys.forEach { remove(it) }
117+ putAll(current)
118+ }
119+ }
90120 _ready.value = true
91121 }
92122 } else {
@@ -105,6 +135,7 @@ open class DraftStore private constructor(
105135 * so a follow-up queued just before process death is flushed before the process exits.
106136 * A blank [text] clears the stored follow-up. */
107137 open fun flushFollowUp(sessionId: String, text: String) {
138+ markFollowUpTouched(sessionId)
108139 _followUps.update { current ->
109140 val updated = current.toMutableMap()
110141 if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
@@ -130,6 +161,7 @@ open class DraftStore private constructor(
130161 * Used when navigation must see the new value synchronously (e.g. share-intent
131162 * injection before ChatScreen composes). Call [set] afterwards to persist. */
132163 open fun setImmediate(sessionId: String, text: String) {
164+ markDraftTouched(sessionId)
133165 _drafts.update { current ->
134166 val updated = current.toMutableMap()
135167 if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
@@ -138,6 +170,7 @@ open class DraftStore private constructor(
138170 }
139171
140172 open suspend fun set(sessionId: String, text: String) {
173+ markDraftTouched(sessionId)
141174 _drafts.update { current ->
142175 val updated = current.toMutableMap()
143176 if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
@@ -155,6 +188,8 @@ open class DraftStore private constructor(
155188 /** Remove the draft for a session (call on session deletion to avoid orphaned entries).
156189 * Also clears any queued follow-up so a deleted session leaves nothing behind. */
157190 open suspend fun remove(sessionId: String) {
191+ markDraftTouched(sessionId)
192+ markFollowUpTouched(sessionId)
158193 _drafts.update { current ->
159194 val updated = current.toMutableMap()
160195 updated.remove(sessionId)
@@ -187,6 +222,7 @@ open class DraftStore private constructor(
187222 * preferable to an ANR.
188223 */
189224 open fun flushDraft(sessionId: String, text: String) {
225+ markDraftTouched(sessionId)
190226 _drafts.update { current ->
191227 val updated = current.toMutableMap()
192228 if (text.isBlank()) updated.remove(sessionId) else updated[sessionId] = text
@@ -215,7 +251,8 @@ open class DraftStore private constructor(
215251
216252 private companion object {
217253 /** 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. */
254+ * prefs file. The leading/trailing spaces can't collide with a real opencode session
255+ * id (which are non-blank, space-free tokens like "ses_..."). */
219256 const val FOLLOWUP_PREFIX = "