Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
764 changes: 383 additions & 381 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatInputField.kt

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ data class ChatState(
// Tool call state — ephemeral, populated only during active streaming
val activeToolCalls: List<ToolCallInfo> = emptyList(),

// Thinking/reasoning content — ephemeral, streamed from models that expose reasoning.
// Populated during active streaming; cleared when a new message starts.
// Not persisted to the database — visible for the current session only.
val activeThinkingContent: String = "",

// Bookmark state — IDs of messages pinned by the user in this session
val bookmarkedMessageIds: Set<String> = emptySet(),

Expand Down
2 changes: 2 additions & 0 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ fun chatView(
val sessionTitle = state.sessionTitle
val project = state.project
val activeToolCalls = state.activeToolCalls
val activeThinkingContent = state.activeThinkingContent
val bookmarkedMessageIds = state.bookmarkedMessageIds
val pendingScrollToMessageId = state.pendingScrollToMessageId

Expand Down Expand Up @@ -1141,6 +1142,7 @@ fun chatView(
viewportTopY = viewportBounds?.top,
projectId = project?.id,
activeToolCalls = activeToolCalls,
activeThinkingContent = activeThinkingContent,
bookmarkedMessageIds = bookmarkedMessageIds,
onToggleBookmark = { messageId -> actions.toggleBookmark(messageId) },
onForkFromMessage = { messageId -> actions.forkFromMessage(messageId) },
Expand Down
42 changes: 40 additions & 2 deletions desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class ChatViewModel(
var activeToolCalls by mutableStateOf<List<ToolCallInfo>>(emptyList())
private set

var activeThinkingContent by mutableStateOf("")
private set

var bookmarkedMessageIds by mutableStateOf<Set<String>>(emptySet())
private set

Expand All @@ -140,6 +143,7 @@ class ChatViewModel(
sessionTitle = sessionTitle ?: "",
project = project,
activeToolCalls = activeToolCalls,
activeThinkingContent = activeThinkingContent,
bookmarkedMessageIds = bookmarkedMessageIds,
pendingScrollToMessageId = pendingScrollToMessageId,
)
Expand Down Expand Up @@ -397,6 +401,38 @@ class ChatViewModel(
}
}

// Subscribe to thinking/reasoning tokens streamed by models that expose reasoning.
// Accumulate chunks into a single string; UI renders them in a collapsible section.
scope.launch {
activeThread.thinkingChunks.collect { chunks ->
if (currentSessionId.value == sessionId) {
activeThinkingContent = chunks.joinToString("")
// Ensure a placeholder bubble exists so the thinking section is visible
// even before the first response token arrives.
if (chunks.isNotEmpty() && messages.none { !it.isUser && it.id == null }) {
isThinking = false
stopThinkingTimer()
val placeholder = ChatMessageDTO(
content = "",
isUser = false,
id = null,
timestamp = null,
)
messages = if (retryInsertPosition != null) {
messages.toMutableList().apply { add(retryInsertPosition!!, placeholder) }
} else {
val lastMsg = messages.lastOrNull()
if (lastMsg != null && !lastMsg.isUser) {
messages.dropLast(1) + placeholder
} else {
messages + placeholder
}
}
}
}
}
}

// Monitor completion in a separate job
scope.launch {
activeThread.isComplete.collect { isComplete ->
Expand Down Expand Up @@ -569,8 +605,9 @@ class ChatViewModel(
isThinking = true
thinkingElapsedSeconds = 0

// Clear tool chips from the previous response before retrying
// Clear tool chips and thinking content from the previous response before retrying
activeToolCalls = emptyList()
activeThinkingContent = ""

startThinkingTimer()

Expand Down Expand Up @@ -649,8 +686,9 @@ class ChatViewModel(
fun sendMessage(projectId: String?, mode: CreationMode, message: String, attachments: List<FileAttachmentDTO> = emptyList(), enabledServerIds: Set<String> = emptySet()) {
if (message.isBlank() || isLoading) return

// Clear tool chips from the previous response now that a new message is starting
// Clear tool chips and thinking content from the previous response now that a new message is starting
activeToolCalls = emptyList()
activeThinkingContent = ""

// Session ID must be set by this point (from resumeSession)
val sessionId = currentSessionId.value ?: run {
Expand Down
Loading
Loading