You've already forked agentic-coding-workflow
feat(web): AcpSessionDetailView (core: status + stream + prompt)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAcpStore } from '@/stores/acp'
|
||||
import { useAcpStream, type UseAcpStreamReturn } from '@/composables/useAcpStream'
|
||||
import type { AcpEvent } from '@/api/acp'
|
||||
import SessionStatusBadge from '@/components/acp/SessionStatusBadge.vue'
|
||||
import AgentEventCard from '@/components/acp/AgentEventCard.vue'
|
||||
import PromptInput from '@/components/acp/PromptInput.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useAcpStore()
|
||||
|
||||
const sessionId = computed(() => route.params.sessionId as string)
|
||||
|
||||
const stream = shallowRef<UseAcpStreamReturn | null>(null)
|
||||
const eventsContainer = ref<HTMLElement | null>(null)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
|
||||
const events = computed<AcpEvent[]>(() => stream.value?.events.value ?? [])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await store.getSession(sessionId.value)
|
||||
} catch (e) {
|
||||
errorMsg.value = e instanceof Error ? e.message : 'failed to load session'
|
||||
return
|
||||
}
|
||||
stream.value = useAcpStream(sessionId.value)
|
||||
watch(
|
||||
() => events.value.length,
|
||||
async () => {
|
||||
await nextTick()
|
||||
if (eventsContainer.value) {
|
||||
eventsContainer.value.scrollTop = eventsContainer.value.scrollHeight
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stream.value?.close()
|
||||
})
|
||||
|
||||
const isPrompting = computed(() => {
|
||||
const evs = events.value
|
||||
let lastPrompt = -1
|
||||
for (let i = evs.length - 1; i >= 0; i--) {
|
||||
if (evs[i].method === 'session/prompt' && evs[i].direction === 'in') {
|
||||
lastPrompt = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (lastPrompt < 0) return false
|
||||
for (let i = lastPrompt + 1; i < evs.length; i++) {
|
||||
if (evs[i].kind === 'response') return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
function onPrompt(text: string) {
|
||||
const sess = store.currentSession
|
||||
if (!sess?.agent_session_id) {
|
||||
errorMsg.value = 'Agent not ready yet'
|
||||
return
|
||||
}
|
||||
errorMsg.value = null
|
||||
stream.value?.prompt(text, sess.agent_session_id)
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
const sess = store.currentSession
|
||||
if (!sess?.agent_session_id) return
|
||||
stream.value?.cancel(sess.agent_session_id)
|
||||
}
|
||||
|
||||
async function terminate() {
|
||||
if (!confirm('Terminate this session?')) return
|
||||
try {
|
||||
await store.terminateSession(sessionId.value)
|
||||
} catch (e) {
|
||||
errorMsg.value = e instanceof Error ? e.message : 'terminate failed'
|
||||
}
|
||||
}
|
||||
|
||||
const sessionStatus = computed(
|
||||
() => stream.value?.sessionStatus.value ?? store.currentSession?.status ?? 'starting',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Top bar -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b">
|
||||
<div class="space-x-3 text-sm">
|
||||
<span class="font-mono">{{ store.currentSession?.branch }}</span>
|
||||
<span class="text-gray-500">·</span>
|
||||
<span
|
||||
class="text-xs text-gray-500 font-mono truncate max-w-md inline-block align-middle"
|
||||
>
|
||||
{{ store.currentSession?.cwd_path }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionStatusBadge :status="sessionStatus" />
|
||||
<button
|
||||
v-if="sessionStatus === 'starting' || sessionStatus === 'running'"
|
||||
class="px-2 py-1 text-xs rounded border text-red-600 hover:bg-red-50"
|
||||
@click="terminate"
|
||||
>
|
||||
Terminate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMsg" class="px-4 py-2 text-sm text-red-600 bg-red-50">
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
|
||||
<!-- Events stream -->
|
||||
<div
|
||||
ref="eventsContainer"
|
||||
class="flex-1 overflow-y-auto px-4 py-3 space-y-1"
|
||||
>
|
||||
<AgentEventCard
|
||||
v-for="ev in events"
|
||||
:key="ev.id"
|
||||
:event="ev"
|
||||
/>
|
||||
<div
|
||||
v-if="events.length === 0"
|
||||
class="text-center text-gray-400 py-8"
|
||||
>
|
||||
Waiting for agent...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt input -->
|
||||
<div class="border-t px-4 py-3">
|
||||
<PromptInput
|
||||
:disabled="sessionStatus === 'crashed' || sessionStatus === 'exited'"
|
||||
:is-prompting="isPrompting"
|
||||
@send="onPrompt"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user