Fix bridge history, profile models, and Windows gateway handling (#845)
* feat: support profile-aware group chat bridge flows * feat: route cron jobs through hermes cli * Fix group chat routing and isolate bridge tests * Add Grok image-to-video media skill * Default Grok videos to media directory * Fix bridge profile fallback and cron repeat clearing * Refine bridge chat and gateway platform handling * Filter bridge tool-call text deltas * Preserve structured bridge chat history * Prepare beta release build artifacts * Fix Windows run profile resolution * Fix Windows path compatibility checks * Fix profile-scoped model page display * Hide Windows subprocess windows for jobs and updates * Hide Windows file backend subprocess windows * Avoid Windows gateway restart lock conflicts * Treat Windows gateway lock as running on startup * Force release Windows gateway lock on restart * Tighten Windows gateway lock cleanup * Update chat e2e source expectation * Bump package version to 0.5.30 --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
updateModelAlias,
|
||||
type AvailableModelGroup,
|
||||
type AvailableModelsResponse,
|
||||
type ProfileAvailableModels,
|
||||
type ModelVisibility,
|
||||
type ModelVisibilityRule,
|
||||
} from '@/api/hermes/system'
|
||||
@@ -31,6 +32,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
const clientOutdated = ref(false)
|
||||
const updating = ref(false)
|
||||
const modelGroups = ref<AvailableModelGroup[]>([])
|
||||
const profileModelGroups = ref<ProfileAvailableModels[]>([])
|
||||
const selectedModel = ref('')
|
||||
const selectedProvider = ref('')
|
||||
const customModels = ref<Record<string, string[]>>({})
|
||||
@@ -80,6 +82,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
function applyAvailableModelsResponse(res: AvailableModelsResponse) {
|
||||
modelGroups.value = res.groups
|
||||
profileModelGroups.value = res.profiles || []
|
||||
modelAliases.value = res.model_aliases || {}
|
||||
modelVisibility.value = res.model_visibility || {}
|
||||
|
||||
@@ -300,6 +303,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
doUpdate,
|
||||
reloadClient,
|
||||
modelGroups,
|
||||
profileModelGroups,
|
||||
customModels,
|
||||
modelAliases,
|
||||
modelVisibility,
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface PendingApproval {
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
profile?: string
|
||||
title: string
|
||||
source?: string
|
||||
messages: Message[]
|
||||
@@ -232,6 +233,7 @@ function mapHermesMessages(msgs: HermesMessage[]): Message[] {
|
||||
function mapHermesSession(s: SessionSummary): Session {
|
||||
return {
|
||||
id: s.id,
|
||||
profile: s.profile || 'default',
|
||||
title: s.title || '',
|
||||
source: s.source || undefined,
|
||||
messages: [],
|
||||
@@ -389,10 +391,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
return streamStates.value.has(sessionId) || serverWorking.value.has(sessionId)
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
function clearActiveSession() {
|
||||
activeSessionId.value = null
|
||||
activeSession.value = null
|
||||
focusMessageId.value = null
|
||||
setAbortState(null)
|
||||
setCompressionState(null)
|
||||
removeItem(storageKey())
|
||||
}
|
||||
|
||||
async function loadSessions(profile?: string | null) {
|
||||
isLoadingSessions.value = true
|
||||
try {
|
||||
const list = await fetchSessions()
|
||||
const list = await fetchSessions(undefined, undefined, profile || undefined)
|
||||
const fresh = list.map(mapHermesSession)
|
||||
// Preserve already-loaded messages for sessions that are still present,
|
||||
// so we don't blow away the active session's messages on refresh.
|
||||
@@ -410,6 +421,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
: sessions.value[0]?.id
|
||||
if (targetId) {
|
||||
await switchSession(targetId)
|
||||
} else {
|
||||
clearActiveSession()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err)
|
||||
@@ -439,14 +452,17 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
|
||||
function createSession(): Session {
|
||||
function createSession(options: { profile?: string; model?: string; provider?: string } = {}): Session {
|
||||
const session: Session = {
|
||||
id: uid(),
|
||||
profile: options.profile || useProfilesStore().activeProfileName || 'default',
|
||||
title: '',
|
||||
source: 'api_server',
|
||||
source: 'cli',
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
model: options.model || undefined,
|
||||
provider: options.provider || '',
|
||||
}
|
||||
sessions.value.unshift(session)
|
||||
return session
|
||||
@@ -606,12 +622,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
resumeServerWorkingRun(sessionId)
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
const session = createSession()
|
||||
// Inherit current global model
|
||||
function newChat(options: { profile?: string; model?: string; provider?: string } = {}) {
|
||||
const appStore = useAppStore()
|
||||
session.model = appStore.selectedModel || undefined
|
||||
session.provider = appStore.selectedProvider || ''
|
||||
const session = createSession({
|
||||
profile: options.profile,
|
||||
model: options.model || appStore.selectedModel || undefined,
|
||||
provider: options.provider || appStore.selectedProvider || '',
|
||||
})
|
||||
switchSession(session.id)
|
||||
}
|
||||
|
||||
@@ -852,7 +869,10 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
// Capture session ID at send time — all callbacks use this, not activeSessionId
|
||||
const sid = activeSessionId.value!
|
||||
const isBridgeSlashCommand = activeSession.value?.source === 'cli' && content.trim().startsWith('/')
|
||||
const shouldSendInitialSessionConfig = activeSession.value
|
||||
? activeSession.value.messageCount == null || activeSession.value.messageCount === 0
|
||||
: false
|
||||
const isBridgeSlashCommand = content.trim().startsWith('/')
|
||||
const isBridgeCompressCommand = isBridgeSlashCommand && /^\/compress(?:\s|$)/i.test(content.trim())
|
||||
const wasLiveBeforeSend = isSessionLive(sid)
|
||||
const shouldQueue = wasLiveBeforeSend && !isBridgeSlashCommand
|
||||
@@ -912,19 +932,22 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const appStore = useAppStore()
|
||||
await appStore.waitForModelsForRun()
|
||||
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
||||
const isBridgeSource = activeSession.value?.source === 'cli'
|
||||
const sessionProvider = activeSession.value?.provider || appStore.selectedProvider
|
||||
const runPayload = {
|
||||
input,
|
||||
session_id: sid,
|
||||
model: isBridgeSource ? undefined : sessionModel || undefined,
|
||||
provider: isBridgeSource ? undefined : sessionProvider || undefined,
|
||||
profile: shouldSendInitialSessionConfig ? activeSession.value?.profile || undefined : undefined,
|
||||
model: shouldSendInitialSessionConfig ? sessionModel || undefined : undefined,
|
||||
provider: shouldSendInitialSessionConfig ? sessionProvider || undefined : undefined,
|
||||
model_groups: appStore.modelGroups.map(group => ({
|
||||
provider: group.provider,
|
||||
models: group.models,
|
||||
})),
|
||||
queue_id: userMsg.id,
|
||||
source: (isBridgeSource ? 'cli' : 'api_server') as 'cli' | 'api_server',
|
||||
source: 'cli' as const,
|
||||
}
|
||||
if (shouldSendInitialSessionConfig && activeSession.value) {
|
||||
activeSession.value.messageCount = Math.max(activeSession.value.messageCount || 0, 1)
|
||||
}
|
||||
|
||||
if (shouldQueue) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { fetchGateways, startGateway, stopGateway, type GatewayStatus } from '@/api/hermes/gateways'
|
||||
|
||||
export const useGatewayStore = defineStore('gateways', () => {
|
||||
const gateways = ref<GatewayStatus[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchStatus() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await fetchGateways()
|
||||
gateways.value = Array.isArray(data) ? data : Object.values(data || {})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function start(name: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = await startGateway(name)
|
||||
// Update the specific gateway in the list
|
||||
const idx = gateways.value.findIndex(g => g.profile === name)
|
||||
if (idx >= 0) {
|
||||
gateways.value[idx] = status
|
||||
} else {
|
||||
gateways.value.push(status)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stop(name: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
await stopGateway(name)
|
||||
// Update the specific gateway in the list
|
||||
const gw = gateways.value.find(g => g.profile === name)
|
||||
if (gw) {
|
||||
gw.running = false
|
||||
gw.pid = undefined
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { gateways, loading, fetchStatus, start, stop }
|
||||
})
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { getApiKey } from '@/api/client'
|
||||
import { getDownloadUrl } from '@/api/hermes/download'
|
||||
import type { Attachment, ContentBlock } from './chat'
|
||||
import {
|
||||
connectGroupChat,
|
||||
disconnectGroupChat,
|
||||
@@ -22,6 +25,66 @@ import {
|
||||
clearRoomContext,
|
||||
} from '@/api/hermes/group-chat'
|
||||
|
||||
async function uploadGroupFiles(attachments: Attachment[]): Promise<{ name: string; path: string }[]> {
|
||||
const formData = new FormData()
|
||||
for (const att of attachments) {
|
||||
if (att.file) formData.append('file', att.file, att.name)
|
||||
}
|
||||
const token = getApiKey()
|
||||
const res = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
|
||||
const data = await res.json() as { files: { name: string; path: string }[] }
|
||||
return data.files
|
||||
}
|
||||
|
||||
function buildGroupContentBlocks(content: string, attachments: Attachment[], files: { name: string; path: string }[]): ContentBlock[] {
|
||||
const blocks: ContentBlock[] = []
|
||||
if (content.trim()) blocks.push({ type: 'text', text: content.trim() })
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
const file = files[i]
|
||||
const attachment = attachments[i]
|
||||
if (attachment?.type.startsWith('image/')) {
|
||||
blocks.push({
|
||||
type: 'image',
|
||||
name: file.name,
|
||||
path: file.path,
|
||||
media_type: attachment.type,
|
||||
})
|
||||
} else {
|
||||
blocks.push({
|
||||
type: 'file',
|
||||
name: file.name,
|
||||
path: file.path,
|
||||
media_type: attachment?.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function uid(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
||||
}
|
||||
|
||||
function normalizeLocalFilePath(path: string): string {
|
||||
return /^[a-zA-Z]:\\/.test(path) ? path.replace(/\\/g, '/') : path
|
||||
}
|
||||
|
||||
export interface GroupPendingApproval {
|
||||
roomId: string
|
||||
agentName: string
|
||||
approvalId: string
|
||||
command: string
|
||||
description: string
|
||||
choices: Array<'once' | 'session' | 'always' | 'deny'>
|
||||
allowPermanent: boolean
|
||||
requestedAt: number
|
||||
}
|
||||
|
||||
export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
// ─── State ─────────────────────────────────────────────
|
||||
const connected = ref(false)
|
||||
@@ -35,6 +98,18 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
const error = ref<string | null>(null)
|
||||
const typingUsers = ref<Map<string, { name: string; timer: ReturnType<typeof setTimeout> }>>(new Map())
|
||||
const contextStatuses = ref<Map<string, { agentName: string; status: string }>>(new Map())
|
||||
const autoPlaySpeechEnabled = ref(false)
|
||||
const pendingApprovals = ref<Map<string, GroupPendingApproval>>(new Map())
|
||||
|
||||
function setAutoPlaySpeech(enabled: boolean) {
|
||||
autoPlaySpeechEnabled.value = enabled
|
||||
}
|
||||
|
||||
function playMessageSpeech(messageId: string, content: string) {
|
||||
window.dispatchEvent(new CustomEvent('auto-play-speech', {
|
||||
detail: { messageId, content },
|
||||
}))
|
||||
}
|
||||
|
||||
// Computed: returns first active status for backward compat
|
||||
const contextStatus = computed(() => {
|
||||
@@ -43,13 +118,18 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
}
|
||||
return null
|
||||
})
|
||||
const activePendingApproval = computed(() => {
|
||||
if (!currentRoomId.value) return null
|
||||
for (const approval of pendingApprovals.value.values()) {
|
||||
if (approval.roomId === currentRoomId.value) return approval
|
||||
}
|
||||
return null
|
||||
})
|
||||
const userId = ref(getStoredUserId())
|
||||
const userName = ref(getStoredUserName() || '')
|
||||
|
||||
// ─── Computed ───────────────────────────────────────────
|
||||
const sortedMessages = computed(() => {
|
||||
return [...messages.value].sort((a, b) => a.timestamp - b.timestamp)
|
||||
})
|
||||
const sortedMessages = computed(() => mapGroupMessages([...messages.value].sort((a, b) => a.timestamp - b.timestamp)))
|
||||
|
||||
const memberNames = computed(() => {
|
||||
return members.value.map(m => m.name)
|
||||
@@ -94,10 +174,89 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
|
||||
socket.on('message', (msg: ChatMessage) => {
|
||||
if (msg.roomId === currentRoomId.value) {
|
||||
const idx = messages.value.findIndex(m => m.id === msg.id)
|
||||
const existing = idx >= 0 ? messages.value[idx] : null
|
||||
const resolvedMsg = {
|
||||
...msg,
|
||||
isStreaming: false,
|
||||
attachments: existing?.attachments,
|
||||
}
|
||||
if (idx >= 0) {
|
||||
messages.value[idx] = resolvedMsg
|
||||
messages.value = [...messages.value]
|
||||
} else {
|
||||
messages.value.push(resolvedMsg)
|
||||
}
|
||||
if (autoPlaySpeechEnabled.value && resolvedMsg.role === 'assistant' && resolvedMsg.content?.trim()) {
|
||||
setTimeout(() => playMessageSpeech(resolvedMsg.id, resolvedMsg.content), 300)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('message_stream_start', (msg: ChatMessage) => {
|
||||
if (msg.roomId !== currentRoomId.value) return
|
||||
messages.value = messages.value.filter(m => !(
|
||||
m.roomId === msg.roomId &&
|
||||
m.senderId === msg.senderId &&
|
||||
m.id !== msg.id &&
|
||||
m.isStreaming &&
|
||||
!m.content?.trim() &&
|
||||
!m.reasoning?.trim() &&
|
||||
!m.tool_calls?.length
|
||||
))
|
||||
msg.isStreaming = true
|
||||
const idx = messages.value.findIndex(m => m.id === msg.id)
|
||||
if (idx >= 0) {
|
||||
messages.value[idx] = { ...messages.value[idx], ...msg, isStreaming: true }
|
||||
messages.value = [...messages.value]
|
||||
} else {
|
||||
messages.value.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('message_stream_delta', (data: { roomId: string; id: string; delta: string }) => {
|
||||
if (data.roomId !== currentRoomId.value) return
|
||||
const idx = messages.value.findIndex(m => m.id === data.id)
|
||||
if (idx < 0) return
|
||||
messages.value[idx] = {
|
||||
...messages.value[idx],
|
||||
content: messages.value[idx].content + data.delta,
|
||||
}
|
||||
messages.value = [...messages.value]
|
||||
})
|
||||
|
||||
socket.on('message_reasoning_delta', (data: { roomId: string; id: string; delta: string }) => {
|
||||
if (data.roomId !== currentRoomId.value) return
|
||||
const idx = messages.value.findIndex(m => m.id === data.id)
|
||||
if (idx < 0) return
|
||||
messages.value[idx] = {
|
||||
...messages.value[idx],
|
||||
reasoning: (messages.value[idx].reasoning || '') + data.delta,
|
||||
reasoning_content: (messages.value[idx].reasoning_content || '') + data.delta,
|
||||
isStreaming: true,
|
||||
}
|
||||
messages.value = [...messages.value]
|
||||
})
|
||||
|
||||
socket.on('message_stream_end', (data: { roomId: string; id: string }) => {
|
||||
if (data.roomId !== currentRoomId.value) return
|
||||
const idx = messages.value.findIndex(m => m.id === data.id)
|
||||
if (
|
||||
idx >= 0 &&
|
||||
!messages.value[idx].content?.trim() &&
|
||||
!messages.value[idx].reasoning?.trim() &&
|
||||
!messages.value[idx].tool_calls?.length
|
||||
) {
|
||||
messages.value.splice(idx, 1)
|
||||
} else if (idx >= 0) {
|
||||
messages.value[idx] = {
|
||||
...messages.value[idx],
|
||||
isStreaming: false,
|
||||
}
|
||||
messages.value = [...messages.value]
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('member_joined', (data: { roomId: string; members: MemberInfo[] }) => {
|
||||
if (data.roomId === currentRoomId.value) {
|
||||
members.value = data.members
|
||||
@@ -129,6 +288,18 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
if (data.roomId === currentRoomId.value) {
|
||||
if (data.status === 'ready') {
|
||||
contextStatuses.value.delete(data.agentName)
|
||||
messages.value = messages.value
|
||||
.map(m => (
|
||||
m.senderName === data.agentName && m.isStreaming
|
||||
? { ...m, isStreaming: false }
|
||||
: m
|
||||
))
|
||||
.filter(m => !(
|
||||
m.senderName === data.agentName &&
|
||||
!m.content?.trim() &&
|
||||
!m.reasoning?.trim() &&
|
||||
!m.tool_calls?.length
|
||||
))
|
||||
} else {
|
||||
contextStatuses.value.set(data.agentName, { agentName: data.agentName, status: data.status })
|
||||
}
|
||||
@@ -137,6 +308,30 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('approval.requested', (data: { roomId: string; agentName?: string; approval_id?: string; command?: string; description?: string; choices?: string[]; allow_permanent?: boolean }) => {
|
||||
if (!data.approval_id) return
|
||||
const choices = (Array.isArray(data.choices) ? data.choices : ['once', 'session', 'deny'])
|
||||
.filter((choice): choice is GroupPendingApproval['choices'][number] =>
|
||||
choice === 'once' || choice === 'session' || choice === 'always' || choice === 'deny')
|
||||
pendingApprovals.value.set(data.approval_id, {
|
||||
roomId: data.roomId,
|
||||
agentName: data.agentName || '',
|
||||
approvalId: data.approval_id,
|
||||
command: data.command || '',
|
||||
description: data.description || '',
|
||||
choices: choices.length ? choices : ['once', 'session', 'deny'],
|
||||
allowPermanent: Boolean(data.allow_permanent),
|
||||
requestedAt: Date.now(),
|
||||
})
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
})
|
||||
|
||||
socket.on('approval.resolved', (data: { approval_id?: string }) => {
|
||||
if (!data.approval_id) return
|
||||
pendingApprovals.value.delete(data.approval_id)
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
})
|
||||
|
||||
socket.on('room_updated', (data: { roomId: string; totalTokens: number }) => {
|
||||
const room = rooms.value.find(r => r.id === data.roomId)
|
||||
if (room) room.totalTokens = data.totalTokens
|
||||
@@ -149,6 +344,7 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
messages.value = []
|
||||
typingUsers.value.clear()
|
||||
contextStatuses.value.clear()
|
||||
pendingApprovals.value.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -163,6 +359,7 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
roomName.value = ''
|
||||
typingUsers.value.clear()
|
||||
contextStatuses.value.clear()
|
||||
pendingApprovals.value.clear()
|
||||
}
|
||||
|
||||
function setUserInfo(name: string, description: string) {
|
||||
@@ -194,7 +391,11 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
const socket = getSocket()
|
||||
if (socket) {
|
||||
await new Promise<void>((resolve) => {
|
||||
socket.emit('join', { roomId, name: userName.value || undefined }, (res: any) => {
|
||||
socket.emit('join', {
|
||||
roomId,
|
||||
name: userName.value || undefined,
|
||||
description: localStorage.getItem('gc_user_description') || undefined,
|
||||
}, (res: any) => {
|
||||
if (!res?.error) {
|
||||
members.value = res.members || []
|
||||
if (res.agents) agents.value = res.agents
|
||||
@@ -222,14 +423,34 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content: string) {
|
||||
async function sendMessage(content: string, attachments?: Attachment[]) {
|
||||
const socket = getSocket()
|
||||
if (!socket || !currentRoomId.value) return
|
||||
emitStopTyping()
|
||||
const messageId = uid()
|
||||
let finalContent: string | ContentBlock[] = content.trim()
|
||||
if (attachments?.length) {
|
||||
const uploaded = await uploadGroupFiles(attachments)
|
||||
finalContent = buildGroupContentBlocks(content, attachments, uploaded)
|
||||
const urlMap = new Map(uploaded.map(f => {
|
||||
return [f.name, getDownloadUrl(normalizeLocalFilePath(f.path), f.name)]
|
||||
}))
|
||||
messages.value.push({
|
||||
id: messageId,
|
||||
roomId: currentRoomId.value,
|
||||
senderId: userId.value,
|
||||
senderName: userName.value || 'You',
|
||||
content: JSON.stringify(finalContent),
|
||||
timestamp: Date.now(),
|
||||
role: 'user',
|
||||
attachments: attachments.map(att => ({ ...att, url: urlMap.get(att.name) || att.url, file: undefined })),
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
socket!.emit('message', { roomId: currentRoomId.value, content }, (res: { id?: string; error?: string }) => {
|
||||
socket!.emit('message', { roomId: currentRoomId.value, id: messageId, content: finalContent }, (res: { id?: string; error?: string }) => {
|
||||
if (res.error) {
|
||||
messages.value = messages.value.filter(m => m.id !== messageId)
|
||||
reject(new Error(res.error))
|
||||
return
|
||||
}
|
||||
@@ -365,6 +586,35 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
if (_typingTimer) { clearTimeout(_typingTimer); _typingTimer = null }
|
||||
}
|
||||
|
||||
async function interruptAgent(agentName: string) {
|
||||
const socket = getSocket()
|
||||
if (!socket || !currentRoomId.value) return
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.emit('interrupt_agent', { roomId: currentRoomId.value, agentName }, (res: any) => {
|
||||
if (res?.error) reject(new Error(res.error))
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function respondApproval(choice: GroupPendingApproval['choices'][number]) {
|
||||
const socket = getSocket()
|
||||
const pending = activePendingApproval.value
|
||||
if (!socket || !pending) return
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.emit('approval.respond', {
|
||||
roomId: pending.roomId,
|
||||
approval_id: pending.approvalId,
|
||||
choice,
|
||||
}, (res: any) => {
|
||||
if (res?.error) reject(new Error(res.error))
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
pendingApprovals.value.delete(pending.approvalId)
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
connected,
|
||||
@@ -378,6 +628,9 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
error,
|
||||
contextStatus,
|
||||
contextStatuses,
|
||||
pendingApprovals,
|
||||
activePendingApproval,
|
||||
autoPlaySpeechEnabled,
|
||||
userId,
|
||||
userName,
|
||||
// Computed
|
||||
@@ -389,11 +642,14 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
connect,
|
||||
disconnect,
|
||||
setUserInfo,
|
||||
setAutoPlaySpeech,
|
||||
joinRoom,
|
||||
sendMessage,
|
||||
loadRooms,
|
||||
emitTyping,
|
||||
emitStopTyping,
|
||||
interruptAgent,
|
||||
respondApproval,
|
||||
createNewRoom,
|
||||
joinByCode,
|
||||
deleteRoom,
|
||||
@@ -404,3 +660,85 @@ export const useGroupChatStore = defineStore('groupChat', () => {
|
||||
removeAgentFromRoom,
|
||||
}
|
||||
})
|
||||
|
||||
function mapGroupMessages(msgs: ChatMessage[]): ChatMessage[] {
|
||||
const toolNameMap = new Map<string, string>()
|
||||
const toolArgsMap = new Map<string, string>()
|
||||
for (const msg of msgs) {
|
||||
if (msg.role === 'assistant' && msg.tool_calls?.length) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (!tc?.id) continue
|
||||
if (tc.function?.name) toolNameMap.set(tc.id, tc.function.name)
|
||||
if (tc.function?.arguments) toolArgsMap.set(tc.id, tc.function.arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: ChatMessage[] = []
|
||||
for (const msg of msgs) {
|
||||
if (
|
||||
msg.role !== 'tool' &&
|
||||
!msg.tool_calls?.length &&
|
||||
!msg.content?.trim() &&
|
||||
!msg.reasoning?.trim() &&
|
||||
(!msg.isStreaming || msg.finish_reason === 'streaming')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === 'assistant' && msg.tool_calls?.length && !msg.content?.trim()) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
result.push({
|
||||
...msg,
|
||||
id: `${msg.id}_${tc.id}`,
|
||||
role: 'tool',
|
||||
content: '',
|
||||
toolName: tc.function?.name || undefined,
|
||||
toolCallId: tc.id,
|
||||
toolArgs: tc.function?.arguments || undefined,
|
||||
toolStatus: 'running',
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === 'tool') {
|
||||
const tcId = msg.tool_call_id || ''
|
||||
const toolName = msg.tool_name || toolNameMap.get(tcId) || undefined
|
||||
const toolArgs = toolArgsMap.get(tcId) || undefined
|
||||
let preview = ''
|
||||
if (msg.content) {
|
||||
try {
|
||||
const parsed = JSON.parse(msg.content)
|
||||
preview = parsed.url || parsed.title || parsed.preview || parsed.summary || ''
|
||||
} catch {
|
||||
preview = msg.content.slice(0, 80)
|
||||
}
|
||||
}
|
||||
const placeholderIdx = result.findIndex(
|
||||
m => m.role === 'tool' && m.toolCallId === tcId && !m.toolResult
|
||||
)
|
||||
const merged: ChatMessage = {
|
||||
...msg,
|
||||
id: placeholderIdx !== -1 ? result[placeholderIdx].id : msg.id,
|
||||
senderId: placeholderIdx !== -1 ? result[placeholderIdx].senderId : msg.senderId,
|
||||
senderName: placeholderIdx !== -1 ? result[placeholderIdx].senderName : msg.senderName,
|
||||
timestamp: placeholderIdx !== -1 ? result[placeholderIdx].timestamp : msg.timestamp,
|
||||
role: 'tool',
|
||||
content: '',
|
||||
toolName: toolName || (placeholderIdx !== -1 ? result[placeholderIdx].toolName : undefined),
|
||||
toolCallId: tcId || undefined,
|
||||
toolArgs: toolArgs || (placeholderIdx !== -1 ? result[placeholderIdx].toolArgs : undefined),
|
||||
toolPreview: typeof preview === 'string' ? preview.slice(0, 100) || undefined : undefined,
|
||||
toolResult: msg.content || undefined,
|
||||
toolStatus: 'done',
|
||||
}
|
||||
if (placeholderIdx !== -1) result[placeholderIdx] = merged
|
||||
else result.push(merged)
|
||||
continue
|
||||
}
|
||||
|
||||
result.push(msg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useModelsStore = defineStore('models', () => {
|
||||
const providers = ref<AvailableModelGroup[]>([])
|
||||
const allProviders = ref<AvailableModelGroup[]>([])
|
||||
const defaultModel = ref('')
|
||||
const defaultProvider = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const customProviders = computed(() =>
|
||||
@@ -26,7 +27,7 @@ export const useModelsStore = defineStore('models', () => {
|
||||
provider: g.provider,
|
||||
label: g.label,
|
||||
base_url: g.base_url,
|
||||
isDefault: m === defaultModel.value,
|
||||
isDefault: m === defaultModel.value && g.provider === defaultProvider.value,
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -39,6 +40,7 @@ export const useModelsStore = defineStore('models', () => {
|
||||
providers.value = res.groups
|
||||
allProviders.value = res.allProviders
|
||||
defaultModel.value = res.default
|
||||
defaultProvider.value = res.default_provider || ''
|
||||
const appStore = useAppStore()
|
||||
appStore.applyAvailableModelsResponse(res)
|
||||
} catch (err) {
|
||||
@@ -51,6 +53,7 @@ export const useModelsStore = defineStore('models', () => {
|
||||
async function setDefaultModel(modelId: string, provider: string) {
|
||||
await systemApi.updateDefaultModel({ default: modelId, provider })
|
||||
defaultModel.value = modelId
|
||||
defaultProvider.value = provider
|
||||
const appStore = useAppStore()
|
||||
appStore.reloadModels()
|
||||
}
|
||||
@@ -69,6 +72,7 @@ export const useModelsStore = defineStore('models', () => {
|
||||
providers,
|
||||
allProviders,
|
||||
defaultModel,
|
||||
defaultProvider,
|
||||
loading,
|
||||
customProviders,
|
||||
builtinProviders,
|
||||
|
||||
@@ -83,10 +83,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSection(section: string, values: Record<string, any>) {
|
||||
async function saveSection(section: string, values: Record<string, any>, options?: { restart?: boolean }) {
|
||||
saving.value = true
|
||||
try {
|
||||
await configApi.updateConfigSection(section, values)
|
||||
await configApi.updateConfigSection(section, values, options)
|
||||
switch (section) {
|
||||
case 'display': display.value = { ...display.value, ...values }; break
|
||||
case 'agent': agent.value = { ...agent.value, ...values }; break
|
||||
|
||||
Reference in New Issue
Block a user