1package soy.iko.opencode.ui.chat
3import androidx.lifecycle.ViewModel
4import androidx.lifecycle.viewModelScope
5import soy.iko.opencode.data.model.Agent
6import soy.iko.opencode.data.model.Command
7import soy.iko.opencode.data.model.MessageWithParts
8import soy.iko.opencode.data.model.ModelOption
9import soy.iko.opencode.data.model.Permission
10import soy.iko.opencode.data.model.PermissionReplied
11import soy.iko.opencode.data.model.PermissionResponse
12import soy.iko.opencode.data.model.PermissionUpdated
13import soy.iko.opencode.data.model.TextPart
14import soy.iko.opencode.data.model.defaultOption
15import soy.iko.opencode.data.model.toOptions
16import soy.iko.opencode.data.network.EventStreamClient
17import soy.iko.opencode.data.repo.SessionRepository
18import soy.iko.opencode.di.AppContainer
19import kotlinx.coroutines.flow.MutableStateFlow
20import kotlinx.coroutines.flow.SharingStarted
21import kotlinx.coroutines.flow.StateFlow
22import kotlinx.coroutines.flow.asStateFlow
23import kotlinx.coroutines.flow.flowOf
24import kotlinx.coroutines.flow.stateIn
25import kotlinx.coroutines.launch
28 private val container: AppContainer,
29 private val sessionId: String,
32 private val connection = container.activeConnection.value
34 val connected: Boolean get() = connection != null
36 val messages: StateFlow<List<MessageWithParts>> =
37 (connection?.repository?.observeMessages(sessionId) ?: flowOf(emptyList<MessageWithParts>()))
39 scope = viewModelScope,
40 started = SharingStarted.WhileSubscribed(5_000),
41 initialValue = emptyList(),
44 private val _running = MutableStateFlow(false)
45 val running: StateFlow<Boolean> = _running.asStateFlow()
47 private val _error = MutableStateFlow<String?>(null)
48 val error: StateFlow<String?> = _error.asStateFlow()
50 private val _models = MutableStateFlow<List<ModelOption>>(emptyList())
51 val models: StateFlow<List<ModelOption>> = _models.asStateFlow()
53 private val _selectedModel = MutableStateFlow<ModelOption?>(null)
54 val selectedModel: StateFlow<ModelOption?> = _selectedModel.asStateFlow()
56 val connectionState: StateFlow<EventStreamClient.ConnectionState> =
57 connection?.events?.state
58 ?: MutableStateFlow(EventStreamClient.ConnectionState.Disconnected).asStateFlow()
60 private val _pendingPermission = MutableStateFlow<Permission?>(null)
61 val pendingPermission: StateFlow<Permission?> = _pendingPermission.asStateFlow()
63 private val _agents = MutableStateFlow<List<Agent>>(emptyList())
64 val agents: StateFlow<List<Agent>> = _agents.asStateFlow()
66 private val _selectedAgent = MutableStateFlow<String?>(null)
67 val selectedAgent: StateFlow<String?> = _selectedAgent.asStateFlow()
69 private val _commands = MutableStateFlow<List<Command>>(emptyList())
70 val commands: StateFlow<List<Command>> = _commands.asStateFlow()
72 private val _sessionTitle = MutableStateFlow<String?>(null)
73 val sessionTitle: StateFlow<String?> = _sessionTitle.asStateFlow()
75 /** The text of the last send that failed, surfaced so the UI can offer a retry. */
76 private val _failedDraft = MutableStateFlow<String?>(null)
77 val failedDraft: StateFlow<String?> = _failedDraft.asStateFlow()
79 fun clearError() { _error.value = null }
81 fun selectModel(option: ModelOption) { _selectedModel.value = option }
83 fun selectAgent(name: String?) { _selectedAgent.value = name }
86 // Watch the bus for run completion / errors / permission asks for this session.
89 viewModelScope.launch {
90 conn.events.events.collect { event ->
91 if (SessionRepository.isIdle(event, sessionId)) _running.value = false
92 if (SessionRepository.isError(event, sessionId)) {
93 _running.value = false
94 _error.value = "The agent reported an error."
97 is PermissionUpdated ->
98 if (event.properties.sessionID == sessionId) _pendingPermission.value = event.properties
99 is PermissionReplied ->
100 if (event.properties.permissionID == _pendingPermission.value?.id) _pendingPermission.value = null
105 // Load the model catalog; preselect the server default. Failure is non-fatal —
106 // sending with no model just uses the server's default agent/model.
107 viewModelScope.launch {
108 runCatching { conn.api.providers() }.getOrNull()?.let { resp ->
109 val options = resp.toOptions()
110 _models.value = options
111 _selectedModel.value = resp.defaultOption(options)
114 // Load the agent catalog (non-fatal).
115 viewModelScope.launch {
116 runCatching { conn.api.agents() }.getOrNull()?.let { _agents.value = it }
118 // Load the command catalog (non-fatal).
119 viewModelScope.launch {
120 runCatching { conn.api.commands() }.getOrNull()?.let { _commands.value = it }
122 // Resolve the human-readable session title for the app bar (non-fatal).
123 viewModelScope.launch {
124 runCatching { conn.repository.listSessions() }
126 ?.firstOrNull { it.id == sessionId }
127 ?.let { _sessionTitle.value = it.displayTitle }
132 /** Sends [text]; returns true on success so the caller can clear the draft only then. */
133 fun send(text: String): Boolean {
134 val conn = connection ?: return false
135 val trimmed = text.trim()
136 if (trimmed.isEmpty() || _running.value) return false
137 _running.value = true
139 _failedDraft.value = null
140 viewModelScope.launch {
141 val ok = runCatching {
142 conn.repository.sendPrompt(
145 model = _selectedModel.value?.ref,
146 agent = _selectedAgent.value,
149 _running.value = false
151 _failedDraft.value = trimmed
152 _error.value = "Failed to send"
158 /** Re-send the last draft whose send failed, if any. */
160 val draft = _failedDraft.value ?: return
161 _failedDraft.value = null
165 /** Invoke a slash-command by name via the server's /command endpoint. */
166 fun runCommand(command: Command) {
167 val conn = connection ?: return
168 if (_running.value) return
169 _running.value = true
171 viewModelScope.launch {
173 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
174 }.onFailure { _error.value = it.message ?: "Failed to run command" }
175 _running.value = false
180 val conn = connection ?: return
181 viewModelScope.launch {
182 runCatching { conn.repository.abort(sessionId) }
183 _running.value = false
187 fun respondPermission(permission: Permission, response: PermissionResponse) {
188 val conn = connection ?: return
189 // Clear optimistically; permission.replied will confirm.
190 _pendingPermission.value = null
191 viewModelScope.launch {
192 runCatching { conn.api.respondPermission(sessionId, permission.id, response) }
193 .onFailure { _error.value = it.message ?: "Failed to respond to permission" }
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