Update CLI chat session bridge (#697)
* feat: add CLI chat sessions with Python agent bridge Introduce a new CLI chat mode that connects Web UI directly to Hermes Agent's AIAgent via a Python bridge subprocess and Socket.IO, bypassing the API Server /v1/responses path. Supports streaming, slash commands (/new, /undo, /retry, /branch, /compress, /save, /title), interrupt, and steer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: update CLI chat session bridge * fix: extend agent bridge startup timeouts * docs: update bridge chat session design * feat: align bridge compression and provider registry * chore: bump version to 0.5.20 --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, type RunEvent, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, type RunEvent, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { getApiKey } from '@/api/client'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -43,6 +43,16 @@ export interface Message {
|
||||
queued?: boolean
|
||||
}
|
||||
|
||||
export interface PendingApproval {
|
||||
sessionId: string
|
||||
approvalId: string
|
||||
command: string
|
||||
description: string
|
||||
choices: Array<'once' | 'session' | 'always' | 'deny'>
|
||||
allowPermanent: boolean
|
||||
requestedAt: number
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
title: string
|
||||
@@ -320,6 +330,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const queueLengths = ref<Map<string, number>>(new Map())
|
||||
/** sessionId → queued user messages not yet visible in the transcript */
|
||||
const queuedUserMessages = ref<Map<string, Message[]>>(new Map())
|
||||
const pendingApprovals = ref<Map<string, PendingApproval>>(new Map())
|
||||
const activePendingApproval = computed(() => {
|
||||
const sid = activeSessionId.value
|
||||
return sid ? pendingApprovals.value.get(sid) || null : null
|
||||
})
|
||||
|
||||
// 自动播放语音开关
|
||||
const autoPlaySpeechEnabled = ref(false)
|
||||
@@ -432,6 +447,30 @@ export const useChatStore = defineStore('chat', () => {
|
||||
return session
|
||||
}
|
||||
|
||||
function newCliSession(): Session {
|
||||
const now = new Date()
|
||||
const ts = [
|
||||
now.getFullYear(),
|
||||
String(now.getMonth() + 1).padStart(2, '0'),
|
||||
String(now.getDate()).padStart(2, '0'),
|
||||
'_',
|
||||
String(now.getHours()).padStart(2, '0'),
|
||||
String(now.getMinutes()).padStart(2, '0'),
|
||||
String(now.getSeconds()).padStart(2, '0'),
|
||||
].join('')
|
||||
const hex = Math.random().toString(16).slice(2, 8)
|
||||
const session: Session = {
|
||||
id: `${ts}_${hex}`,
|
||||
title: '',
|
||||
source: 'cli',
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
sessions.value.unshift(session)
|
||||
return session
|
||||
}
|
||||
|
||||
async function switchSession(sessionId: string, focusId?: string | null) {
|
||||
clearThinkingObservationFor(sessionId)
|
||||
activeSessionId.value = sessionId
|
||||
@@ -503,6 +542,49 @@ export const useChatStore = defineStore('chat', () => {
|
||||
setAbortState({ aborting: true, synced: null })
|
||||
} else if (e.event === 'abort.completed') {
|
||||
setAbortState({ aborting: false, synced: e.synced ?? false })
|
||||
} else if (e.event === 'approval.requested') {
|
||||
setPendingApproval({ ...e, session_id: sessionId } as RunEvent)
|
||||
} else if (e.event === 'approval.resolved') {
|
||||
clearPendingApproval({ ...e, session_id: sessionId } as RunEvent)
|
||||
} else if (e.event === 'tool.started') {
|
||||
const msgs = getSessionMsgs(sessionId)
|
||||
const toolCallId = e.tool_call_id as string | undefined
|
||||
const existingTool = toolCallId
|
||||
? msgs.find(m => m.role === 'tool' && m.toolCallId === toolCallId)
|
||||
: null
|
||||
if (existingTool) {
|
||||
updateMessage(sessionId, existingTool.id, {
|
||||
toolName: e.tool || e.name,
|
||||
toolArgs: typeof e.arguments === 'string' ? e.arguments : existingTool.toolArgs,
|
||||
toolPreview: e.preview || existingTool.toolPreview,
|
||||
toolStatus: existingTool.toolStatus || 'running',
|
||||
})
|
||||
} else {
|
||||
addMessage(sessionId, {
|
||||
id: uid(),
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
toolName: e.tool || e.name,
|
||||
toolCallId,
|
||||
toolPreview: e.preview,
|
||||
toolArgs: typeof e.arguments === 'string' ? e.arguments : undefined,
|
||||
toolStatus: 'running',
|
||||
})
|
||||
}
|
||||
} else if (e.event === 'tool.completed') {
|
||||
const msgs = getSessionMsgs(sessionId)
|
||||
const toolCallId = e.tool_call_id as string | undefined
|
||||
const toolMsgs = toolCallId
|
||||
? msgs.filter(m => m.role === 'tool' && m.toolCallId === toolCallId)
|
||||
: msgs.filter(m => m.role === 'tool' && m.toolStatus === 'running')
|
||||
if (toolMsgs.length > 0) {
|
||||
updateMessage(sessionId, toolMsgs[toolMsgs.length - 1].id, {
|
||||
toolStatus: e.error === true ? 'error' : 'done',
|
||||
toolDuration: e.duration,
|
||||
toolResult: typeof e.output === 'string' ? e.output : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,6 +685,45 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function setPendingApproval(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
const approvalId = (evt as any).approval_id as string | undefined
|
||||
if (!sid || !approvalId) return
|
||||
const rawChoices = Array.isArray((evt as any).choices) ? (evt as any).choices : ['once', 'session', 'deny']
|
||||
const choices = rawChoices
|
||||
.filter((choice: unknown): choice is PendingApproval['choices'][number] =>
|
||||
choice === 'once' || choice === 'session' || choice === 'always' || choice === 'deny')
|
||||
pendingApprovals.value.set(sid, {
|
||||
sessionId: sid,
|
||||
approvalId,
|
||||
command: String((evt as any).command || ''),
|
||||
description: String((evt as any).description || ''),
|
||||
choices: choices.length ? choices : ['once', 'session', 'deny'],
|
||||
allowPermanent: Boolean((evt as any).allow_permanent),
|
||||
requestedAt: Date.now(),
|
||||
})
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
}
|
||||
|
||||
function clearPendingApproval(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
if (!sid) return
|
||||
const current = pendingApprovals.value.get(sid)
|
||||
if (!current) return
|
||||
const approvalId = (evt as any).approval_id
|
||||
if (approvalId && current.approvalId !== approvalId) return
|
||||
pendingApprovals.value.delete(sid)
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
}
|
||||
|
||||
function respondApproval(choice: PendingApproval['choices'][number]) {
|
||||
const pending = activePendingApproval.value
|
||||
if (!pending) return
|
||||
respondToolApproval(pending.sessionId, pending.approvalId, choice)
|
||||
pendingApprovals.value.delete(pending.sessionId)
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
}
|
||||
|
||||
function showNextQueuedUserMessage(sessionId: string) {
|
||||
const queue = queuedUserMessages.value.get(sessionId)
|
||||
if (!queue?.length) return
|
||||
@@ -715,6 +836,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
session_id: sid,
|
||||
model: sessionModel || undefined,
|
||||
queue_id: userMsg.id,
|
||||
source: (activeSession.value?.source === 'cli' ? 'cli' : 'api_server') as 'cli' | 'api_server',
|
||||
}
|
||||
|
||||
if (shouldQueue) {
|
||||
@@ -967,6 +1089,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'approval.requested': {
|
||||
setPendingApproval(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'approval.resolved': {
|
||||
clearPendingApproval(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'run.completed': {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = activeAssistantMessageId
|
||||
@@ -1394,6 +1526,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'approval.requested': {
|
||||
setPendingApproval(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'approval.resolved': {
|
||||
clearPendingApproval(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'run.completed': {
|
||||
const hasQueue = (evt as any).queue_remaining > 0
|
||||
if (hasQueue) {
|
||||
@@ -1689,12 +1831,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
isAborting,
|
||||
queueLengths,
|
||||
queuedUserMessages,
|
||||
pendingApprovals,
|
||||
activePendingApproval,
|
||||
removeQueuedMessage,
|
||||
isLoadingSessions,
|
||||
sessionsLoaded,
|
||||
isLoadingMessages,
|
||||
|
||||
newChat,
|
||||
newCliSession,
|
||||
switchSession,
|
||||
switchSessionModel,
|
||||
addOrUpdateSession,
|
||||
@@ -1702,6 +1847,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
deleteSession,
|
||||
sendMessage,
|
||||
stopStreaming,
|
||||
respondApproval,
|
||||
loadSessions,
|
||||
refreshActiveSession,
|
||||
getThinkingObservation,
|
||||
|
||||
Reference in New Issue
Block a user