Explorar el Código

WIP: 验证快速模式与显示思考端到端同步

[gstack-context]
Decisions: 假 9119 记录 RPC;验证 fast config 在 prompt 前且 off 写 normal;show thinking 跨同 Agent 会话;会话切换关闭设置浮层
Remaining: 全量测试、真实页面视觉与 9119 运行验证、重载 8800
Skill: /gitnexus-exploring + /frontend-skill
[/gstack-context]
samien hace 2 meses
padre
commit
275a7d3b64

+ 5 - 1
src/client/components/composer/ComposerShell.vue

@@ -366,7 +366,11 @@ function onDocumentPointer(event: PointerEvent) {
 }
 
 watch(text, value => { persistDraft(value); nextTick(autoSize) })
-watch(() => props.draftKey, restoreDraft)
+watch(() => props.draftKey, () => {
+  restoreDraft()
+  reasoningOpen.value = false
+  settingsOpen.value = false
+})
 
 onMounted(() => {
   restoreDraft()

+ 44 - 0
tests/e2e/workspace.spec.ts

@@ -97,6 +97,50 @@ test('opens a conversation even when marking it read is unsupported', async ({ p
   expect(pageErrors).toEqual([])
 })
 
+test('syncs the blue fast-mode shortcut with the iOS-compatible session config', async ({ page }) => {
+  await page.request.post('http://127.0.0.1:19119/__test/rpc-requests/reset')
+  await page.goto('/chat/session-demo')
+  const fast = page.getByRole('button', { name: '快速模式:已关闭' })
+  await expect(fast).toHaveAttribute('aria-pressed', 'false')
+  await fast.click()
+  const enabled = page.getByRole('button', { name: '快速模式:已开启' })
+  await expect(enabled).toHaveAttribute('aria-pressed', 'true')
+  await expect(enabled).toHaveCSS('color', 'rgb(22, 119, 255)')
+
+  await page.getByRole('textbox', { name: '输入消息,Enter 发送,Shift + Enter 换行' }).fill('使用快速模式')
+  await page.getByRole('button', { name: '发送消息' }).click()
+  await expect(page.getByText('这是来自假 Gateway 的流式回复。', { exact: true })).toBeVisible()
+
+  const payload = await (await page.request.get('http://127.0.0.1:19119/__test/rpc-requests')).json() as { requests: Array<{ method: string; params: Record<string, unknown> }> }
+  const fastConfigIndex = payload.requests.findIndex(request => request.method === 'config.set' && request.params.key === 'fast')
+  const promptIndex = payload.requests.findIndex(request => request.method === 'prompt.submit')
+  expect(fastConfigIndex).toBeGreaterThanOrEqual(0)
+  expect(promptIndex).toBeGreaterThan(fastConfigIndex)
+  expect(payload.requests[fastConfigIndex]?.params).toMatchObject({ key: 'fast', value: 'fast', scope: 'session' })
+
+  await enabled.click()
+  await expect(page.getByRole('button', { name: '快速模式:已关闭' })).toHaveAttribute('aria-pressed', 'false')
+  await expect.poll(async () => {
+    const next = await (await page.request.get('http://127.0.0.1:19119/__test/rpc-requests')).json() as typeof payload
+    return next.requests.filter(request => request.method === 'config.set' && request.params.key === 'fast').at(-1)?.params.value
+  }).toBe('normal')
+})
+
+test('keeps the show-thinking preference across sessions of the same Agent', async ({ page }) => {
+  await page.goto('/chat/session-demo')
+  await expect(page.locator('.turn-trace')).toHaveCount(1)
+  await page.locator('.composer-tool[aria-label="设置"]').click()
+  const setting = page.getByRole('switch', { name: /显示思考/ })
+  await expect(setting).toHaveAttribute('aria-checked', 'true')
+  await setting.click()
+  await expect(page.locator('.turn-trace')).toHaveCount(0)
+  await page.getByRole('option').filter({ hasText: '第二个会话' }).click()
+  await expect(page.locator('.turn-trace')).toHaveCount(0)
+  await page.locator('.composer-tool[aria-label="设置"]').click()
+  await page.getByRole('switch', { name: /显示思考/ }).click()
+  await expect(page.locator('.turn-trace')).toHaveCount(1)
+})
+
 test('restores a legacy session link under its owning Agent profile', async ({ page }) => {
   await page.goto('/chat/session-yaoer')
   await expect(page.getByText('瑶儿历史消息', { exact: true })).toBeVisible()

+ 9 - 2
tests/fixtures/fake-hermes.mjs

@@ -58,10 +58,16 @@ const groupMessage = { seq: 1, id: '44444444-4444-4444-8444-444444444444', roomI
 const groupAssistantMessage = { seq: 2, id: '55555555-5555-4555-8555-555555555555', roomId, senderKind: 'agent', senderId: agentId, senderName: '夭夭', content: '历史兼容:MEDIA:/brand/AppIcon-1024.png', status: 'completed', createdAt: now() - 100, updatedAt: now() - 100 }
 const groupMessages = [groupMessage, groupAssistantMessage]
 const room = { id: roomId, name: '设计与工程协作', cwd: '/tmp', createdAt: now() - 2000, updatedAt: now(), archived: false, agentCount: 1, maxReplyRounds: 3, lastMessage: groupAssistantMessage, unreadCount: 0 }
+const rpcRequests = []
 const previewPdf = Buffer.from('JVBERi0xLjQKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCAyMDAgMjAwXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTAgMDAwMDAgbiAKMDAwMDAwMDA1MyAwMDAwMCBuIAowMDAwMDAwMTA1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMTY2CiUlRU9G', 'base64')
 
 const server = createServer(async (request, response) => {
   const url = new URL(request.url || '/', `http://${request.headers.host}`)
+  if (url.pathname === '/__test/rpc-requests' && request.method === 'GET') return json(response, 200, { requests: rpcRequests })
+  if (url.pathname === '/__test/rpc-requests/reset' && request.method === 'POST') {
+    rpcRequests.length = 0
+    return json(response, 200, { ok: true })
+  }
   if (url.pathname === '/api/status') return json(response, 200, { version: 'test', overall: 'ok', auth_required: true, auth_providers: ['basic'], gateway_running: true, gateway_state: 'running' })
   if (url.pathname === '/api/auth/providers') return json(response, 200, { providers: [{ name: 'basic', display_name: '账号密码', supports_password: true }] })
   if (url.pathname === '/auth/password-login' && request.method === 'POST') {
@@ -126,9 +132,10 @@ server.on('upgrade', (request, socket, head) => {
     client.send(JSON.stringify({ jsonrpc: '2.0', method: 'event', params: { type: 'gateway.ready', payload: { capabilities: ['safe_interrupt', 'session_reasoning_config'] } } }))
     client.on('message', raw => {
       const requestFrame = JSON.parse(raw.toString())
+      rpcRequests.push(requestFrame)
       const respond = result => client.send(JSON.stringify({ jsonrpc: '2.0', id: requestFrame.id, result }))
-      if (requestFrame.method === 'session.resume') return respond({ session_id: 'runtime-demo', stored_session_id: requestFrame.params.session_id, messages: [] })
-      if (requestFrame.method === 'session.create') return respond({ session_id: 'runtime-new', stored_session_id: 'session-new' })
+      if (requestFrame.method === 'session.resume') return respond({ session_id: 'runtime-demo', stored_session_id: requestFrame.params.session_id, fast: false, messages: [] })
+      if (requestFrame.method === 'session.create') return respond({ session_id: 'runtime-new', stored_session_id: 'session-new', fast: requestFrame.params.fast === true })
       if (requestFrame.method === 'session.usage') return respond({ context_used: 12500, context_max: 114688, total: 12500, input: 9000, output: 3500 })
       if (requestFrame.method === 'prompt.submit') {
         respond({ status: 'accepted' })