You've already forked agentic-coding-workflow
Web优化 1
This commit is contained in:
@@ -17,8 +17,8 @@ export interface UseAcpStreamReturn {
|
|||||||
events: Ref<AcpEvent[]>
|
events: Ref<AcpEvent[]>
|
||||||
pendingPermissions: Ref<PermissionRequest[]>
|
pendingPermissions: Ref<PermissionRequest[]>
|
||||||
errorMsg: Ref<string | null>
|
errorMsg: Ref<string | null>
|
||||||
prompt: (text: string, agentSessionID: string) => void
|
prompt: (text: string, agentSessionID: string) => boolean
|
||||||
cancel: (agentSessionID: string) => void
|
cancel: (agentSessionID: string) => boolean
|
||||||
reconnect: () => void
|
reconnect: () => void
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
@@ -181,21 +181,23 @@ export function useAcpStream(sessionId: string): UseAcpStreamReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function send(method: string, params: Record<string, unknown>) {
|
function send(method: string, params: Record<string, unknown>) {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
if (!ws || ws.readyState !== WebSocket.OPEN) return false
|
||||||
const id = 'client-' + crypto.randomUUID()
|
const uuid = crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)
|
||||||
|
const id = 'client-' + uuid
|
||||||
const msg = { jsonrpc: '2.0', id, method, params }
|
const msg = { jsonrpc: '2.0', id, method, params }
|
||||||
ws.send(JSON.stringify(msg))
|
ws.send(JSON.stringify(msg))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function prompt(text: string, agentSessionID: string) {
|
function prompt(text: string, agentSessionID: string) {
|
||||||
send('session/prompt', {
|
return send('session/prompt', {
|
||||||
sessionId: agentSessionID,
|
sessionId: agentSessionID,
|
||||||
prompt: [{ type: 'text', text }],
|
prompt: [{ type: 'text', text }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel(agentSessionID: string) {
|
function cancel(agentSessionID: string) {
|
||||||
send('session/cancel', { sessionId: agentSessionID })
|
return send('session/cancel', { sessionId: agentSessionID })
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconnect() {
|
function reconnect() {
|
||||||
|
|||||||
@@ -74,31 +74,40 @@ export function useConversationSession() {
|
|||||||
if (!currentConversation.value) return
|
if (!currentConversation.value) return
|
||||||
if (streamingMessageId.value !== null) return
|
if (streamingMessageId.value !== null) return
|
||||||
const conv = currentConversation.value
|
const conv = currentConversation.value
|
||||||
const resp = await chatApi.sendMessage(conv.id, {
|
|
||||||
content,
|
|
||||||
attachment_ids: attachmentIds,
|
|
||||||
})
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
messages.value.push({
|
const userMsg: ChatMessage = {
|
||||||
id: resp.user_message_id,
|
id: -1,
|
||||||
conversation_id: conv.id,
|
conversation_id: conv.id,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
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',
|
status: 'pending',
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
}
|
||||||
streamingMessageId.value = resp.assistant_message_id
|
messages.value.push(userMsg)
|
||||||
attachStream(resp.stream_url, resp.user_message_id)
|
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() {
|
async function cancelMessage() {
|
||||||
|
|||||||
+16
-3
@@ -28,8 +28,10 @@ export const useUIStore = defineStore('ui', () => {
|
|||||||
? window.matchMedia('(prefers-color-scheme: dark)')
|
? window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
: null
|
: null
|
||||||
const systemDark = ref(mediaQuery ? mediaQuery.matches : false)
|
const systemDark = ref(mediaQuery ? mediaQuery.matches : false)
|
||||||
|
let mediaListener: ((e: MediaQueryListEvent) => void) | null = null
|
||||||
if (mediaQuery) {
|
if (mediaQuery) {
|
||||||
mediaQuery.addEventListener('change', (e) => (systemDark.value = e.matches))
|
mediaListener = (e: MediaQueryListEvent) => (systemDark.value = e.matches)
|
||||||
|
mediaQuery.addEventListener('change', mediaListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveDark = computed(() =>
|
const effectiveDark = computed(() =>
|
||||||
@@ -38,10 +40,21 @@ export const useUIStore = defineStore('ui', () => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
effectiveDark,
|
effectiveDark,
|
||||||
(v) => document.documentElement.classList.toggle('dark', v),
|
(v) => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.classList.toggle('dark', v)
|
||||||
|
}
|
||||||
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function cleanupMediaQuery() {
|
||||||
|
if (mediaQuery && mediaListener) {
|
||||||
|
mediaQuery.removeEventListener('change', mediaListener)
|
||||||
|
mediaListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setThemeMode(mode: ThemeMode) {
|
function setThemeMode(mode: ThemeMode) {
|
||||||
themeMode.value = mode
|
themeMode.value = mode
|
||||||
safeWrite('themeMode', mode)
|
safeWrite('themeMode', mode)
|
||||||
@@ -50,5 +63,5 @@ export const useUIStore = defineStore('ui', () => {
|
|||||||
const sidebarCollapsed = ref(safeReadString('sidebarCollapsed') === 'true')
|
const sidebarCollapsed = ref(safeReadString('sidebarCollapsed') === 'true')
|
||||||
watch(sidebarCollapsed, (v) => safeWrite('sidebarCollapsed', String(v)))
|
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()
|
stream.value?.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPrompting = computed(() => {
|
const lastPromptIndex = ref(-1)
|
||||||
const evs = events.value
|
const hasResponseAfterPrompt = ref(false)
|
||||||
let lastPrompt = -1
|
const isPrompting = computed(() => lastPromptIndex.value >= 0 && !hasResponseAfterPrompt.value)
|
||||||
for (let i = evs.length - 1; i >= 0; i--) {
|
|
||||||
if (evs[i].method === 'session/prompt' && evs[i].direction === 'in') {
|
watch(
|
||||||
lastPrompt = i
|
() => events.value.length,
|
||||||
break
|
() => {
|
||||||
|
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 (lastPromptIndex.value >= 0) {
|
||||||
if (lastPrompt < 0) return false
|
for (let i = lastPromptIndex.value + 1; i < evs.length; i++) {
|
||||||
for (let i = lastPrompt + 1; i < evs.length; i++) {
|
if (evs[i].kind === 'response') {
|
||||||
if (evs[i].kind === 'response') return false
|
hasResponseAfterPrompt.value = true
|
||||||
}
|
break
|
||||||
return true
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function onPrompt(text: string) {
|
function onPrompt(text: string) {
|
||||||
const sess = store.currentSession
|
const sess = store.currentSession
|
||||||
@@ -95,13 +107,15 @@ function onPrompt(text: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
errorMsg.value = null
|
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() {
|
function onCancel() {
|
||||||
const sess = store.currentSession
|
const sess = store.currentSession
|
||||||
if (!sess?.agent_session_id) return
|
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() {
|
async function terminate() {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
attr-type="submit"
|
attr-type="submit"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
:disabled="!isValid"
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { NForm, NFormItem, NInput, NButton } from 'naive-ui'
|
import { NForm, NFormItem, NInput, NButton } from 'naive-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
@@ -50,6 +51,8 @@ const password = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
|
const isValid = computed(() => email.value.trim().length > 0 && password.value.length > 0)
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -215,12 +215,18 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 流式输出时 messages.length 不变,只 content 增长 —— 同时观察末消息内容长度。
|
// 流式输出时 messages.length 不变,只 content 增长 —— 同时观察末消息内容长度。
|
||||||
|
let scrollPending = false
|
||||||
watch(
|
watch(
|
||||||
() => [chat.messages.length, chat.messages.at(-1)?.content?.length ?? 0],
|
() => [chat.messages.length, chat.messages.at(-1)?.content?.length ?? 0],
|
||||||
() => {
|
() => {
|
||||||
nextTick(() => {
|
if (scrollPending) return
|
||||||
const el = scrollEl.value
|
scrollPending = true
|
||||||
if (el) el.scrollTo(0, el.scrollHeight)
|
requestAnimationFrame(() => {
|
||||||
|
scrollPending = false
|
||||||
|
nextTick(() => {
|
||||||
|
const el = scrollEl.value
|
||||||
|
if (el) el.scrollTo(0, el.scrollHeight)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -82,9 +82,8 @@ function remove(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
attachments,
|
() => attachments.value.map((a) => a.id),
|
||||||
() => emit('change', attachments.value.map((a) => a.id)),
|
() => emit('change', attachments.value.map((a) => a.id)),
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -247,12 +247,18 @@ function onSaveMessage(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 流式输出时自动滚到底部(同 ChatDetailView 的策略)。
|
// 流式输出时自动滚到底部(同 ChatDetailView 的策略)。
|
||||||
|
let scrollPending = false
|
||||||
watch(
|
watch(
|
||||||
() => [session.messages.value.length, session.messages.value.at(-1)?.content?.length ?? 0],
|
() => [session.messages.value.length, session.messages.value.at(-1)?.content?.length ?? 0],
|
||||||
() => {
|
() => {
|
||||||
nextTick(() => {
|
if (scrollPending) return
|
||||||
const el = scrollEl.value
|
scrollPending = true
|
||||||
if (el) el.scrollTo(0, el.scrollHeight)
|
requestAnimationFrame(() => {
|
||||||
|
scrollPending = false
|
||||||
|
nextTick(() => {
|
||||||
|
const el = scrollEl.value
|
||||||
|
if (el) el.scrollTo(0, el.scrollHeight)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -262,7 +268,10 @@ watch(
|
|||||||
() => props.phase,
|
() => props.phase,
|
||||||
() => {
|
() => {
|
||||||
session.closeCurrent()
|
session.closeCurrent()
|
||||||
|
draft.value = ''
|
||||||
showCreateForm.value = false
|
showCreateForm.value = false
|
||||||
|
newSystemPrompt.value = ''
|
||||||
|
newFirstMessage.value = DEFAULT_FIRST_MESSAGE
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NCard,
|
NCard,
|
||||||
@@ -263,21 +263,31 @@ const hasAnyContent = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 默认展开有内容的第一项(优先当前阶段)
|
// 默认展开有内容的第一项(优先当前阶段)
|
||||||
const defaultExpandedNames = computed(() => {
|
const defaultExpandedNames = ref<(ArtifactPhase | 'conversations' | 'acp-sessions')[]>([])
|
||||||
const names: (ArtifactPhase | 'conversations' | 'acp-sessions')[] = []
|
watch(
|
||||||
if (props.currentPhase && (ARTIFACT_PHASES as Phase[]).includes(props.currentPhase)) {
|
[
|
||||||
const cp = props.currentPhase as ArtifactPhase
|
() => props.currentPhase,
|
||||||
if (artifactsByPhase.value[cp].length > 0) names.push(cp)
|
() => artifacts.value.length,
|
||||||
}
|
() => conversations.value.length,
|
||||||
for (const phase of ARTIFACT_PHASES) {
|
() => acpSessions.value.length,
|
||||||
if (artifactsByPhase.value[phase].length > 0 && !names.includes(phase)) {
|
],
|
||||||
names.push(phase)
|
() => {
|
||||||
|
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 (conversations.value.length > 0) names.push('conversations')
|
if (artifactsByPhase.value[phase].length > 0 && !names.includes(phase)) {
|
||||||
if (acpSessions.value.length > 0) names.push('acp-sessions')
|
names.push(phase)
|
||||||
return names.slice(0, 2) // 最多默认展开前两项,避免过长
|
}
|
||||||
})
|
}
|
||||||
|
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(() =>
|
const renderedArtifact = computed(() =>
|
||||||
selectedArtifact.value ? md.render(selectedArtifact.value.content) : '',
|
selectedArtifact.value ? md.render(selectedArtifact.value.content) : '',
|
||||||
|
|||||||
@@ -67,17 +67,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed, defineAsyncComponent } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { NTabs, NTabPane, NPageHeader, useMessage } from 'naive-ui'
|
import { NTabs, NTabPane, NPageHeader, useMessage } from 'naive-ui'
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
import SyncBar from './components/SyncBar.vue'
|
import SyncBar from './components/SyncBar.vue'
|
||||||
import WorktreesTab from './components/WorktreesTab.vue'
|
const WorktreesTab = defineAsyncComponent(() => import('./components/WorktreesTab.vue'))
|
||||||
import MainTab from './components/MainTab.vue'
|
const MainTab = defineAsyncComponent(() => import('./components/MainTab.vue'))
|
||||||
import CommitHistoryTab from './components/CommitHistoryTab.vue'
|
const CommitHistoryTab = defineAsyncComponent(() => import('./components/CommitHistoryTab.vue'))
|
||||||
import SettingsTab from './components/SettingsTab.vue'
|
const SettingsTab = defineAsyncComponent(() => import('./components/SettingsTab.vue'))
|
||||||
import RunsTab from './components/RunsTab.vue'
|
const RunsTab = defineAsyncComponent(() => import('./components/RunsTab.vue'))
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useWorkspacesStore()
|
const store = useWorkspacesStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user