From 7e9df04bf82e5fbf85e1b0b6ff269943da35976c Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 12 Jun 2026 14:01:55 +0800 Subject: [PATCH] =?UTF-8?q?Web=E4=BC=98=E5=8C=96=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/composables/useAcpStream.ts | 14 +++--- web/src/composables/useConversationSession.ts | 45 ++++++++++-------- web/src/stores/ui.ts | 19 ++++++-- web/src/views/acp/AcpSessionDetailView.vue | 46 ++++++++++++------- web/src/views/auth/LoginView.vue | 5 +- web/src/views/chat/ChatDetailView.vue | 12 +++-- .../chat/components/AttachmentUploader.vue | 3 +- .../components/RequirementChatPanel.vue | 15 ++++-- .../components/RequirementHistoryPanel.vue | 40 ++++++++++------ .../views/workspaces/WorkspaceHomeView.vue | 12 ++--- 10 files changed, 138 insertions(+), 73 deletions(-) diff --git a/web/src/composables/useAcpStream.ts b/web/src/composables/useAcpStream.ts index 5a37584..4e095f8 100644 --- a/web/src/composables/useAcpStream.ts +++ b/web/src/composables/useAcpStream.ts @@ -17,8 +17,8 @@ export interface UseAcpStreamReturn { events: Ref pendingPermissions: Ref errorMsg: Ref - prompt: (text: string, agentSessionID: string) => void - cancel: (agentSessionID: string) => void + prompt: (text: string, agentSessionID: string) => boolean + cancel: (agentSessionID: string) => boolean reconnect: () => void close: () => void } @@ -181,21 +181,23 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn { } function send(method: string, params: Record) { - if (!ws || ws.readyState !== WebSocket.OPEN) return - const id = 'client-' + crypto.randomUUID() + if (!ws || ws.readyState !== WebSocket.OPEN) return false + const uuid = crypto.randomUUID?.() ?? Math.random().toString(36).slice(2) + const id = 'client-' + uuid const msg = { jsonrpc: '2.0', id, method, params } ws.send(JSON.stringify(msg)) + return true } function prompt(text: string, agentSessionID: string) { - send('session/prompt', { + return send('session/prompt', { sessionId: agentSessionID, prompt: [{ type: 'text', text }], }) } function cancel(agentSessionID: string) { - send('session/cancel', { sessionId: agentSessionID }) + return send('session/cancel', { sessionId: agentSessionID }) } function reconnect() { diff --git a/web/src/composables/useConversationSession.ts b/web/src/composables/useConversationSession.ts index 8f41615..0e090f8 100644 --- a/web/src/composables/useConversationSession.ts +++ b/web/src/composables/useConversationSession.ts @@ -74,31 +74,40 @@ export function useConversationSession() { if (!currentConversation.value) return if (streamingMessageId.value !== null) return const conv = currentConversation.value - const resp = await chatApi.sendMessage(conv.id, { - content, - attachment_ids: attachmentIds, - }) const now = new Date().toISOString() - messages.value.push({ - id: resp.user_message_id, + const userMsg: ChatMessage = { + id: -1, conversation_id: conv.id, role: 'user', content, - status: 'ok', - created_at: now, - updated_at: now, - }) - messages.value.push({ - id: resp.assistant_message_id, - conversation_id: conv.id, - role: 'assistant', - content: '', status: 'pending', created_at: now, updated_at: now, - }) - streamingMessageId.value = resp.assistant_message_id - attachStream(resp.stream_url, resp.user_message_id) + } + messages.value.push(userMsg) + try { + const resp = await chatApi.sendMessage(conv.id, { + content, + attachment_ids: attachmentIds, + }) + userMsg.id = resp.user_message_id + userMsg.status = 'ok' + messages.value.push({ + id: resp.assistant_message_id, + conversation_id: conv.id, + role: 'assistant', + content: '', + status: 'pending', + created_at: now, + updated_at: now, + }) + streamingMessageId.value = resp.assistant_message_id + attachStream(resp.stream_url, resp.user_message_id) + } catch (e) { + userMsg.status = 'error' + userMsg.error_message = e instanceof Error ? e.message : 'send failed' + throw e + } } async function cancelMessage() { diff --git a/web/src/stores/ui.ts b/web/src/stores/ui.ts index 8b51b2d..59dcbec 100644 --- a/web/src/stores/ui.ts +++ b/web/src/stores/ui.ts @@ -28,8 +28,10 @@ export const useUIStore = defineStore('ui', () => { ? window.matchMedia('(prefers-color-scheme: dark)') : null const systemDark = ref(mediaQuery ? mediaQuery.matches : false) + let mediaListener: ((e: MediaQueryListEvent) => void) | null = null if (mediaQuery) { - mediaQuery.addEventListener('change', (e) => (systemDark.value = e.matches)) + mediaListener = (e: MediaQueryListEvent) => (systemDark.value = e.matches) + mediaQuery.addEventListener('change', mediaListener) } const effectiveDark = computed(() => @@ -38,10 +40,21 @@ export const useUIStore = defineStore('ui', () => { watch( effectiveDark, - (v) => document.documentElement.classList.toggle('dark', v), + (v) => { + if (typeof document !== 'undefined') { + document.documentElement.classList.toggle('dark', v) + } + }, { immediate: true }, ) + function cleanupMediaQuery() { + if (mediaQuery && mediaListener) { + mediaQuery.removeEventListener('change', mediaListener) + mediaListener = null + } + } + function setThemeMode(mode: ThemeMode) { themeMode.value = mode safeWrite('themeMode', mode) @@ -50,5 +63,5 @@ export const useUIStore = defineStore('ui', () => { const sidebarCollapsed = ref(safeReadString('sidebarCollapsed') === 'true') watch(sidebarCollapsed, (v) => safeWrite('sidebarCollapsed', String(v))) - return { themeMode, effectiveDark, setThemeMode, sidebarCollapsed } + return { themeMode, effectiveDark, setThemeMode, sidebarCollapsed, cleanupMediaQuery } }) diff --git a/web/src/views/acp/AcpSessionDetailView.vue b/web/src/views/acp/AcpSessionDetailView.vue index 84ed51f..1dbb07a 100644 --- a/web/src/views/acp/AcpSessionDetailView.vue +++ b/web/src/views/acp/AcpSessionDetailView.vue @@ -72,21 +72,33 @@ 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 +const lastPromptIndex = ref(-1) +const hasResponseAfterPrompt = ref(false) +const isPrompting = computed(() => lastPromptIndex.value >= 0 && !hasResponseAfterPrompt.value) + +watch( + () => events.value.length, + () => { + const evs = events.value + if (lastPromptIndex.value < 0) { + for (let i = evs.length - 1; i >= 0; i--) { + if (evs[i].method === 'session/prompt' && evs[i].direction === 'in') { + lastPromptIndex.value = i + hasResponseAfterPrompt.value = false + break + } + } } - } - if (lastPrompt < 0) return false - for (let i = lastPrompt + 1; i < evs.length; i++) { - if (evs[i].kind === 'response') return false - } - return true -}) + if (lastPromptIndex.value >= 0) { + for (let i = lastPromptIndex.value + 1; i < evs.length; i++) { + if (evs[i].kind === 'response') { + hasResponseAfterPrompt.value = true + break + } + } + } + }, +) function onPrompt(text: string) { const sess = store.currentSession @@ -95,13 +107,15 @@ function onPrompt(text: string) { return } errorMsg.value = null - stream.value?.prompt(text, sess.agent_session_id) + const ok = stream.value?.prompt(text, sess.agent_session_id) + if (!ok) errorMsg.value = '发送失败:连接未就绪' } function onCancel() { const sess = store.currentSession if (!sess?.agent_session_id) return - stream.value?.cancel(sess.agent_session_id) + const ok = stream.value?.cancel(sess.agent_session_id) + if (!ok) errorMsg.value = '取消失败:连接未就绪' } async function terminate() { diff --git a/web/src/views/auth/LoginView.vue b/web/src/views/auth/LoginView.vue index 19458b5..7d39733 100644 --- a/web/src/views/auth/LoginView.vue +++ b/web/src/views/auth/LoginView.vue @@ -22,6 +22,7 @@ type="primary" attr-type="submit" :loading="loading" + :disabled="!isValid" block > 登录 @@ -38,7 +39,7 @@