1package soy.iko.opencode.data.repo
3import android.content.Context
4import kotlinx.coroutines.flow.MutableStateFlow
5import kotlinx.coroutines.flow.StateFlow
6import kotlinx.coroutines.flow.asStateFlow
9 * Persists the most recently picked models so the model picker can surface them as a
10 * "Recent" section above the full catalog. Backed by SharedPreferences because the UI
11 * needs the initial value synchronously and the list is tiny (≤ [MAX_ENTRIES]), mirroring
12 * the low-stakes-persistence rationale of [SearchHistoryStore]. `open` with a `protected`
13 * no-arg constructor so tests can subclass and override without a real Context.
15 * Entries are stored as `providerID\modelID` composite keys (the separator is chosen so it
16 * can't collide with provider or model ids, which are URL/path-safe identifiers).
18open class RecentModelsStore private constructor(
19 private val appContext: Context?,
20 @Suppress("unused") private val testMode: Boolean,
22 constructor(context: Context) : this(context.applicationContext, false)
23 protected constructor() : this(null, true)
25 private val prefs by lazy {
26 appContext?.getSharedPreferences("recent_models", Context.MODE_PRIVATE)
27 ?: error("No context — override methods in test subclass")
30 private val _entries = MutableStateFlow<List<String>>(emptyList())
31 open val entries: StateFlow<List<String>> = _entries.asStateFlow()
34 if (appContext != null) {
35 _entries.value = runCatching {
36 prefs.getString(KEY, null)
37 ?.takeIf { it.isNotBlank() }
39 ?.filter { it.isNotBlank() }
41 }.getOrDefault(emptyList()) ?: emptyList()
45 /** Insert [providerId]/[modelId] at the head of the recents, deduped and capped at
46 * [MAX_ENTRIES]. Blank ids are ignored. */
47 open fun add(providerId: String, modelId: String) {
48 val p = providerId.trim()
49 val m = modelId.trim()
50 if (p.isBlank() || m.isBlank()) return
51 val key = "$p$SEPARATOR$m"
52 val updated = (listOf(key) + _entries.value.filter { it != key }).take(MAX_ENTRIES)
53 _entries.value = updated
55 prefs.edit().putString(KEY, updated.joinToString(SEPARATOR)).apply()
59 /** Split a stored composite key back into its provider/model id pair. Returns null if
60 * the entry is malformed (shouldn't happen, but a corrupt prefs value must never crash
62 fun split(entry: String): Pair<String, String>? {
63 val parts = entry.split(SEPARATOR)
64 return if (parts.size == 2 && parts[0].isNotBlank() && parts[1].isNotBlank())
65 parts[0] to parts[1] else null
69 _entries.value = emptyList()
70 runCatching { prefs.edit().remove(KEY).apply() }
73 private companion object {
74 const val KEY = "models"
75 // A newline can't appear in a provider/model id (they're URL/path-safe), so it's a
76 // safe list separator that won't collide with id contents.
77 const val SEPARATOR = "\n"
78 const val MAX_ENTRIES = 5
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list