You've already forked agentic-coding-workflow
Web优化 1
This commit is contained in:
@@ -17,8 +17,8 @@ export interface UseAcpStreamReturn {
|
||||
events: Ref<AcpEvent[]>
|
||||
pendingPermissions: Ref<PermissionRequest[]>
|
||||
errorMsg: Ref<string | null>
|
||||
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<string, unknown>) {
|
||||
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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+16
-3
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
type="primary"
|
||||
attr-type="submit"
|
||||
:loading="loading"
|
||||
:disabled="!isValid"
|
||||
block
|
||||
>
|
||||
登录
|
||||
@@ -38,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { NForm, NFormItem, NInput, NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { authApi } from '@/api/auth'
|
||||
@@ -50,6 +51,8 @@ const password = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const isValid = computed(() => email.value.trim().length > 0 && password.value.length > 0)
|
||||
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
@@ -215,12 +215,18 @@ watch(
|
||||
)
|
||||
|
||||
// 流式输出时 messages.length 不变,只 content 增长 —— 同时观察末消息内容长度。
|
||||
let scrollPending = false
|
||||
watch(
|
||||
() => [chat.messages.length, chat.messages.at(-1)?.content?.length ?? 0],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
const el = scrollEl.value
|
||||
if (el) el.scrollTo(0, el.scrollHeight)
|
||||
if (scrollPending) return
|
||||
scrollPending = true
|
||||
requestAnimationFrame(() => {
|
||||
scrollPending = false
|
||||
nextTick(() => {
|
||||
const el = scrollEl.value
|
||||
if (el) el.scrollTo(0, el.scrollHeight)
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -82,9 +82,8 @@ function remove(id: string) {
|
||||
}
|
||||
|
||||
watch(
|
||||
attachments,
|
||||
() => attachments.value.map((a) => a.id),
|
||||
() => emit('change', attachments.value.map((a) => a.id)),
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -247,12 +247,18 @@ function onSaveMessage(id: number) {
|
||||
}
|
||||
|
||||
// 流式输出时自动滚到底部(同 ChatDetailView 的策略)。
|
||||
let scrollPending = false
|
||||
watch(
|
||||
() => [session.messages.value.length, session.messages.value.at(-1)?.content?.length ?? 0],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
const el = scrollEl.value
|
||||
if (el) el.scrollTo(0, el.scrollHeight)
|
||||
if (scrollPending) return
|
||||
scrollPending = true
|
||||
requestAnimationFrame(() => {
|
||||
scrollPending = false
|
||||
nextTick(() => {
|
||||
const el = scrollEl.value
|
||||
if (el) el.scrollTo(0, el.scrollHeight)
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -262,7 +268,10 @@ watch(
|
||||
() => props.phase,
|
||||
() => {
|
||||
session.closeCurrent()
|
||||
draft.value = ''
|
||||
showCreateForm.value = false
|
||||
newSystemPrompt.value = ''
|
||||
newFirstMessage.value = DEFAULT_FIRST_MESSAGE
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
NCard,
|
||||
@@ -263,21 +263,31 @@ const hasAnyContent = computed(() =>
|
||||
)
|
||||
|
||||
// 默认展开有内容的第一项(优先当前阶段)
|
||||
const defaultExpandedNames = computed(() => {
|
||||
const names: (ArtifactPhase | 'conversations' | 'acp-sessions')[] = []
|
||||
if (props.currentPhase && (ARTIFACT_PHASES as Phase[]).includes(props.currentPhase)) {
|
||||
const cp = props.currentPhase as ArtifactPhase
|
||||
if (artifactsByPhase.value[cp].length > 0) names.push(cp)
|
||||
}
|
||||
for (const phase of ARTIFACT_PHASES) {
|
||||
if (artifactsByPhase.value[phase].length > 0 && !names.includes(phase)) {
|
||||
names.push(phase)
|
||||
const defaultExpandedNames = ref<(ArtifactPhase | 'conversations' | 'acp-sessions')[]>([])
|
||||
watch(
|
||||
[
|
||||
() => props.currentPhase,
|
||||
() => artifacts.value.length,
|
||||
() => conversations.value.length,
|
||||
() => acpSessions.value.length,
|
||||
],
|
||||
() => {
|
||||
const names: (ArtifactPhase | 'conversations' | 'acp-sessions')[] = []
|
||||
if (props.currentPhase && (ARTIFACT_PHASES as Phase[]).includes(props.currentPhase)) {
|
||||
const cp = props.currentPhase as ArtifactPhase
|
||||
if (artifactsByPhase.value[cp].length > 0) names.push(cp)
|
||||
}
|
||||
}
|
||||
if (conversations.value.length > 0) names.push('conversations')
|
||||
if (acpSessions.value.length > 0) names.push('acp-sessions')
|
||||
return names.slice(0, 2) // 最多默认展开前两项,避免过长
|
||||
})
|
||||
for (const phase of ARTIFACT_PHASES) {
|
||||
if (artifactsByPhase.value[phase].length > 0 && !names.includes(phase)) {
|
||||
names.push(phase)
|
||||
}
|
||||
}
|
||||
if (conversations.value.length > 0) names.push('conversations')
|
||||
if (acpSessions.value.length > 0) names.push('acp-sessions')
|
||||
defaultExpandedNames.value = names.slice(0, 2) // 最多默认展开前两项,避免过长
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const renderedArtifact = computed(() =>
|
||||
selectedArtifact.value ? md.render(selectedArtifact.value.content) : '',
|
||||
|
||||
@@ -67,17 +67,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { onMounted, ref, computed, defineAsyncComponent } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NTabs, NTabPane, NPageHeader, useMessage } from 'naive-ui'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||
import SyncBar from './components/SyncBar.vue'
|
||||
import WorktreesTab from './components/WorktreesTab.vue'
|
||||
import MainTab from './components/MainTab.vue'
|
||||
import CommitHistoryTab from './components/CommitHistoryTab.vue'
|
||||
import SettingsTab from './components/SettingsTab.vue'
|
||||
import RunsTab from './components/RunsTab.vue'
|
||||
const WorktreesTab = defineAsyncComponent(() => import('./components/WorktreesTab.vue'))
|
||||
const MainTab = defineAsyncComponent(() => import('./components/MainTab.vue'))
|
||||
const CommitHistoryTab = defineAsyncComponent(() => import('./components/CommitHistoryTab.vue'))
|
||||
const SettingsTab = defineAsyncComponent(() => import('./components/SettingsTab.vue'))
|
||||
const RunsTab = defineAsyncComponent(() => import('./components/RunsTab.vue'))
|
||||
|
||||
const route = useRoute()
|
||||
const store = useWorkspacesStore()
|
||||
|
||||
Reference in New Issue
Block a user