feat: lots of speech stuff

This commit is contained in:
Brooklyn Nicholson
2026-05-01 19:28:02 -05:00
parent 9f3d393a4d
commit d5d7b5c6dc
41 changed files with 1405 additions and 361 deletions
@@ -1,6 +1,5 @@
import type { QueryClient } from '@tanstack/react-query'
import { type MutableRefObject, useCallback } from 'react'
import { flushSync } from 'react-dom'
import {
appendReasoningPart,
@@ -60,7 +59,6 @@ export function useMessageStream({
transform: (parts: ChatMessagePart[], message: ChatMessage) => ChatMessagePart[],
seed: () => ChatMessagePart[],
opts: {
sync?: boolean
pending?: (message: ChatMessage) => boolean
} = {}
) => {
@@ -112,7 +110,7 @@ export function useMessageStream({
})
}
opts.sync ? flushSync(apply) : apply()
apply()
},
[updateSessionState]
)
@@ -126,8 +124,7 @@ export function useMessageStream({
mutateStream(
sessionId,
parts => appendTextPart(parts, delta),
() => [textPart(delta)],
{ sync: true }
() => [textPart(delta)]
)
},
[mutateStream]
@@ -152,8 +149,7 @@ export function useMessageStream({
return appendReasoningPart(parts, delta)
},
() => [reasoningPart(delta)],
{ sync: true }
() => [reasoningPart(delta)]
)
},
[mutateStream]
@@ -299,6 +295,7 @@ export function useMessageStream({
const apply = explicitSid ? isActiveEvent : !activeSessionIdRef.current
const modelChanged = typeof payload?.model === 'string'
const providerChanged = typeof payload?.provider === 'string'
const runningChanged = typeof payload?.running === 'boolean'
if (apply) {
if (modelChanged) {
@@ -320,6 +317,35 @@ export function useMessageStream({
if (typeof payload?.personality === 'string') {
setCurrentPersonality(normalizePersonalityValue(payload.personality))
}
if (runningChanged && sessionId) {
updateSessionState(sessionId, state => {
const busy = Boolean(payload!.running)
if (state.busy === busy && (busy || !state.awaitingResponse)) {
return state
}
if (busy) {
return {
...state,
busy
}
}
if (state.awaitingResponse && !state.sawAssistantPayload) {
return state
}
return {
...state,
awaitingResponse: false,
busy,
pendingBranchGroup: null,
streamId: null
}
})
}
}
void refreshHermesConfig()
@@ -355,11 +381,11 @@ export function useMessageStream({
}
} else if (event.type === 'reasoning.delta') {
if (sessionId) {
appendReasoningDelta(sessionId, coerceGatewayText(payload?.text))
appendReasoningDelta(sessionId, coerceThinkingText(payload?.text))
}
} else if (event.type === 'reasoning.available') {
if (sessionId) {
appendReasoningDelta(sessionId, coerceGatewayText(payload?.text), true)
appendReasoningDelta(sessionId, coerceThinkingText(payload?.text), true)
}
} else if (event.type === 'message.complete') {
if (!sessionId) {
@@ -13,7 +13,7 @@ import {
import { triggerHaptic } from '@/lib/haptics'
import { $composerAttachments, clearComposerAttachments } from '@/store/composer'
import { clearNotifications, notify, notifyError } from '@/store/notifications'
import { $busy, $messages, setAwaitingResponse, setBusy } from '@/store/session'
import { $busy, $messages, setAwaitingResponse, setBusy, setMessages } from '@/store/session'
import type { ClientSessionState, SlashExecResponse } from '../../types'
@@ -296,12 +296,34 @@ export function usePromptActions({
)
const cancelRun = useCallback(async () => {
if (!activeSessionId) {
const sessionId = activeSessionId || activeSessionIdRef.current
busyRef.current = false
setBusy(false)
setAwaitingResponse(false)
const finalizeMessages = (messages: ChatMessage[]) =>
messages.map(message =>
message.pending
? {
...message,
parts: chatMessageText(message).trim()
? appendTextPart(message.parts, INTERRUPTED_MARKER)
: [...message.parts, textPart(INTERRUPTED_MARKER.trim())],
pending: false
}
: message
)
if (!sessionId) {
setMessages(finalizeMessages($messages.get()))
return
}
updateSessionState(activeSessionId, state => {
updateSessionState(sessionId, state => {
const streamId = state.streamId
const messages = streamId
? state.messages.map(message =>
message.id === streamId
@@ -314,7 +336,7 @@ export function usePromptActions({
}
: message
)
: state.messages
: finalizeMessages(state.messages)
return {
...state,
@@ -328,11 +350,11 @@ export function usePromptActions({
})
try {
await requestGateway('session.interrupt', { session_id: activeSessionId })
await requestGateway('session.interrupt', { session_id: sessionId })
} catch (err) {
notifyError(err, 'Stop failed')
}
}, [activeSessionId, requestGateway, updateSessionState])
}, [activeSessionId, activeSessionIdRef, busyRef, requestGateway, updateSessionState])
const reloadFromMessage = useCallback(
async (parentId: string | null) => {
@@ -87,6 +87,11 @@ export function useSessionActions({
const createBackendSessionForSend = useCallback(async (): Promise<string | null> => {
const created = await requestGateway<SessionCreateResponse>('session.create', { cols: 96 })
if (created.stored_session_id) {
navigate(sessionRoute(created.stored_session_id), { replace: true })
}
setActiveSessionId(created.session_id)
activeSessionIdRef.current = created.session_id
ensureSessionState(created.session_id, created.stored_session_id ?? null)
@@ -94,7 +99,6 @@ export function useSessionActions({
if (created.stored_session_id) {
setSelectedStoredSessionId(created.stored_session_id)
selectedStoredSessionIdRef.current = created.stored_session_id
navigate(sessionRoute(created.stored_session_id), { replace: true })
}
if (created.info?.model) {