[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:
ekko
2026-05-25 19:26:23 +08:00
committed by GitHub
parent 0eab6a1125
commit badb17cf8e
30 changed files with 1535 additions and 85 deletions
+122 -29
View File
@@ -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