[codex] integrate goal command workflow (#1025)
* feat: integrate goal command workflow * fix: keep goal done visible * fix: add goal done slash command * fix: promote queued message on run start
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, onPeerUserMessage, respondClarify, type RunEvent, type ResumeSessionPayload, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, onPeerUserMessage, onSessionCommand, respondClarify, type RunEvent, type ResumeSessionPayload, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, setSessionModel, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||
import { getActiveProfileName } from '@/api/client'
|
||||
import { getDownloadUrl } from '@/api/hermes/download'
|
||||
@@ -365,6 +365,7 @@ function removeItem(key: string) {
|
||||
// File objects don't serialize and we only need name/type/size/url for display.
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const seenSessionCommandEvents = new WeakSet<RunEvent>()
|
||||
const sessions = ref<Session[]>([])
|
||||
const activeSessionId = ref<string | null>(null)
|
||||
const focusMessageId = ref<string | null>(null)
|
||||
@@ -778,6 +779,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function clearAgentEventMessages(sessionId: string) {
|
||||
const s = sessions.value.find(s => s.id === sessionId)
|
||||
if (!s) return
|
||||
s.messages = s.messages.filter(m => m.commandAction !== 'agent.event')
|
||||
}
|
||||
|
||||
function handleSubagentEvent(sessionId: string, evt: RunEvent) {
|
||||
const eventName = String(evt.event || '')
|
||||
if (!eventName.startsWith('subagent.')) return
|
||||
@@ -867,12 +874,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
function handleSessionCommandEvent(evt: RunEvent) {
|
||||
if (seenSessionCommandEvents.has(evt)) return
|
||||
seenSessionCommandEvents.add(evt)
|
||||
|
||||
const sid = evt.session_id
|
||||
if (!sid) return
|
||||
const target = sessions.value.find(s => s.id === sid)
|
||||
const action = (evt as any).action as string | undefined
|
||||
const command = String((evt as any).command || '').toLowerCase()
|
||||
if ((evt as any).started === true && (evt as any).terminal === false) {
|
||||
serverWorking.value.add(sid)
|
||||
}
|
||||
|
||||
if (action === 'clear') {
|
||||
if (action === 'clear' && command === 'clear') {
|
||||
if (target) target.messages = []
|
||||
queuedUserMessages.value.delete(sid)
|
||||
queueLengths.value.delete(sid)
|
||||
@@ -931,6 +945,36 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAgentEvent(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
if (!sid) return
|
||||
const text = String((evt as any).text || (evt as any).message || '').trim()
|
||||
if (!text) return
|
||||
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const last = msgs[msgs.length - 1]
|
||||
const commandData = { ...(evt as any) }
|
||||
if (last?.role === 'system' && last.commandAction === 'agent.event') {
|
||||
if (last.content === text) return
|
||||
updateMessage(sid, last.id, {
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
commandData,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addMessage(sid, {
|
||||
id: uid(),
|
||||
role: 'system',
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
systemType: 'command',
|
||||
commandAction: 'agent.event',
|
||||
commandData,
|
||||
})
|
||||
}
|
||||
|
||||
function enqueueUserMessage(sessionId: string, message: Message) {
|
||||
const queue = queuedUserMessages.value.get(sessionId) || []
|
||||
if (queue.some(item => item.id === message.id)) return
|
||||
@@ -957,6 +1001,23 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function promoteNextQueuedUserMessage(sessionId: string) {
|
||||
const queue = queuedUserMessages.value.get(sessionId)
|
||||
if (!queue?.length) return
|
||||
const [next, ...rest] = queue
|
||||
const nextMap = new Map(queuedUserMessages.value)
|
||||
if (rest.length > 0) {
|
||||
nextMap.set(sessionId, rest)
|
||||
} else {
|
||||
nextMap.delete(sessionId)
|
||||
}
|
||||
queuedUserMessages.value = nextMap
|
||||
if (!getSessionMsgs(sessionId).some(message => message.id === next.id)) {
|
||||
addMessage(sessionId, { ...next, queued: false })
|
||||
updateSessionTitle(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeQueuedUserMessages(rawMessages: unknown): Message[] {
|
||||
if (!Array.isArray(rawMessages)) return []
|
||||
return rawMessages.flatMap((raw) => {
|
||||
@@ -1004,7 +1065,27 @@ export const useChatStore = defineStore('chat', () => {
|
||||
queueLengths.value.delete(sessionId)
|
||||
}
|
||||
|
||||
if (Array.isArray((evt as any).queued_messages) && !(evt as any).dequeued_queue_id) {
|
||||
const dequeuedId = (evt as any).dequeued_queue_id != null
|
||||
? String((evt as any).dequeued_queue_id)
|
||||
: ''
|
||||
if (dequeuedId) {
|
||||
const existingQueue = queuedUserMessages.value.get(sessionId) || []
|
||||
const dequeued = existingQueue.find(message => message.id === dequeuedId)
|
||||
if (Array.isArray((evt as any).queued_messages)) {
|
||||
const queued = normalizeQueuedUserMessages((evt as any).queued_messages)
|
||||
replaceQueuedUserMessages(sessionId, queued)
|
||||
} else {
|
||||
const nextQueue = existingQueue.filter(message => message.id !== dequeuedId)
|
||||
replaceQueuedUserMessages(sessionId, nextQueue)
|
||||
}
|
||||
if (dequeued && !getSessionMsgs(sessionId).some(message => message.id === dequeued.id)) {
|
||||
addMessage(sessionId, { ...dequeued, queued: false })
|
||||
updateSessionTitle(sessionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray((evt as any).queued_messages)) {
|
||||
const queued = normalizeQueuedUserMessages((evt as any).queued_messages)
|
||||
replaceQueuedUserMessages(sessionId, queued)
|
||||
return
|
||||
@@ -1129,21 +1210,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
pendingApprovals.value = new Map(pendingApprovals.value)
|
||||
}
|
||||
|
||||
function showNextQueuedUserMessage(sessionId: string) {
|
||||
const queue = queuedUserMessages.value.get(sessionId)
|
||||
if (!queue?.length) return
|
||||
const [next, ...rest] = queue
|
||||
const nextMap = new Map(queuedUserMessages.value)
|
||||
if (rest.length > 0) {
|
||||
nextMap.set(sessionId, rest)
|
||||
} else {
|
||||
nextMap.delete(sessionId)
|
||||
}
|
||||
queuedUserMessages.value = nextMap
|
||||
addMessage(sessionId, { ...next, queued: false })
|
||||
updateSessionTitle(sessionId)
|
||||
}
|
||||
|
||||
function updateSessionTitle(sessionId: string) {
|
||||
const target = sessions.value.find(s => s.id === sessionId)
|
||||
if (!target) return
|
||||
@@ -1189,6 +1255,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const isBridgeSlashCommand = content.trim().startsWith('/')
|
||||
const isBridgeCompressCommand = isBridgeSlashCommand && /^\/compress(?:\s|$)/i.test(content.trim())
|
||||
const isBridgePlanCommand = isBridgeSlashCommand && /^\/plan(?:\s|$)/i.test(content.trim())
|
||||
const isBridgeGoalCommand = isBridgeSlashCommand && /^\/goal(?:\s|$)/i.test(content.trim())
|
||||
const wasLiveBeforeSend = isSessionLive(sid)
|
||||
const shouldQueue = wasLiveBeforeSend && (!isBridgeSlashCommand || isBridgePlanCommand)
|
||||
|
||||
@@ -1283,10 +1350,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
let runHadToolActivity = false
|
||||
let activeAssistantMessageId: string | null = null
|
||||
|
||||
const startNextQueuedUser = () => {
|
||||
showNextQueuedUserMessage(sid)
|
||||
}
|
||||
|
||||
const closeStreamingAssistant = () => {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
msgs.forEach(m => {
|
||||
@@ -1386,12 +1449,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'run.failed':
|
||||
addAgentErrorMessage(sid, e.error)
|
||||
break
|
||||
case 'agent.event':
|
||||
handleAgentEvent(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeSessionId.value === sid) activeSession.value = target
|
||||
if (!data.isWorking && !(data.queueLength && data.queueLength > 0)) {
|
||||
clearAgentEventMessages(sid)
|
||||
cleanup()
|
||||
activeAssistantMessageId = null
|
||||
updateSessionTitle(sid)
|
||||
@@ -1405,12 +1472,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
(evt: RunEvent) => {
|
||||
switch (evt.event) {
|
||||
case 'run.started':
|
||||
clearAgentEventMessages(sid)
|
||||
setAbortState(null)
|
||||
setCompressionState(null)
|
||||
runProducedAssistantText = false
|
||||
runHadToolActivity = false
|
||||
closeStreamingAssistant()
|
||||
startNextQueuedUser()
|
||||
promoteNextQueuedUserMessage(sid)
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
} else {
|
||||
@@ -1428,6 +1496,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'agent.event': {
|
||||
handleAgentEvent(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'compression.started': {
|
||||
setCompressionState({
|
||||
compressing: true,
|
||||
@@ -1656,6 +1729,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
case 'run.completed': {
|
||||
clearAgentEventMessages(sid)
|
||||
const msgs = getSessionMsgs(sid)
|
||||
const lastMsg = activeAssistantMessageId
|
||||
? msgs.find(m => m.id === activeAssistantMessageId)
|
||||
@@ -1759,6 +1833,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
case 'run.failed': {
|
||||
clearAgentEventMessages(sid)
|
||||
if ((evt as any).inputTokens != null) {
|
||||
const target = sessions.value.find(s => s.id === sid)
|
||||
if (target) {
|
||||
@@ -1819,7 +1894,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
{ onReconnectResume: applyReconnectResume },
|
||||
)
|
||||
|
||||
if (!isBridgeSlashCommand || isBridgeCompressCommand || isBridgePlanCommand) {
|
||||
if (!isBridgeSlashCommand || isBridgeCompressCommand || isBridgePlanCommand || isBridgeGoalCommand) {
|
||||
streamStates.value.set(sid, ctrl)
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -1857,10 +1932,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
unregisterSessionHandlers(sid)
|
||||
}
|
||||
|
||||
const startNextQueuedUser = () => {
|
||||
showNextQueuedUserMessage(sid)
|
||||
}
|
||||
|
||||
const closeStreamingAssistant = () => {
|
||||
const msgs = getSessionMsgs(sid)
|
||||
msgs.forEach(m => {
|
||||
@@ -1887,13 +1958,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'agent.event': {
|
||||
handleAgentEvent(evt)
|
||||
break
|
||||
}
|
||||
|
||||
case 'run.started':
|
||||
clearAgentEventMessages(sid)
|
||||
setAbortState(null)
|
||||
setCompressionState(null)
|
||||
runProducedAssistantText = false
|
||||
runHadToolActivity = false
|
||||
closeStreamingAssistant()
|
||||
startNextQueuedUser()
|
||||
promoteNextQueuedUserMessage(sid)
|
||||
if ((evt as any).queue_length > 0) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_length)
|
||||
} else {
|
||||
@@ -2118,6 +2195,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
case 'run.completed': {
|
||||
clearAgentEventMessages(sid)
|
||||
const hasQueue = (evt as any).queue_remaining > 0
|
||||
if (hasQueue) {
|
||||
queueLengths.value.set(sid, (evt as any).queue_remaining)
|
||||
@@ -2207,6 +2285,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
case 'run.failed': {
|
||||
clearAgentEventMessages(sid)
|
||||
if ((evt as any).inputTokens != null) {
|
||||
const target = sessions.value.find(s => s.id === sid)
|
||||
if (target) {
|
||||
@@ -2263,6 +2342,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
onAbortStarted: (evt) => handleEvent(evt),
|
||||
onAbortCompleted: (evt) => handleEvent(evt),
|
||||
onUsageUpdated: (evt) => handleEvent(evt),
|
||||
onAgentEvent: (evt) => handleEvent(evt),
|
||||
onSessionCommand: (evt) => handleEvent(evt),
|
||||
onRunQueued: (evt) => handleEvent(evt),
|
||||
onClarifyRequested: (evt) => handleEvent(evt),
|
||||
@@ -2326,6 +2406,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
onPeerUserMessage(handlePeerUserMessage)
|
||||
|
||||
function handleGlobalSessionCommand(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
if (!sid || activeSessionId.value !== sid || !activeSession.value) return
|
||||
const shouldAttachToStartedRun = (evt as any).started === true && (evt as any).terminal === false
|
||||
handleSessionCommandEvent(evt)
|
||||
if (shouldAttachToStartedRun) {
|
||||
serverWorking.value.add(sid)
|
||||
resumeServerWorkingRun(sid, true)
|
||||
}
|
||||
}
|
||||
|
||||
onSessionCommand(handleGlobalSessionCommand)
|
||||
|
||||
function stopStreaming() {
|
||||
const sid = activeSessionId.value
|
||||
if (!sid) return
|
||||
|
||||
Reference in New Issue
Block a user