Просмотр исходного кода

WIP: 将显示思考设为 Agent 级偏好

[gstack-context]
Decisions: show thinking 按 account/profile 持久化;隐藏整个 grouped trace;普通聊天与群聊统一文案和语义
Remaining: 模型旁闪电快速模式按钮、RPC 端到端验证与重载
Skill: /frontend-skill
[/gstack-context]
samien 2 месяцев назад
Родитель
Сommit
2e021eafdc

+ 3 - 3
src/client/components/composer/ComposerShell.vue

@@ -455,7 +455,7 @@ defineExpose({
             <Transition name="composer-menu">
               <div v-if="settingsOpen" class="composer-popover composer-popover--settings" role="dialog" aria-label="输入设置">
                 <button class="composer-setting-row" type="button" role="switch" :aria-checked="toolTraceVisible" @click="emit('settingsClick')">
-                  <span><AppIcon name="tools" :size="15" />显示工具轨迹</span>
+                  <span><AppIcon name="brain" :size="15" />显示思考</span>
                   <AppIcon v-if="toolTraceVisible" name="check" :size="16" />
                 </button>
                 <button class="composer-setting-row" type="button" title="首版暂未启用语音输入" @click="showError('语音输入尚未启用')">
@@ -464,8 +464,8 @@ defineExpose({
               </div>
             </Transition>
           </div>
-          <button v-else class="composer-tool" type="button" :class="{ active: toolTraceVisible }" :disabled="disabled" title="显示工具调用" @click="emit('toolTraceToggle')">
-            <AppIcon name="tools" :size="16" /><span>工具</span>
+          <button v-else class="composer-tool" type="button" :class="{ active: toolTraceVisible }" :disabled="disabled" title="显示思考" @click="emit('toolTraceToggle')">
+            <AppIcon name="brain" :size="16" /><span>思考</span>
           </button>
           <button v-if="mode === 'chat'" class="composer-tool composer-tool--model" type="button" :disabled="disabled" :title="modelLabel" @click="emit('modelClick')">
             <AppIcon name="model" :size="15" /><span>{{ compactModel }}</span><AppIcon name="chevron-down" :size="11" />

+ 3 - 2
src/client/components/messages/MessageTimeline.vue

@@ -176,9 +176,9 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
         </button>
 
         <template v-for="row in timelineRows" :key="row.id">
-        <TurnTrace v-if="row.kind === 'trace'" :group="row" />
+        <TurnTrace v-if="row.kind === 'trace' && showTools" :group="row" />
+        <template v-else-if="row.kind === 'message'">
         <article
-          v-else
           v-for="message in [row.message]"
           :key="message.id"
           :data-message-id="message.id"
@@ -261,6 +261,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
           </div>
         </article>
         </template>
+        </template>
 
         <InteractionCard
           v-if="interaction"

+ 16 - 4
src/client/views/ChatView.vue

@@ -20,6 +20,7 @@ import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
 import WorkspaceView from '@/components/workspace/WorkspaceView.vue'
 import { chatInteraction, chatMessagesToUi, sessionSidebarItem } from '@/components/workspace/viewModels'
 import { consumeLibraryItemForComposer, loadComposerFile } from '@/components/workspace/pendingComposer'
+import { readAgentShowThinking, writeAgentShowThinking } from '@/utils/sessionPreferences'
 import { useAuthStore } from '@/stores/auth'
 import { useChatStore } from '@/stores/chat'
 import { getSession } from '@/api/sessions'
@@ -35,7 +36,7 @@ const filePreview = ref<UiLibraryItem | null>(null)
 const mediaPreviewIndex = ref<number | null>(null)
 const outlineOpen = ref(false)
 const modelDialog = ref(false)
-const showTools = ref(true)
+const showThinking = ref(true)
 const queueMode = ref(false)
 const actionSessionId = ref('')
 const renaming = ref(false)
@@ -203,6 +204,15 @@ function selectReasoning(id: string) {
   chat.reasoningEffort = id || undefined
 }
 
+function restoreShowThinking(profile = auth.activeProfile?.name || 'default') {
+  showThinking.value = readAgentShowThinking(auth.user?.id ?? 'local', profile)
+}
+
+function toggleShowThinking() {
+  showThinking.value = !showThinking.value
+  writeAgentShowThinking(auth.user?.id ?? 'local', auth.activeProfile?.name || 'default', showThinking.value)
+}
+
 function openSessionActions(id: string, event?: MouseEvent) {
   actionSessionId.value = id
   const session = chat.sessions.find(item => item.id === id)
@@ -295,6 +305,7 @@ async function revealSourceMessage() {
 }
 
 onMounted(async () => {
+  restoreShowThinking()
   document.addEventListener('pointerdown', handleSessionActionPointer)
   document.addEventListener('keydown', handleSessionActionKey)
   await refreshSessions()
@@ -312,6 +323,7 @@ onBeforeUnmount(() => {
 
 watch(() => auth.activeProfile?.name, async (profile, previous) => {
   if (!profile || profile === previous) return
+  restoreShowThinking(profile)
   outlineOpen.value = false
   preview.value = null
   if (restoringRouteProfile.value === profile) {
@@ -375,7 +387,7 @@ watch(() => chat.activeSessionId, async id => {
         :has-older="chat.hasMoreBefore"
         :connected="connected"
         :synced="chat.historySynced"
-        :show-tools="showTools"
+        :show-tools="showThinking"
         :show-assistant-identity="false"
         :interaction="interaction"
         empty-logo
@@ -408,7 +420,7 @@ watch(() => chat.activeSessionId, async id => {
         :context-used="chat.contextUsage?.contextTokens || chat.contextUsage?.totalTokens || 0"
         :context-limit="chat.contextUsage?.contextLimit || 262144"
         :queue-mode="queueMode || chat.isQueued"
-        :tool-trace-visible="showTools"
+        :tool-trace-visible="showThinking"
         :reference="reference"
         :slash-commands="[
           { id: 'fork', label: 'fork', detail: '从当前会话创建分支' },
@@ -419,7 +431,7 @@ watch(() => chat.activeSessionId, async id => {
         @stop="chat.interrupt"
         @model-click="modelDialog = true"
         @reasoning-change="selectReasoning"
-        @settings-click="showTools = !showTools"
+        @settings-click="toggleShowThinking"
         @queue-toggle="queueMode = !queueMode"
         @clear-reference="quoted = null"
       />

+ 16 - 4
src/client/views/GroupsView.vue

@@ -18,6 +18,7 @@ import type { UiMessage } from '@/components/messages/types'
 import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
 import WorkspaceView from '@/components/workspace/WorkspaceView.vue'
 import { loadComposerFile } from '@/components/workspace/pendingComposer'
+import { readAgentShowThinking, writeAgentShowThinking } from '@/utils/sessionPreferences'
 import { agentToUi, groupInteraction, groupMessageToUi, roomSidebarItem, roomToUi } from '@/components/workspace/viewModels'
 import { useAuthStore } from '@/stores/auth'
 import { useGroupsStore } from '@/stores/groups'
@@ -29,7 +30,7 @@ const router = useRouter()
 const search = ref('')
 const createOpen = ref(false)
 const managerOpen = ref(false)
-const showTools = ref(true)
+const showThinking = ref(true)
 const quoted = ref<UiMessage | null>(null)
 const preview = ref<UiLibraryItem | null>(null)
 const mediaPreviewIndex = ref<number | null>(null)
@@ -56,6 +57,15 @@ const mentionOptions = computed<ComposerOption[]>(() => [
   ...groups.agents.map(agent => ({ id: agent.id, label: agent.displayName || agent.profile, detail: agent.profile, disabled: !agent.enabled })),
 ])
 
+function restoreShowThinking(profile = auth.activeProfile?.name || 'default') {
+  showThinking.value = readAgentShowThinking(auth.user?.id ?? 'local', profile)
+}
+
+function toggleShowThinking() {
+  showThinking.value = !showThinking.value
+  writeAgentShowThinking(auth.user?.id ?? 'local', auth.activeProfile?.name || 'default', showThinking.value)
+}
+
 async function selectRoom(id: string) {
   await router.push(`/groups/${encodeURIComponent(id)}`)
   await groups.selectRoom(id)
@@ -138,6 +148,7 @@ async function addMediaToComposer(media: PreviewMedia) {
 }
 
 onMounted(async () => {
+  restoreShowThinking()
   try {
     await groups.start()
     const requested = typeof route.params.roomId === 'string' ? route.params.roomId : ''
@@ -151,6 +162,7 @@ onBeforeUnmount(() => groups.stop())
 watch(() => route.params.roomId, async roomId => {
   if (typeof roomId === 'string' && roomId && roomId !== groups.selectedRoomId) await groups.selectRoom(roomId)
 })
+watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThinking(profile) })
 </script>
 
 <template>
@@ -188,7 +200,7 @@ watch(() => route.params.roomId, async roomId => {
         :has-older="groups.hasMoreBefore"
         :connected="connected"
         :synced="!!groups.selectedRoom"
-        :show-tools="showTools"
+        :show-tools="showThinking"
         :interaction="activeInteraction"
         :mention-names="mentionNames"
         empty-title="让多个 Agent 一起工作"
@@ -215,14 +227,14 @@ watch(() => route.params.roomId, async roomId => {
         :disabled="!groups.selectedRoom || groups.availability !== 'available'"
         :streaming="groups.agents.some(agent => ['queued', 'running'].includes(agent.status))"
         :sending="groups.isSending"
-        :tool-trace-visible="showTools"
+        :tool-trace-visible="showThinking"
         :reference="reference"
         :mention-options="mentionOptions"
         :attachments-enabled="uploadsEnabled"
         placeholder="发消息给群聊,输入 @ 提及 Agent"
         @send="send"
         @stop="groups.agents.filter(agent => ['queued', 'running'].includes(agent.status)).forEach(agent => groups.interruptAgent(agent.id))"
-        @tool-trace-toggle="showTools = !showTools"
+        @tool-trace-toggle="toggleShowThinking"
         @clear-reference="quoted = null"
       />
     </div>