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.defaultOption
14import soy.iko.opencode.data.model.toOptions
15import soy.iko.opencode.data.network.EventStreamClient
16import soy.iko.opencode.data.repo.SessionRepository
17import soy.iko.opencode.di.AppContainer
18import kotlinx.coroutines.flow.MutableStateFlow
19import kotlinx.coroutines.flow.SharingStarted
20import kotlinx.coroutines.flow.StateFlow
21import kotlinx.coroutines.flow.asStateFlow
22import kotlinx.coroutines.flow.flowOf
23import kotlinx.coroutines.flow.stateIn
24import kotlinx.coroutines.launch
27 private val container: AppContainer,
28 private val sessionId: String,
31 private val connection = container.activeConnection.value
33 val connected: Boolean get() = connection != null
35 val messages: StateFlow<List<MessageWithParts>> =
36 (connection?.repository?.observeMessages(sessionId) ?: flowOf(emptyList<MessageWithParts>()))
38 scope = viewModelScope,
39 started = SharingStarted.WhileSubscribed(5_000),
40 initialValue = emptyList(),
43 private val _running = MutableStateFlow(false)
44 val running: StateFlow<Boolean> = _running.asStateFlow()
46 private val _error = MutableStateFlow<String?>(null)
47 val error: StateFlow<String?> = _error.asStateFlow()
49 private val _models = MutableStateFlow<List<ModelOption>>(emptyList())
50 val models: StateFlow<List<ModelOption>> = _models.asStateFlow()
52 private val _selectedModel = MutableStateFlow<ModelOption?>(null)
53 val selectedModel: StateFlow<ModelOption?> = _selectedModel.asStateFlow()
55 val connectionState: StateFlow<EventStreamClient.ConnectionState> =
56 connection?.events?.state
57 ?: MutableStateFlow(EventStreamClient.ConnectionState.Disconnected).asStateFlow()
59 private val _pendingPermission = MutableStateFlow<Permission?>(null)
60 val pendingPermission: StateFlow<Permission?> = _pendingPermission.asStateFlow()
62 private val _agents = MutableStateFlow<List<Agent>>(emptyList())
63 val agents: StateFlow<List<Agent>> = _agents.asStateFlow()
65 private val _selectedAgent = MutableStateFlow<String?>(null)
66 val selectedAgent: StateFlow<String?> = _selectedAgent.asStateFlow()
68 private val _commands = MutableStateFlow<List<Command>>(emptyList())
69 val commands: StateFlow<List<Command>> = _commands.asStateFlow()
71 fun clearError() { _error.value = null }
73 fun selectModel(option: ModelOption) { _selectedModel.value = option }
75 fun selectAgent(name: String?) { _selectedAgent.value = name }
78 // Watch the bus for run completion / errors / permission asks for this session.
81 viewModelScope.launch {
82 conn.events.events.collect { event ->
83 if (SessionRepository.isIdle(event, sessionId)) _running.value = false
84 if (SessionRepository.isError(event, sessionId)) {
85 _running.value = false
86 _error.value = "The agent reported an error."
89 is PermissionUpdated ->
90 if (event.properties.sessionID == sessionId) _pendingPermission.value = event.properties
91 is PermissionReplied ->
92 if (event.properties.permissionID == _pendingPermission.value?.id) _pendingPermission.value = null
97 // Load the model catalog; preselect the server default. Failure is non-fatal —
98 // sending with no model just uses the server's default agent/model.
99 viewModelScope.launch {
100 runCatching { conn.api.providers() }.getOrNull()?.let { resp ->
101 val options = resp.toOptions()
102 _models.value = options
103 _selectedModel.value = resp.defaultOption(options)
106 // Load the agent catalog (non-fatal).
107 viewModelScope.launch {
108 runCatching { conn.api.agents() }.getOrNull()?.let { _agents.value = it }
110 // Load the command catalog (non-fatal).
111 viewModelScope.launch {
112 runCatching { conn.api.commands() }.getOrNull()?.let { _commands.value = it }
117 fun send(text: String) {
118 val conn = connection ?: return
119 val trimmed = text.trim()
120 if (trimmed.isEmpty() || _running.value) return
121 _running.value = true
123 viewModelScope.launch {
125 conn.repository.sendPrompt(
128 model = _selectedModel.value?.ref,
129 agent = _selectedAgent.value,
131 }.onFailure { _error.value = it.message ?: "Failed to send" }
132 _running.value = false
136 /** Invoke a slash-command: sends the command's template text with the command's agent/model if set. */
137 fun runCommand(command: Command) {
138 val conn = connection ?: return
139 if (_running.value) return
140 _running.value = true
142 viewModelScope.launch {
147 agent = command.agent,
149 }.onFailure { _error.value = it.message ?: "Failed to run command" }
150 _running.value = false
155 val conn = connection ?: return
156 viewModelScope.launch {
157 runCatching { conn.repository.abort(sessionId) }
158 _running.value = false
162 fun respondPermission(permission: Permission, response: PermissionResponse) {
163 val conn = connection ?: return
164 // Clear optimistically; permission.replied will confirm.
165 _pendingPermission.value = null
166 viewModelScope.launch {
167 runCatching { conn.api.respondPermission(sessionId, permission.id, response) }
168 .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