import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
  within,
  waitFor
} from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type {
  AgentEvent,
  BrowserLiveState,
  DesktopApi
} from '../../shared/contracts'
import type { ApplicationSettings } from '../../shared/application-settings-contracts'

const speechRecognitionMocks = vi.hoisted(() => ({
  startPcmRecording: vi.fn()
}))

vi.mock('./speech-recognition', async (importOriginal) => ({
  ...(await importOriginal<
    typeof import('./speech-recognition')
  >()),
  startPcmRecording: speechRecognitionMocks.startPcmRecording
}))

import App from './App'
import { loadActivityRecords } from './activity-store'

let agentListener: ((event: AgentEvent) => void) | undefined
let browserListener: ((state: BrowserLiveState) => void) | undefined
let newConversationListener: (() => void) | undefined
let maximizedChangedListener: ((maximized: boolean) => void) | undefined
const removeMaximizedChangedListener = vi.fn()
const run = vi.fn<DesktopApi['agent']['run']>()
const modelProfileId = '00000000-0000-4000-8000-000000000001'
const projectId = '00000000-0000-4000-8000-000000000101'
const project = {
  id: projectId,
  name: '默认项目',
  description: '测试项目',
  rootPath: 'C:\\Users\\test',
  defaultWorkMode: 'ask' as const,
  kind: 'user' as const,
  status: 'active' as const,
  createdAt: '2026-07-31T00:00:00.000Z',
  updatedAt: '2026-07-31T00:00:00.000Z'
}

const api: DesktopApi = {
  app: {
    getInfo: vi.fn(async () => ({
      name: 'GoodBuddy',
      version: '0.1.0',
      platform: 'win32',
      arch: 'x64',
      shortcut: 'Ctrl+Shift+Space'
    })),
    show: vi.fn(async () => {}),
    hide: vi.fn(async () => {}),
    minimize: vi.fn(async () => {}),
    toggleMaximize: vi.fn(async () => {}),
    close: vi.fn(async () => {}),
    isMaximized: vi.fn(async () => false),
    onMaximizedChanged: vi.fn((listener) => {
      maximizedChangedListener = listener
      return removeMaximizedChangedListener
    }),
    clearLocalData: vi.fn(async () => {}),
    onNewConversation: vi.fn((listener) => {
      newConversationListener = listener
      return () => {
        newConversationListener = undefined
      }
    }),
    onOpenSettings: vi.fn(() => () => {})
  },
  agent: {
    getStatus: vi.fn<DesktopApi['agent']['getStatus']>(async () => ({
      id: 'model' as const,
      label: 'sonnet-5',
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })),
    run,
    cancel: vi.fn(async () => {}),
    respondApproval: vi.fn(async () => {}),
    respondQuestion: vi.fn(async () => {}),
    onEvent: vi.fn((listener) => {
      agentListener = listener
      return () => {
        agentListener = undefined
      }
    })
  },
  browser: {
    interact: vi.fn(async () => {}),
    stop: vi.fn(async () => {}),
    onState: vi.fn((listener) => {
      browserListener = listener
      return () => {
        browserListener = undefined
      }
    })
  },
  speech: {
    transcribe: vi.fn(async () => ({ text: '本地语音结果' })),
    cancel: vi.fn(async () => true)
  },
  settings: {
    getRuntime: vi.fn<DesktopApi['settings']['getRuntime']>(async () => ({
      provider: 'auto',
      modelBaseUrl: 'https://bigtoken.ai',
      modelName: 'sonnet-5',
      modelProtocol: 'anthropic-messages',
      modelAuthentication: 'api-key',
      imageGenerationQuality: 'auto',
      opencodeBaseUrl: '',
      opencodeEmbedded: false,
      opencodeBinaryPath: '',
      opencodeConfigPath: '',
      continueBinaryPath: '',
      continueConfigPath: '',
      continueMode: 'chat',
      runtimeSandboxMode: 'auto',
      subagentSmartRoutingEnabled: false,
      knowledgeEmbeddingEnabled: false,
      knowledgeEmbeddingBaseUrl:
        'http://127.0.0.1:11434/v1/embeddings',
      knowledgeEmbeddingModel: 'nomic-embed-text',
      knowledgeEmbeddingApiKeyConfigured: false,
      knowledgeEmbeddingCredentialSource: 'none',
      workspacePath: 'C:\\Users\\test',
      apiKeyConfigured: false,
      credentialSource: 'none',
      modelProfiles: [
        {
          id: modelProfileId,
          name: '默认模型',
          baseUrl: 'https://bigtoken.ai',
          modelName: 'sonnet-5',
          protocol: 'anthropic-messages',
          authentication: 'api-key',
          imageGenerationQuality: 'auto',
          apiKeyConfigured: false,
          credentialSource: 'none'
        }
      ],
      defaultModelProfileId: modelProfileId,
      opencodeModelSource: {
        kind: 'profile',
        profileId: modelProfileId
      },
      continueModelSource: {
        kind: 'profile',
        profileId: modelProfileId
      },
      secureStorageAvailable: true,
      toolApproval: 'always'
    })),
    updateRuntime: vi.fn<DesktopApi['settings']['updateRuntime']>(
      async (input) => ({
        provider: input.provider,
        modelBaseUrl: input.modelBaseUrl,
        modelName: input.modelName,
        modelProtocol: input.modelProtocol,
        modelAuthentication: input.modelAuthentication,
        imageGenerationQuality: input.imageGenerationQuality,
        opencodeBaseUrl: input.opencodeBaseUrl,
        opencodeEmbedded: input.opencodeEmbedded,
        opencodeBinaryPath: input.opencodeBinaryPath,
        opencodeConfigPath: input.opencodeConfigPath,
        continueBinaryPath: input.continueBinaryPath,
        continueConfigPath: input.continueConfigPath,
        continueMode: input.continueMode,
        runtimeSandboxMode: input.runtimeSandboxMode,
        subagentSmartRoutingEnabled:
          input.subagentSmartRoutingEnabled ?? false,
        knowledgeEmbeddingEnabled: input.knowledgeEmbeddingEnabled,
        knowledgeEmbeddingBaseUrl: input.knowledgeEmbeddingBaseUrl,
        knowledgeEmbeddingModel: input.knowledgeEmbeddingModel,
        knowledgeEmbeddingApiKeyConfigured:
          input.knowledgeEmbeddingApiKey?.action === 'replace',
        knowledgeEmbeddingCredentialSource:
          input.knowledgeEmbeddingApiKey?.action === 'replace'
            ? 'encrypted'
            : 'none',
        workspacePath: input.workspacePath,
        apiKeyConfigured: input.apiKey.action === 'replace',
        credentialSource:
          input.apiKey.action === 'replace' ? 'encrypted' : 'none',
        modelProfiles: (
          input.modelProfiles ?? [
            {
              id: modelProfileId,
              name: '默认模型',
              baseUrl: input.modelBaseUrl,
              modelName: input.modelName,
              protocol: input.modelProtocol,
              authentication: input.modelAuthentication,
              imageGenerationQuality:
                input.imageGenerationQuality,
              apiKey: input.apiKey
            }
          ]
        ).map(({ apiKey, ...profile }) => ({
          ...profile,
          apiKeyConfigured: apiKey.action === 'replace',
          credentialSource:
            apiKey.action === 'replace'
              ? ('encrypted' as const)
              : ('none' as const)
        })),
        defaultModelProfileId:
          input.defaultModelProfileId ?? modelProfileId,
        opencodeModelSource:
          input.opencodeModelSource ?? { kind: 'platform' },
        continueModelSource:
          input.continueModelSource ?? { kind: 'platform' },
        secureStorageAvailable: true,
        toolApproval: input.toolApproval
      })
    ),
    selectWorkspace: vi.fn(async () => undefined),
    detectAgentRuntimes: vi.fn<
      DesktopApi['settings']['detectAgentRuntimes']
    >(async () => ({
      opencode: {
        available: false,
        detail: '未检测到 OpenCode'
      },
      continue: {
        available: false,
        detail: '未检测到 Continue'
      }
    })),
    selectRuntimeFile: vi.fn(async () => undefined),
    openRuntimeConfig: vi.fn(async () => {}),
    testModelConnection: vi.fn<
      DesktopApi['settings']['testModelConnection']
    >(async () => ({
      id: 'model',
      label: 'sonnet-5',
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })),
    testRuntime: vi.fn<DesktopApi['settings']['testRuntime']>(
      async () => ({
        id: 'model',
        label: 'sonnet-5',
        available: true,
        supportsToolExecution: true,
        detail: 'Ready'
      })
    )
  },
  projects: {
    list: vi.fn(async () => [project]),
    create: vi.fn(async (input) => ({
      ...project,
      ...input,
      id: crypto.randomUUID()
    })),
    update: vi.fn(async (_projectId, input) => ({
      ...project,
      ...input,
      id: _projectId
    })),
    setArchived: vi.fn(async () => {}),
    delete: vi.fn(async () => {})
  },
  conversations: {
    list: vi.fn(async () => []),
    replace: vi.fn(async () => {}),
    onChanged: vi.fn(() => () => undefined)
  },
  workspace: {
    getChanges: vi.fn(async () => ({
      rootPath: 'C:\\Workspace',
      available: true,
      status: '',
      patch: '',
      files: [],
      truncated: false
    })),
    listDirectory: vi.fn(async (path: string) => ({
      path,
      entries: [],
      truncated: false
    })),
    readFile: vi.fn(async (path: string) => ({
      path,
      name: path.split('/').at(-1) ?? path,
      content: '',
      mimeType: 'text/plain' as const,
      size: 0
    })),
    openPath: vi.fn(async () => {})
  },
  tasks: {
    list: vi.fn(async () => []),
    setStatus: vi.fn(async () => {})
  },
  usage: {
    getTokenSummary: vi.fn(async () => ({
      totals: {
        callCount: 0,
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        totalTokens: 0
      },
      records: []
    }))
  },
  artifacts: {
    list: vi.fn(async () => []),
    get: vi.fn(async () => {
      throw new Error('Artifact not found')
    }),
    importFiles: vi.fn(async () => [])
  },
  memory: {
    list: vi.fn(async () => []),
    create: vi.fn(async (input) => ({
      ...input,
      id: crypto.randomUUID(),
      confidence: 1,
      salience: 1,
      status: 'confirmed' as const,
      createdAt: '2026-07-31T00:00:00.000Z',
      updatedAt: '2026-07-31T00:00:00.000Z'
    })),
    setStatus: vi.fn(async () => {}),
    remove: vi.fn(async () => {})
  },
  schedules: {
    list: vi.fn(async () => []),
    create: vi.fn(async (input) => ({
      ...input,
      id: crypto.randomUUID(),
      enabled: true,
      createdAt: '2026-07-31T00:00:00.000Z',
      updatedAt: '2026-07-31T00:00:00.000Z'
    })),
    setEnabled: vi.fn(async () => {}),
    remove: vi.fn(async () => {}),
    runNow: vi.fn(async () => {})
  },
  heartbeats: {
    list: vi.fn(async () => []),
    create: vi.fn(async (input) => ({
      ...input,
      id: crypto.randomUUID(),
      nextRunAt: '2026-08-01T09:00:00.000Z',
      createdAt: '2026-08-01T00:00:00.000Z',
      updatedAt: '2026-08-01T00:00:00.000Z'
    })),
    update: vi.fn(async (heartbeatId, input) => ({
      ...input,
      id: heartbeatId,
      nextRunAt: '2026-08-01T09:00:00.000Z',
      createdAt: '2026-08-01T00:00:00.000Z',
      updatedAt: '2026-08-01T00:00:00.000Z'
    })),
    setPaused: vi.fn(async () => {}),
    remove: vi.fn(async () => {}),
    runNow: vi.fn(async (heartbeatId) => ({
      id: crypto.randomUUID(),
      configId: heartbeatId,
      trigger: 'manual' as const,
      scheduledFor: '2026-08-01T00:00:00.000Z',
      status: 'completed' as const,
      attemptCount: 1,
      createdAt: '2026-08-01T00:00:00.000Z',
      updatedAt: '2026-08-01T00:00:00.000Z'
    })),
    history: vi.fn(async () => ({ runs: [], entries: [] }))
  },
  experts: {
    list: vi.fn(async () => []),
    create: vi.fn(async (input) => ({
      ...input,
      id: crypto.randomUUID(),
      enabled: true,
      createdAt: '2026-07-31T00:00:00.000Z',
      updatedAt: '2026-07-31T00:00:00.000Z'
    })),
    update: vi.fn(async (expertId, input) => ({
      ...input,
      id: expertId,
      enabled: true,
      createdAt: '2026-07-31T00:00:00.000Z',
      updatedAt: '2026-08-04T00:00:00.000Z'
    })),
    remove: vi.fn(async () => {})
  },
  capabilities: {
    getSnapshot: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    importSkill: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    removeSkill: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    setSkillEnabled: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    setSkillAssignments: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    saveMcpServer: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    removeMcpServer: vi.fn(async () => ({
      skills: [],
      mcpServers: []
    })),
    testMcpServer: vi.fn(async () => ({
      toolCount: 0,
      tools: []
    }))
  },
  context: {
    selectFiles: vi.fn(async () => []),
    captureScreen: vi.fn(async () => {
      throw new Error('not used')
    }),
    listWindows: vi.fn(async () => []),
    captureWindow: vi.fn(async () => {
      throw new Error('not used')
    }),
    readClipboard: vi.fn(async () => {
      throw new Error('not used')
    }),
    remove: vi.fn(async () => {})
  },
  magicNotes: {
    list: vi.fn(async () => ({ notes: [] })),
    get: vi.fn(async () => {
      throw new Error('not used')
    }),
    create: vi.fn(async () => {
      throw new Error('not used')
    }),
    update: vi.fn(async () => {
      throw new Error('not used')
    }),
    remove: vi.fn(async () => {}),
    createEntry: vi.fn(async () => {
      throw new Error('not used')
    }),
    updateEntry: vi.fn(async () => {
      throw new Error('not used')
    }),
    removeEntry: vi.fn(async () => {
      throw new Error('not used')
    }),
    analyze: vi.fn(async () => {
      throw new Error('not used')
    }),
    analyzeDraft: vi.fn(async () => {
      throw new Error('not used')
    }),
    listTodos: vi.fn(async () => ({ todos: [] })),
    analyzeTodo: vi.fn(async () => {
      throw new Error('not used')
    })
  },
  knowledge: {
    getSnapshot: vi.fn(async () => ({
      libraries: [],
      sources: [],
      documents: [],
      graphNodes: [],
      graphRelations: [],
      evidence: []
    })),
    createLibrary: vi.fn(async (input) => ({
      ...input,
      id: crypto.randomUUID(),
      sourceCount: 0,
      documentCount: 0,
      indexedDocumentCount: 0
    })),
    updateLibrary: vi.fn(async () => {}),
    deleteLibrary: vi.fn(async () => {}),
    selectFiles: vi.fn(async () => {}),
    selectDirectory: vi.fn(async () => {}),
    importDroppedFiles: vi.fn(async () => {}),
    importUrl: vi.fn(async () => {}),
    syncSource: vi.fn(async () => {}),
    pauseSource: vi.fn(async () => {}),
    retrySource: vi.fn(async () => {}),
    removeSource: vi.fn(async () => {}),
    search: vi.fn(async () => []),
    createEntity: vi.fn(async () => {}),
    updateEntity: vi.fn(async () => {}),
    moveEntity: vi.fn(async () => {}),
    deleteEntity: vi.fn(async () => {}),
    mergeEntities: vi.fn(async () => {}),
    createRelation: vi.fn(async () => {}),
    updateRelation: vi.fn(async () => {}),
    deleteRelation: vi.fn(async () => {})
  }
}

describe('App', () => {
  beforeEach(() => {
    localStorage.clear()
    delete document.documentElement.dataset.theme
    document.documentElement.style.colorScheme = ''
    vi.clearAllMocks()
    newConversationListener = undefined
    browserListener = undefined
    maximizedChangedListener = undefined
    speechRecognitionMocks.startPcmRecording.mockResolvedValue({
      result: Promise.resolve({
        audio: new Float32Array([0, 0.25, -0.25]).buffer,
        sampleRate: 16_000
      }),
      stop: vi.fn(),
      cancel: vi.fn()
    })
    Object.defineProperty(navigator, 'mediaDevices', {
      configurable: true,
      value: { getUserMedia: vi.fn() }
    })
    Object.defineProperty(window, 'AudioContext', {
      configurable: true,
      value: class AudioContextMock {}
    })
    vi.mocked(api.speech!.transcribe).mockResolvedValue({
      text: '本地语音结果'
    })
    vi.mocked(api.speech!.cancel).mockResolvedValue(true)
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'model',
      label: 'sonnet-5',
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })
    Object.defineProperty(window, 'goodbuddy', {
      configurable: true,
      value: api
    })
  })

  afterEach(() => {
    cleanup()
    vi.restoreAllMocks()
  })

  it('provides custom minimize, maximize, and close controls', async () => {
    const { unmount } = render(<App />)

    fireEvent.click(screen.getByLabelText('最小化窗口'))
    fireEvent.click(screen.getByLabelText('最大化窗口'))
    fireEvent.click(screen.getByLabelText('关闭窗口'))

    await waitFor(() => {
      expect(api.app.minimize).toHaveBeenCalledOnce()
      expect(api.app.toggleMaximize).toHaveBeenCalledOnce()
      expect(api.app.close).toHaveBeenCalledOnce()
    })
    act(() => maximizedChangedListener?.(true))
    expect(await screen.findByLabelText('还原窗口')).toBeInTheDocument()
    act(() => maximizedChangedListener?.(false))
    expect(screen.getByLabelText('最大化窗口')).toBeInTheDocument()

    unmount()
    expect(removeMaximizedChangedListener).toHaveBeenCalledOnce()
  })

  it('checks for updates silently on startup and only reports a new version', async () => {
    const check = vi.fn(async () => ({
      updateAvailable: true,
      currentVersion: '0.8.4',
      latestVersion: '0.9.0',
      releaseUrl:
        'https://github.com/mesalogo/goodbuddy/releases/tag/v0.9.0',
      target: {
        platform: 'windows' as const,
        arch: 'x64' as const,
        formats: ['nsis', 'portable'],
        files: [
          {
            name: 'GoodBuddy-0.9.0-windows-x64-portable.zip',
            size: 1,
            sha256: 'a'.repeat(64)
          }
        ]
      }
    }))
    api.updates = {
      getSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: true,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      updateSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: true,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      check,
      openReleasePage: vi.fn(async () => {}),
      onResult: vi.fn(() => () => {})
    }
    try {
      render(<App />)
      await waitFor(() => expect(check).toHaveBeenCalledOnce())
      expect(
        await screen.findByText(
          '发现 GoodBuddy 0.9.0，可在“关于与更新”中查看'
        )
      ).toBeInTheDocument()
      expect(api.updates.onResult).not.toHaveBeenCalled()
    } finally {
      delete api.updates
    }
  })

  it('does not disturb startup when updates are current or offline', async () => {
    const currentResult = {
      updateAvailable: false,
      currentVersion: '0.8.4',
      latestVersion: '0.8.4',
      releaseUrl:
        'https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.4',
      target: {
        platform: 'windows' as const,
        arch: 'x64' as const,
        formats: ['nsis', 'portable'],
        files: []
      }
    }
    const check = vi
      .fn()
      .mockResolvedValueOnce(currentResult)
      .mockRejectedValueOnce(new Error('offline'))
    api.updates = {
      getSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: true,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      updateSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: true,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      check,
      openReleasePage: vi.fn(async () => {}),
      onResult: vi.fn(() => () => {})
    }
    try {
      const first = render(<App />)
      await waitFor(() => expect(check).toHaveBeenCalledTimes(1))
      expect(
        screen.queryByText(/发现 GoodBuddy|版本检查失败/u)
      ).not.toBeInTheDocument()

      first.unmount()
      render(<App />)
      await waitFor(() => expect(check).toHaveBeenCalledTimes(2))
      expect(
        screen.queryByText(/发现 GoodBuddy|版本检查失败/u)
      ).not.toBeInTheDocument()
    } finally {
      delete api.updates
    }
  })

  it('keeps rendering when an older preload has no browser bridge', async () => {
    Object.defineProperty(window, 'goodbuddy', {
      configurable: true,
      value: {
        ...api,
        browser: undefined
      }
    })

    render(<App />)

    expect(
      await screen.findByLabelText('向 GoodBuddy 提问')
    ).toBeInTheDocument()
  })

  it('uses local transcription when Electron has no Web Speech API', async () => {
    Object.defineProperty(window, 'SpeechRecognition', {
      configurable: true,
      value: undefined
    })
    Object.defineProperty(window, 'webkitSpeechRecognition', {
      configurable: true,
      value: undefined
    })

    render(<App />)
    fireEvent.click(await screen.findByLabelText('语音输入'))

    await waitFor(() =>
      expect(api.speech?.transcribe).toHaveBeenCalledWith(
        expect.objectContaining({
          sampleRate: 16_000,
          audio: expect.any(ArrayBuffer)
        })
      )
    )
    expect(await screen.findByDisplayValue('本地语音结果')).toBeInTheDocument()
    expect(
      screen.getByText('快捷唤起：', { exact: false })
    ).toHaveTextContent('快捷唤起：Ctrl+Shift+Space')
    expect(
      screen.queryByText(/CommandOrControl/)
    ).not.toBeInTheDocument()
  })

  it('shows a red recording state until microphone capture stops', async () => {
    let resolveRecording!: (value: {
      audio: ArrayBuffer
      sampleRate: 16_000
    }) => void
    const stop = vi.fn()
    speechRecognitionMocks.startPcmRecording.mockResolvedValueOnce({
      result: new Promise((resolve) => {
        resolveRecording = resolve
      }),
      stop,
      cancel: vi.fn()
    })

    render(<App />)
    const input = await screen.findByLabelText('向 GoodBuddy 提问')
    expect(input).toHaveAttribute('rows', '3')
    expect(input).toHaveStyle({ height: '72px' })

    fireEvent.click(screen.getByLabelText('语音输入'))
    const recordingButton = await screen.findByRole('button', {
      name: '停止录音'
    })
    expect(recordingButton).toHaveAttribute('data-state', 'recording')
    expect(recordingButton).toHaveAttribute('aria-pressed', 'true')
    expect(recordingButton).toHaveClass(
      'composer__voice-button--recording'
    )

    fireEvent.click(recordingButton)
    expect(stop).toHaveBeenCalledOnce()
    resolveRecording({
      audio: new Float32Array([0, 0.25, -0.25]).buffer,
      sampleRate: 16_000
    })
    expect(
      await screen.findByDisplayValue('本地语音结果')
    ).toBeInTheDocument()
  })

  it('keeps conversation actions in the conversation list', async () => {
    const writeText = vi.fn(async () => {})
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: { writeText }
    })
    const { container } = render(<App />)
    const topbar = container.querySelector<HTMLElement>('.topbar')
    const conversationList =
      container.querySelector<HTMLElement>('.conversation-list')
    expect(topbar).not.toBeNull()
    expect(conversationList).not.toBeNull()
    if (!topbar || !conversationList) {
      return
    }

    expect(within(topbar).queryByLabelText('专家角色')).not.toBeInTheDocument()
    expect(screen.getByLabelText('专家角色').closest('.composer')).not.toBeNull()

    const appMenuTrigger = within(topbar).getByLabelText('应用菜单')
    fireEvent.click(appMenuTrigger)
    expect(
      screen.queryByRole('menuitem', { name: '重命名会话' })
    ).not.toBeInTheDocument()
    expect(
      screen.getByRole('menuitem', { name: '安全与 Runtime 设置' })
    ).toBeVisible()
    await waitFor(() =>
      expect(
        screen.getByRole('menuitem', { name: '安全与 Runtime 设置' })
      ).toHaveFocus()
    )
    fireEvent.keyDown(document, { key: 'ArrowDown' })
    expect(screen.getByRole('menuitem', { name: '使用帮助' })).toHaveFocus()
    fireEvent.keyDown(document, { key: 'Escape' })
    expect(appMenuTrigger).toHaveFocus()
    expect(screen.queryByRole('menu')).not.toBeInTheDocument()

    const conversationMenuTrigger = within(
      conversationList
    ).getByLabelText('更多会话操作 新对话')
    fireEvent.click(conversationMenuTrigger)
    const renameButton = within(conversationList).getByRole('button', {
      name: '重命名会话'
    })
    expect(renameButton).toBeVisible()
    expect(
      within(conversationList).getByRole('button', {
        name: '复制完整会话'
      })
    ).toBeVisible()
    expect(
      within(conversationList).getByRole('button', {
        name: '导出 Markdown'
      })
    ).toBeVisible()

    fireEvent.click(renameButton)
    const renameInput = within(conversationList).getByLabelText(
      '重命名会话 新对话'
    )
    fireEvent.change(renameInput, {
      target: { value: '重命名后的会话' }
    })
    fireEvent.submit(renameInput.closest('form')!)
    expect(
      within(conversationList).getByText('重命名后的会话')
    ).toBeInTheDocument()
    await waitFor(() => expect(conversationMenuTrigger).toHaveFocus())

    fireEvent.click(screen.getByRole('button', { name: '知识库' }))
    fireEvent.click(
      within(conversationList).getByLabelText(
        '更多会话操作 重命名后的会话'
      )
    )
    fireEvent.click(
      within(conversationList).getByRole('button', {
        name: '复制完整会话'
      })
    )
    await waitFor(() => expect(writeText).toHaveBeenCalledOnce())
    expect(await screen.findByRole('status')).toBeVisible()
  })

  it('requires an accessible confirmation before permanently deleting a conversation', async () => {
    render(<App />)
    const menuTrigger = screen.getByLabelText(
      '更多会话操作 新对话'
    )
    fireEvent.click(menuTrigger)
    const deleteTrigger = screen.getByRole('button', {
      name: '删除对话 新对话'
    })
    fireEvent.click(deleteTrigger)

    const dialog = screen.getByRole('alertdialog', {
      name: '确认永久删除对话 新对话'
    })
    expect(dialog).toHaveTextContent('将永久删除此会话的全部内容')
    expect(dialog).toHaveTextContent(
      '如果此会话有正在运行的任务，也会同时停止'
    )
    expect(dialog).toHaveTextContent('此操作不可恢复')
    const cancel = screen.getByRole('button', {
      name: '取消删除对话 新对话'
    })
    const confirm = screen.getByRole('button', {
      name: '确认永久删除对话 新对话'
    })
    expect(cancel).toHaveFocus()

    fireEvent.keyDown(dialog, { key: 'Tab' })
    expect(confirm).toHaveFocus()
    fireEvent.keyDown(dialog, { key: 'Tab', shiftKey: true })
    expect(cancel).toHaveFocus()
    fireEvent.keyDown(dialog, { key: 'Escape' })
    await waitFor(() =>
      expect(
        screen.getByRole('button', { name: '删除对话 新对话' })
      ).toHaveFocus()
    )
    expect(
      screen.queryByRole('alertdialog', {
        name: '确认永久删除对话 新对话'
      })
    ).not.toBeInTheDocument()

    fireEvent.click(
      screen.getByRole('button', { name: '删除对话 新对话' })
    )
    fireEvent.click(
      screen.getByRole('button', {
        name: '确认永久删除对话 新对话'
      })
    )
    await waitFor(() =>
      expect(
        screen.queryByRole('alertdialog', {
          name: '确认永久删除对话 新对话'
        })
      ).not.toBeInTheDocument()
    )
  })

  it('keeps a conversation when cancelling its active task fails', async () => {
    vi.mocked(api.agent.cancel).mockRejectedValueOnce(
      new Error('cancel failed')
    )
    render(<App />)
    const title = '取消失败时保留会话'

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: title }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())

    fireEvent.click(
      await screen.findByLabelText(`更多会话操作 ${title}`)
    )
    fireEvent.click(
      screen.getByRole('button', { name: `删除对话 ${title}` })
    )
    const confirm = screen.getByRole('button', {
      name: `确认永久删除对话 ${title}`
    })
    fireEvent.click(confirm)

    await waitFor(() =>
      expect(api.agent.cancel).toHaveBeenCalledOnce()
    )
    expect(
      await screen.findByText('停止会话中的运行任务失败，尚未删除对话')
    ).toBeInTheDocument()
    expect(
      screen.getByRole('alertdialog', {
        name: `确认永久删除对话 ${title}`
      })
    ).toBeInTheDocument()
    await waitFor(() => expect(confirm).toBeEnabled())

    vi.mocked(api.agent.cancel).mockResolvedValueOnce()
    fireEvent.click(confirm)
    await waitFor(() =>
      expect(
        screen.queryByRole('alertdialog', {
          name: `确认永久删除对话 ${title}`
        })
      ).not.toBeInTheDocument()
    )
  })

  it('renders streamed reasoning, text, and tools in event order', async () => {
    render(<App />)

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '帮我分析项目' }
    })
    await waitFor(() => expect(screen.getByLabelText('发送')).toBeEnabled())
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    expect(request?.prompt).toBe('帮我分析项目')
    expect(request?.runtimeSelection).toEqual({
      provider: 'model',
      profileId: modelProfileId
    })
    expect(
      screen
        .getByText('正在连接 Agent Runtime')
        .querySelector('.message__status-dot')
    ).toHaveClass('message__status-dot--active')
    const userMessage = screen
      .getAllByText('帮我分析项目')
      .map((element) => element.closest('article'))
      .find((element) => element?.classList.contains('message--user'))
    expect(userMessage).toHaveClass('message--user')

    act(() => {
      if (!request) {
        throw new Error('Missing request')
      }
      agentListener?.({
        requestId: request.requestId,
        type: 'text',
        delta: '这是'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'text',
        delta: '回答内容'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'reasoning',
        delta: '先检查项目结构'
      })
    })

    expect(await screen.findByText('这是回答内容')).toBeInTheDocument()
    const streamingReasoning = screen
      .getByText('正在推理')
      .closest('details')
    expect(streamingReasoning).toHaveAttribute('open')
    expect(screen.getByText('先检查项目结构')).toBeInTheDocument()

    act(() => {
      if (!request) {
        throw new Error('Missing request')
      }
      agentListener?.({
        requestId: request.requestId,
        type: 'tool',
        callId: 'call-1',
        name: 'read',
        state: 'running',
        summary: 'OpenCode 工具：read',
        input: '{"path":"README.md"}'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'tool',
        callId: 'call-2',
        name: 'grep',
        state: 'completed',
        summary: 'OpenCode 工具：grep',
        input: '{"pattern":"runtime"}',
        output: 'src/main/agent/runtime.ts'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'reasoning',
        delta: '再检查关键文件'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'text',
        delta: '最终结论'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'tool',
        callId: 'call-1',
        name: 'read',
        state: 'completed',
        summary: 'OpenCode 工具：read',
        input: '{"path":"README.md"}',
        output:
          'README contents\nAuthorization: Bearer visible-token'
      })
    })

    const assistantArticle = screen
      .getByText('最终结论')
      .closest('article')
    const orderedBlocks = [
      ...assistantArticle!.querySelectorAll('.message-blocks > *')
    ].map((element) => element.textContent)
    expect(orderedBlocks).toEqual([
      expect.stringContaining('这是回答内容'),
      expect.stringContaining('先检查项目结构'),
      expect.stringContaining('OpenCode 工具：read'),
      expect.stringContaining('再检查关键文件'),
      expect.stringContaining('最终结论')
    ])
    expect(
      screen.getAllByText('OpenCode 工具：read')
    ).toHaveLength(1)
    expect(
      screen.getByRole('region', { name: '工具执行，共 2 项' })
    ).toBeInTheDocument()
    const readTool = screen.getByText('read').closest('details')
    const rawToolOutput =
      'README contents\nAuthorization: Bearer visible-token'
    expect(readTool).not.toHaveAttribute('open')
    expect(
      screen.getByText(
        (_, element) => element?.textContent === rawToolOutput
      )
    ).not.toBeVisible()
    fireEvent.click(screen.getByText('read').closest('summary')!)
    expect(readTool).toHaveAttribute('open')
    expect(within(readTool!).getByText('调用参数')).toBeVisible()
    expect(
      within(readTool!).getByText(
        (_, element) => element?.textContent === rawToolOutput
      )
    ).toBeVisible()

    act(() => {
      if (!request) {
        throw new Error('Missing request')
      }
      agentListener?.({
        requestId: request.requestId,
        type: 'done'
      })
    })

    const completedReasoning = await screen.findAllByText('推理过程')
    expect(completedReasoning).toHaveLength(2)
    for (const reasoning of completedReasoning) {
      expect(reasoning.closest('details')).not.toHaveAttribute('open')
    }
    expect(screen.getByText('项目：默认项目')).toHaveClass('scope-badge')
  })

  it('submits knowledge scope without eager search or prompt injection and merges runtime references', async () => {
    const libraryId = '11111111-1111-4111-8111-111111111111'
    vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce({
      libraries: [
        {
          id: libraryId,
          name: '产品知识',
          description: '',
          storageMode: 'managed',
          graphEnabled: false,
          graphStrategy: 'rules',
          sourceCount: 1,
          documentCount: 1,
          indexedDocumentCount: 1
        }
      ],
      sources: [],
      documents: [],
      graphNodes: [],
      graphRelations: [],
      evidence: []
    })
    render(<App />)
    await screen.findByRole('button', {
      name: '选择知识库，本次已启用 1 个'
    })

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '发布流程是什么？' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    expect(request).toMatchObject({
      prompt: '发布流程是什么？',
      knowledgeLibraryIds: [libraryId]
    })
    expect(api.knowledge.search).not.toHaveBeenCalled()
    expect(
      screen.queryByText(/查看 \d+ 条证据引用/u)
    ).not.toBeInTheDocument()

    act(() => {
      if (!request) {
        throw new Error('Missing request')
      }
      for (let batch = 0; batch < 5; batch += 1) {
        agentListener?.({
          requestId: request.requestId,
          type: 'source-references',
          references: Array.from({ length: 25 }, (_, index) => ({
            libraryId,
            libraryName: '产品知识',
            documentId: crypto.randomUUID(),
            documentName: `发布手册 ${batch}-${index}`,
            sourceName: `release-${batch}-${index}.md`,
            locator: `第 ${batch}-${index} 节`,
            snippet: `证据 ${batch}-${index}`,
            rank: index + 1
          }))
        })
      }
      agentListener?.({
        requestId: request.requestId,
        type: 'done'
      })
    })
    expect(
      await screen.findByText('查看 20 条证据引用')
    ).toBeInTheDocument()
    await waitFor(
      () => {
        const persistedMessages = vi
          .mocked(api.conversations.replace)
          .mock.calls.flatMap(([conversations]) =>
            conversations.flatMap((conversation) => conversation.messages)
          )
        const persisted = persistedMessages
          .filter((message) => message.role === 'assistant')
          .slice()
          .reverse()
          .find(
            (message) => message.sourceReferences?.length === 20
          )
        expect(persisted?.sourceReferences).toHaveLength(20)
        expect(persisted?.sources).toHaveLength(100)
      },
      { timeout: 2_000 }
    )
  })

  it('keeps a running response visible when cancellation fails', async () => {
    vi.mocked(api.agent.cancel).mockRejectedValueOnce(
      new Error('cancel failed')
    )
    render(<App />)

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '开始一个长任务' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())

    fireEvent.click(await screen.findByLabelText('停止生成'))

    await waitFor(() =>
      expect(api.agent.cancel).toHaveBeenCalledOnce()
    )
    expect(
      await screen.findByText(/停止生成失败，请重试/u)
    ).toBeInTheDocument()
    expect(screen.getByLabelText('停止生成')).toBeInTheDocument()
    vi.useFakeTimers()
    try {
      act(() => vi.advanceTimersByTime(10_000))
      expect(
        screen.getByText(/停止生成失败，请重试/u)
      ).toBeInTheDocument()
      fireEvent.click(
        screen.getByRole('button', {
          name: '关闭通知'
        })
      )
      expect(
        screen.queryByText(/停止生成失败，请重试/u)
      ).not.toBeInTheDocument()
    } finally {
      vi.useRealTimers()
    }
  })

  it('keeps sent documents and images in conversation history', async () => {
    const documentAttachment = {
      id: '00000000-0000-4000-8000-000000000301',
      name: '需求说明.md',
      size: 2_048,
      preview: '需要保留在用户消息中的文档',
      kind: 'text' as const
    }
    const imageAttachment = {
      id: '00000000-0000-4000-8000-000000000302',
      name: '页面截图.png',
      size: 4_096,
      preview: '1280 × 720',
      kind: 'image' as const,
      thumbnailUrl:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB',
      contentUrl:
        'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2Q=='
    }
    vi.mocked(api.context.selectFiles).mockResolvedValueOnce([
      documentAttachment,
      imageAttachment
    ])
    render(<App />)

    fireEvent.click(await screen.findByLabelText('添加附件'))
    const composer = screen
      .getByLabelText('向 GoodBuddy 提问')
      .closest<HTMLElement>('.composer')
    expect(composer).not.toBeNull()
    if (!composer) {
      return
    }
    expect(
      await within(composer).findByText('需求说明.md')
    ).toBeInTheDocument()
    expect(
      within(composer).getByText('页面截图.png')
    ).toBeInTheDocument()
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '分析这些附件' }
    })
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    expect(run.mock.calls[0]?.[0].contextIds).toEqual([
      documentAttachment.id,
      imageAttachment.id
    ])
    const userArticle = screen
      .getAllByText('分析这些附件')
      .map((element) => element.closest('article'))
      .find((element) => element?.classList.contains('message--user'))
    expect(userArticle).not.toBeNull()
    if (!userArticle) {
      return
    }
    const anchorClick = vi
      .spyOn(HTMLAnchorElement.prototype, 'click')
      .mockImplementation(() => {})
    expect(within(userArticle).getByText('需求说明.md')).toBeInTheDocument()
    expect(within(userArticle).getByText('2 KB')).toBeInTheDocument()
    expect(
      within(userArticle).getByRole('img', { name: '页面截图.png' })
    ).toHaveAttribute('src', imageAttachment.contentUrl)
    fireEvent.click(
      within(userArticle).getByRole('button', {
        name: '查看图片 页面截图.png'
      })
    )
    const imageDialog = await screen.findByRole('dialog', {
      name: '页面截图.png'
    })
    expect(
      within(imageDialog).getByRole('img', { name: '页面截图.png' })
    ).toHaveAttribute('src', imageAttachment.contentUrl)
    fireEvent.click(
      within(imageDialog).getByRole('button', {
        name: '关闭图片查看器'
      })
    )
    expect(
      screen.queryByRole('dialog', { name: '页面截图.png' })
    ).not.toBeInTheDocument()
    fireEvent.click(
      within(userArticle).getByRole('button', {
        name: '下载图片 页面截图.png'
      })
    )
    expect(anchorClick).toHaveBeenCalledOnce()
    await waitFor(
      () =>
        expect(api.conversations.replace).toHaveBeenCalledWith(
          expect.arrayContaining([
            expect.objectContaining({
              messages: expect.arrayContaining([
                expect.objectContaining({
                  role: 'user',
                  attachments: [
                    documentAttachment,
                    imageAttachment
                  ]
                })
              ])
            })
          ])
        ),
      { timeout: 2_000 }
    )
  })

  it('sends and renders five selected images together', async () => {
    const imageAttachments = Array.from({ length: 5 }, (_, index) => ({
      id: `00000000-0000-4000-8000-00000000031${index}`,
      name: `参考图-${index + 1}.png`,
      size: 4_096,
      preview: '640 × 480',
      kind: 'image' as const,
      thumbnailUrl:
        'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2Q==',
      contentUrl:
        'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2Q=='
    }))
    vi.mocked(api.context.selectFiles).mockResolvedValueOnce(imageAttachments)
    render(<App />)

    fireEvent.click(await screen.findByLabelText('添加附件'))
    const composer = screen
      .getByLabelText('向 GoodBuddy 提问')
      .closest<HTMLElement>('.composer')
    expect(composer).not.toBeNull()
    if (!composer) {
      return
    }
    await waitFor(() =>
      expect(
        within(composer).getAllByText(/^参考图-\d\.png$/u)
      ).toHaveLength(5)
    )
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '比较这五张图片' }
    })
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    expect(run.mock.calls[0]?.[0].contextIds).toEqual(
      imageAttachments.map((attachment) => attachment.id)
    )
    const userArticle = screen
      .getAllByText('比较这五张图片')
      .map((element) => element.closest('article'))
      .find((element) => element?.classList.contains('message--user'))
    expect(userArticle).not.toBeNull()
    if (!userArticle) {
      return
    }
    expect(within(userArticle).getAllByRole('img')).toHaveLength(5)
    expect(within(userArticle).getByLabelText('消息附件')).toHaveClass(
      'message-attachments'
    )
  })

  it('lists capturable application windows vertically before capture', async () => {
    vi.mocked(api.context.listWindows).mockResolvedValueOnce([
      { id: 'window-1', name: 'Visual Studio Code' },
      { id: 'window-2', name: 'Browser' },
      { id: 'window-3', name: 'Terminal' }
    ])
    vi.mocked(api.context.captureWindow).mockResolvedValueOnce({
      id: '00000000-0000-4000-8000-000000000303',
      name: '窗口-Browser.jpg',
      size: 120_000,
      preview: '1280 × 800',
      kind: 'image',
      thumbnailUrl:
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB'
    })
    render(<App />)

    fireEvent.click(await screen.findByLabelText('捕获应用窗口'))

    const dialog = await screen.findByRole('dialog', {
      name: '选择应用窗口'
    })
    const list = within(dialog).getByLabelText('可捕获的应用窗口')
    expect(list).toHaveClass('window-capture-dialog__list')
    expect(within(list).getAllByRole('button')).toHaveLength(3)

    fireEvent.click(
      within(list).getByRole('button', { name: 'Browser' })
    )
    await waitFor(() =>
      expect(api.context.captureWindow).toHaveBeenCalledWith('window-2')
    )
    const composer = screen
      .getByLabelText('向 GoodBuddy 提问')
      .closest<HTMLElement>('.composer')
    expect(composer).not.toBeNull()
    if (!composer) {
      return
    }
    expect(
      await within(composer).findByText('窗口-Browser.jpg')
    ).toBeInTheDocument()
  })

  it('keeps a draft in chat when Enter is pressed while the runtime loads', async () => {
    vi.mocked(api.agent.getStatus).mockReturnValue(
      new Promise(() => {})
    )
    render(<App />)

    const composer = screen.getByLabelText('向 GoodBuddy 提问')
    fireEvent.change(composer, {
      target: { value: '等待 Runtime' }
    })
    fireEvent.keyDown(composer, { key: 'Enter' })

    expect(composer).toHaveValue('等待 Runtime')
    expect(
      screen.queryByRole('heading', { name: '设置中心' })
    ).not.toBeInTheDocument()
    expect(
      await screen.findByText('Agent Runtime 正在加载，请稍后重试')
    ).toBeInTheDocument()
    expect(run).not.toHaveBeenCalled()
  })

  it('keeps a new-conversation draft in chat when the runtime is unavailable', async () => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'setup',
      label: '需要配置模型',
      available: false,
      supportsToolExecution: false,
      detail: '请配置模型'
    })
    render(<App />)

    expect(
      await screen.findByRole('heading', { name: '设置中心' })
    ).toBeInTheDocument()
    const newConversation = screen.getByRole('button', {
      name: /新建对话/u
    })
    fireEvent.click(newConversation)

    const composer = screen.getByLabelText('向 GoodBuddy 提问')
    await waitFor(() => expect(composer).toHaveFocus())
    fireEvent.change(composer, {
      target: { value: '保留这条草稿' }
    })
    fireEvent.keyDown(composer, { key: 'Enter' })

    expect(composer).toHaveValue('保留这条草稿')
    expect(
      screen.queryByRole('heading', { name: '设置中心' })
    ).not.toBeInTheDocument()
    expect(
      await screen.findByText(/请先配置可用的模型或 Agent Runtime/u)
    ).toBeInTheDocument()
    expect(run).not.toHaveBeenCalled()
  })

  it('opens chat and focuses the composer for tray conversations', async () => {
    render(<App />)

    fireEvent.click(await screen.findByText('本地工作区'))
    expect(
      await screen.findByRole('heading', { name: '设置中心' })
    ).toBeInTheDocument()

    act(() => newConversationListener?.())

    const composer = await screen.findByLabelText('向 GoodBuddy 提问')
    await waitFor(() => expect(composer).toHaveFocus())
  })

  it('reuses the active empty conversation and preserves its draft', async () => {
    render(<App />)

    const composer = await screen.findByLabelText('向 GoodBuddy 提问')
    fireEvent.change(composer, {
      target: { value: '尚未发送的草稿' }
    })
    const newConversation = screen.getByRole('button', {
      name: /新建对话/u
    })
    fireEvent.click(newConversation)
    fireEvent.click(newConversation)

    expect(composer).toHaveValue('尚未发送的草稿')
    expect(
      screen.getAllByRole('button', {
        name: '更多会话操作 新对话'
      })
    ).toHaveLength(1)
  })

  it('coalesces batched new-conversation requests after a used conversation', async () => {
    render(<App />)

    fireEvent.change(await screen.findByLabelText('向 GoodBuddy 提问'), {
      target: { value: '已有内容' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const requestId = run.mock.calls[0]?.[0].requestId
    act(() => {
      if (requestId) {
        agentListener?.({ requestId, type: 'done' })
      }
      newConversationListener?.()
      newConversationListener?.()
    })

    expect(
      screen.getAllByRole('button', {
        name: /^更多会话操作/u
      })
    ).toHaveLength(2)
  })

  it('opens a workspace Markdown file in the right-side preview', async () => {
    vi.mocked(api.workspace.listDirectory).mockResolvedValue({
      path: '',
      entries: [
        {
          name: 'README.md',
          path: 'README.md',
          type: 'file'
        }
      ],
      truncated: false
    })
    vi.mocked(api.workspace.readFile).mockResolvedValue({
      path: 'README.md',
      name: 'README.md',
      content: '# 工作区说明',
      mimeType: 'text/markdown',
      size: 19
    })
    render(<App />)

    fireEvent.click(screen.getByLabelText('切换助手工作栏'))
    fireEvent.click(await screen.findByRole('tab', { name: '工作区' }))
    fireEvent.click(
      await screen.findByRole('button', { name: 'README.md' })
    )

    expect(
      await screen.findByRole('heading', { name: '工作区说明' })
    ).toBeInTheDocument()
    expect(
      screen.getByRole('tab', { name: '工作区' })
    ).toHaveAttribute('aria-selected', 'true')
    expect(
      screen.queryByRole('tab', { name: '预览' })
    ).not.toBeInTheDocument()
    expect(api.workspace.readFile).toHaveBeenCalledWith(
      projectId,
      'README.md'
    )
    fireEvent.click(
      screen.getByRole('button', { name: '返回工作区' })
    )
    expect(
      await screen.findByRole('button', { name: 'README.md' })
    ).toBeInTheDocument()
  })

  it('opens workspace entries from their row actions', async () => {
    vi.mocked(api.workspace.listDirectory).mockResolvedValue({
      path: '',
      entries: [
        { name: 'docs', path: 'docs', type: 'directory' },
        { name: 'README.md', path: 'README.md', type: 'file' }
      ],
      truncated: false
    })
    render(<App />)

    fireEvent.click(screen.getByLabelText('切换助手工作栏'))
    fireEvent.click(await screen.findByRole('tab', { name: '工作区' }))
    fireEvent.click(
      await screen.findByRole('button', {
        name: '在系统资源管理器中打开文件夹 docs'
      })
    )
    fireEvent.click(
      screen.getByRole('button', {
        name: '使用默认应用打开文件 README.md'
      })
    )
    await waitFor(() =>
      expect(api.workspace.openPath).toHaveBeenCalledWith(
        projectId,
        'docs',
        'directory'
      )
    )
    expect(api.workspace.openPath).toHaveBeenCalledWith(
      projectId,
      'README.md',
      'file'
    )
  })

  it('refreshes generated workspace files when a run completes', async () => {
    vi.mocked(api.workspace.getChanges)
      .mockResolvedValueOnce({
        rootPath: project.rootPath,
        available: true,
        status: '',
        patch: '',
        files: [],
        truncated: false
      })
      .mockResolvedValueOnce({
        rootPath: project.rootPath,
        available: true,
        status: '?? generated.md',
        patch: '',
        files: [{ path: 'generated.md', status: '??' }],
        truncated: false
      })
    render(<App />)

    fireEvent.click(screen.getByLabelText('切换助手工作栏'))
    fireEvent.click(await screen.findByRole('tab', { name: '工作区' }))
    await waitFor(() =>
      expect(api.workspace.getChanges).toHaveBeenCalledOnce()
    )
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '生成文件' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const requestId = run.mock.calls[0]?.[0].requestId
    act(() => {
      if (requestId) {
        agentListener?.({ requestId, type: 'done' })
      }
    })

    expect(await screen.findByText('generated.md')).toBeInTheDocument()
  })

  it('ignores stale Git changes after switching projects', async () => {
    const secondProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000102',
      name: '第二项目',
      rootPath: 'C:\\Second'
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      secondProject
    ])
    let resolveFirst:
      | ((value: Awaited<ReturnType<DesktopApi['workspace']['getChanges']>>) => void)
      | undefined
    let resolveSecond:
      | ((value: Awaited<ReturnType<DesktopApi['workspace']['getChanges']>>) => void)
      | undefined
    vi.mocked(api.workspace.getChanges)
      .mockImplementationOnce(
        () =>
          new Promise((resolve) => {
            resolveFirst = resolve
          })
      )
      .mockImplementationOnce(
        () =>
          new Promise((resolve) => {
            resolveSecond = resolve
          })
      )
    render(<App />)

    fireEvent.click(screen.getByLabelText('切换助手工作栏'))
    fireEvent.click(await screen.findByRole('tab', { name: '工作区' }))
    await waitFor(() =>
      expect(api.workspace.getChanges).toHaveBeenCalledWith(projectId)
    )
    fireEvent.change(screen.getByLabelText('当前项目'), {
      target: { value: secondProject.id }
    })
    await waitFor(() =>
      expect(api.workspace.getChanges).toHaveBeenCalledWith(
        secondProject.id
      )
    )

    resolveSecond?.({
      rootPath: secondProject.rootPath,
      available: true,
      status: '?? second.md',
      patch: '',
      files: [{ path: 'second.md', status: '??' }],
      truncated: false
    })
    expect(await screen.findByText('second.md')).toBeInTheDocument()
    resolveFirst?.({
      rootPath: project.rootPath,
      available: true,
      status: '?? stale.md',
      patch: '',
      files: [{ path: 'stale.md', status: '??' }],
      truncated: false
    })

    await waitFor(() =>
      expect(screen.queryByText('stale.md')).not.toBeInTheDocument()
    )
    expect(screen.getByText('second.md')).toBeInTheDocument()
  })

  it('applies and persists a dark appearance from Settings', async () => {
    render(<App />)

    fireEvent.click(await screen.findByText('本地工作区'))
    fireEvent.click(screen.getByRole('tab', { name: '外观' }))
    fireEvent.click(screen.getByRole('radio', { name: /暗色/u }))

    await waitFor(() =>
      expect(document.documentElement.dataset.theme).toBe('dark')
    )
    expect(document.documentElement.style.colorScheme).toBe('dark')
    expect(localStorage.getItem('goodbuddy.appearance-theme')).toBe(
      'dark'
    )
  })

  it('loads token usage in activity and refreshes it when a run finishes', async () => {
    vi.mocked(api.usage.getTokenSummary).mockResolvedValueOnce({
      totals: {
        callCount: 1,
        input: 100,
        output: 20,
        cacheRead: 0,
        cacheWrite: 0,
        totalTokens: 120
      },
      records: [
        {
          requestId: 'usage-request-1',
          projectId,
          projectName: project.name,
          conversationId: 'usage-conversation-1',
          conversationTitle: '用量会话',
          runtime: 'model',
          provider: 'anthropic',
          model: 'sonnet-5',
          callCount: 1,
          input: 100,
          output: 20,
          cacheRead: 0,
          cacheWrite: 0,
          totalTokens: 120
        }
      ]
    })

    render(<App />)
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '统计用量' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }

    fireEvent.click(screen.getByText('任务与活动'))
    const stats = await screen.findByLabelText('Token 用量统计')
    expect(
      screen.getByRole('heading', { level: 1, name: '任务与活动' })
    ).toBeInTheDocument()
    expect(screen.queryByLabelText('专家角色')).not.toBeInTheDocument()
    expect(
      screen.queryByLabelText('切换助手工作栏')
    ).not.toBeInTheDocument()
    await waitFor(() =>
      expect(api.usage.getTokenSummary).toHaveBeenCalledOnce()
    )
    expect(within(stats).getByText('120')).toBeInTheDocument()

    vi.mocked(api.usage.getTokenSummary).mockResolvedValueOnce({
      totals: {
        callCount: 2,
        input: 300,
        output: 45,
        cacheRead: 10,
        cacheWrite: 5,
        totalTokens: 360
      },
      records: [
        {
          requestId: request.requestId,
          projectId,
          projectName: project.name,
          conversationId: request.conversationId,
          conversationTitle: '用量会话',
          runtime: 'model',
          provider: 'anthropic',
          model: 'sonnet-5',
          callCount: 2,
          input: 300,
          output: 45,
          cacheRead: 10,
          cacheWrite: 5,
          totalTokens: 360
        }
      ]
    })
    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'done'
      })
    })

    await waitFor(() =>
      expect(api.usage.getTokenSummary).toHaveBeenCalledTimes(2)
    )
    expect(within(stats).getByText('345')).toBeInTheDocument()
  })

  it('offers only Ask and Execute in visible work mode controls', async () => {
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    expect(
      within(mode)
        .getAllByRole('option')
        .map((option) => option.textContent)
    ).toEqual(['Ask · 只读问答', 'Execute · 受控执行'])

    fireEvent.click(screen.getByLabelText('新建项目'))
    const dialog = screen.getByRole('dialog', { name: '新建项目' })
    const defaultMode = within(dialog).getByRole('combobox', {
      name: '默认模式'
    })
    expect(
      within(defaultMode)
        .getAllByRole('option')
        .map((option) => option.textContent)
    ).toEqual(['Ask · 只读问答', 'Execute · 受控执行'])
    expect(screen.queryByRole('option', { name: /Plan/u })).toBeNull()
  })

  it('groups composer tools and exposes clear control descriptions', async () => {
    render(<App />)

    const composer = (await screen.findByLabelText(
      '向 GoodBuddy 提问'
    )).closest<HTMLElement>('.composer')
    expect(composer).not.toBeNull()

    const contentTools = within(composer!).getByRole('group', {
      name: '添加内容'
    })
    expect(
      within(contentTools).getByRole('button', { name: '添加附件' })
    ).toHaveAttribute('title', '添加附件')
    expect(
      within(contentTools).getByRole('button', { name: '语音输入' })
    ).toHaveAttribute(
      'title',
      '语音转文字，转写后可编辑再发送'
    )

    const conversationSettings = within(composer!).getByRole(
      'group',
      { name: '对话设置' }
    )
    expect(
      within(conversationSettings).getByLabelText('专家角色')
    ).toBeInTheDocument()
    expect(
      within(conversationSettings).getByLabelText('工作模式')
    ).toBeInTheDocument()
    expect(
      within(conversationSettings).getByRole('button', {
        name: /默认模型/u
      })
    ).toHaveAttribute(
      'title',
      expect.stringContaining('Runtime 和模型')
    )
  })

  it('normalizes a legacy Plan project default to Ask', async () => {
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      {
        ...project,
        defaultWorkMode: 'plan'
      }
    ])
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    expect(mode).toHaveValue('ask')
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '制定发布方案' }
    })
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          prompt: '制定发布方案',
          workMode: 'ask'
        })
      )
    )
  })

  it('restores and persists the last active project', async () => {
    const secondProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000102',
      name: '第二项目',
      rootPath: 'C:\\Second',
      defaultWorkMode: 'execute' as const
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      secondProject
    ])
    localStorage.setItem(
      'goodbuddy.active-project.v1',
      secondProject.id
    )

    render(<App />)

    expect(await screen.findByLabelText('当前项目')).toHaveValue(
      secondProject.id
    )
    expect(screen.getByLabelText('工作模式')).toHaveValue('execute')

    fireEvent.change(screen.getByLabelText('当前项目'), {
      target: { value: project.id }
    })
    await waitFor(() =>
      expect(
        localStorage.getItem('goodbuddy.active-project.v1')
      ).toBe(project.id)
    )
  })

  it('keeps channel projects empty until a client message creates a remote conversation', async () => {
    const channelProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000201',
      name: '微信 ClawBot',
      kind: 'channel' as const,
      channel: 'weixin' as const,
      runtimeSelection: {
        provider: 'model' as const,
        profileId: modelProfileId
      }
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      channelProject
    ])
    render(<App />)

    await screen.findByRole('option', { name: '微信 ClawBot' })
    fireEvent.change(await screen.findByLabelText('当前项目'), {
      target: { value: channelProject.id }
    })

    expect(
      screen.queryByRole('button', { name: /新建对话/u })
    ).not.toBeInTheDocument()
    expect(screen.queryByText('Ctrl N')).not.toBeInTheDocument()
    expect(
      screen.getAllByText('尚无远程会话').length
    ).toBeGreaterThan(0)
    expect(
      screen.getByText(
        '请先连接微信 ClawBot，远程用户发送第一条消息后，会话会自动出现在这里。'
      )
    ).toBeInTheDocument()

    fireEvent.keyDown(document, {
      key: 'n',
      ctrlKey: true
    })
    expect(
      await screen.findByText(
        '通道项目的会话由客户端收到新消息后自动创建'
      )
    ).toBeInTheDocument()
    await act(
      () =>
        new Promise((resolve) => {
          setTimeout(resolve, 550)
        })
    )
    expect(api.conversations.replace).not.toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          projectId: channelProject.id,
          remote: undefined
        })
      ])
    )
    expect(screen.getAllByText('尚无远程会话').length).toBeGreaterThan(0)
  })

  it('shows client-created remote conversations without obsolete approval copy', async () => {
    const channelProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000201',
      name: '微信 ClawBot',
      kind: 'channel' as const,
      channel: 'weixin' as const,
      runtimeSelection: {
        provider: 'model' as const,
        profileId: modelProfileId
      }
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      channelProject
    ])
    vi.mocked(api.conversations.list).mockResolvedValueOnce([
      {
        id: '00000000-0000-4000-8000-000000000301',
        projectId: channelProject.id,
        runtimeSelection: channelProject.runtimeSelection,
        remote: {
          channel: 'weixin',
          accountDisplay: '发送者 ****0001',
          conversationType: 'direct'
        },
        title: '微信 ClawBot · ****0001',
        updatedAt: 1_775_000_000_000,
        messages: []
      }
    ])
    render(<App />)

    await screen.findByRole('option', { name: '微信 ClawBot' })
    fireEvent.change(await screen.findByLabelText('当前项目'), {
      target: { value: channelProject.id }
    })

    expect(
      screen.getAllByRole('button', {
        name: /微信 ClawBot · \*{4}0001/u
      }).length
    ).toBeGreaterThan(0)
    expect(
      screen.getByText(
        '请在 微信 ClawBot 客户端继续发送消息。本窗口用于查看历史、任务与执行结果。'
      )
    ).toBeInTheDocument()
    expect(screen.queryByText(/审批执行/u)).not.toBeInTheDocument()
  })

  it('falls back when the last active project is no longer available', async () => {
    localStorage.setItem(
      'goodbuddy.active-project.v1',
      '00000000-0000-4000-8000-000000000999'
    )

    render(<App />)

    expect(await screen.findByLabelText('当前项目')).toHaveValue(
      project.id
    )
    await waitFor(() =>
      expect(
        localStorage.getItem('goodbuddy.active-project.v1')
      ).toBe(project.id)
    )
  })

  it.each([
    ['opencode', 'OpenCode'],
    ['continue', 'Continue CLI']
  ] as const)(
    'lets %s select Ask or Execute',
    async (runtimeId, label) => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: runtimeId,
      label,
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    expect(mode).toHaveValue('ask')
    expect(mode).toBeEnabled()
    expect(mode.closest('.composer')).not.toBeNull()
    expect(
      await screen.findByText(
        new RegExp(`${label} Ask 模式.*只允许搜索当前启用的知识库`)
      )
    ).toBeInTheDocument()
    fireEvent.change(mode, { target: { value: 'execute' } })

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '执行任务' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          prompt: '执行任务',
          workMode: 'execute'
        })
      )
    )
    }
  )

  it('restores the direct-model mode after leaving an Agent Runtime', async () => {
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      provider: 'opencode',
      opencodeEmbedded: true,
      opencodeModelSource: { kind: 'platform' }
    })
    vi.mocked(api.agent.getStatus)
      .mockResolvedValueOnce({
        id: 'opencode',
        label: 'OpenCode',
        available: true,
        supportsToolExecution: true,
        detail: 'Ready'
      })
      .mockResolvedValueOnce({
        id: 'model',
        label: 'sonnet-5',
        available: true,
        supportsToolExecution: false,
        detail: 'Ready'
      })
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    expect(mode).toHaveValue('ask')
    expect(mode).toBeEnabled()
    fireEvent.change(mode, { target: { value: 'execute' } })
    expect(mode).toHaveValue('execute')

    fireEvent.click(await screen.findByRole('button', { name: /OpenCode/u }))
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^默认模型.*sonnet-5$/u
      })
    )

    await waitFor(() => {
      expect(mode).toHaveValue('ask')
      expect(mode).toBeEnabled()
    })
  })

  it('disables Execute for a runtime without tool support', async () => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'model',
      label: 'legacy-model',
      available: true,
      supportsToolExecution: false,
      detail: 'Ready'
    })
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    expect(
      within(mode).getByRole('option', {
        name: 'Execute · 受控执行'
      })
    ).toBeDisabled()
    expect(mode).toHaveValue('ask')
  })

  it('allows a direct model to submit Execute with GoodBuddy approvals', async () => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'model',
      label: 'sonnet-5',
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })
    render(<App />)

    const mode = await screen.findByLabelText('工作模式')
    fireEvent.change(mode, { target: { value: 'execute' } })
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '读取项目文件' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          prompt: '读取项目文件',
          workMode: 'execute'
        })
      )
    )
    expect(mode).toBeEnabled()
  })

  it('terminalizes tools and activity when a request is cancelled', async () => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'opencode',
      label: 'OpenCode',
      available: true,
      supportsToolExecution: true,
      detail: 'Ready'
    })
    render(<App />)
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '执行长任务' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }

    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'tool',
        callId: 'call-1',
        name: 'bash',
        state: 'running',
        summary: 'OpenCode 工具：bash'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'error',
        status: 'cancelled',
        message: '请求已取消'
      })
    })

    expect(await screen.findByText('已取消')).toBeInTheDocument()
    const cancelledStatus = screen
      .getAllByText('请求已取消')
      .find((element) => element.classList.contains('message__status'))
    expect(cancelledStatus).toBeDefined()
    const cancelledDot = cancelledStatus?.querySelector(
      '.message__status-dot'
    )
    expect(cancelledDot).toHaveClass('message__status-dot')
    expect(cancelledDot).not.toHaveClass('message__status-dot--active')
    fireEvent.click(screen.getByText('任务与活动'))
    expect((await screen.findAllByText('已取消')).length).toBeGreaterThan(0)
    fireEvent.click(screen.getByRole('button', { name: '进行中' }))
    expect(
      screen.getByText('当前没有等待中或正在运行的活动。')
    ).toBeInTheDocument()
  })

  it('keeps persisted completed message statuses static', async () => {
    vi.mocked(api.conversations.list).mockResolvedValueOnce([
      {
        id: '00000000-0000-4000-8000-000000000210',
        projectId,
        title: '已完成会话',
        updatedAt: 1_775_000_000_000,
        messages: [
          {
            id: '00000000-0000-4000-8000-000000000211',
            role: 'assistant',
            content: '任务结果',
            createdAt: 1_775_000_000_000,
            state: 'complete',
            status: '任务已完成'
          }
        ]
      }
    ])

    render(<App />)

    const completedStatus = await screen.findByText('任务已完成')
    expect(
      completedStatus.querySelector('.message__status-dot')
    ).toHaveClass('message__status-dot')
    expect(
      completedStatus.querySelector('.message__status-dot')
    ).not.toHaveClass('message__status-dot--active')
  })

  it('switches runtime profiles from the composer dropdown', async () => {
    render(<App />)

    const runtimeButton = await screen.findByRole('button', {
      name: /sonnet-5/u
    })
    fireEvent.click(runtimeButton)
    expect(
      await screen.findByRole('menu', { name: 'Runtime 和模型' })
    ).toBeInTheDocument()
    expect(
      screen.queryByText('自动选择')
    ).not.toBeInTheDocument()
    expect(
      screen.getByRole('menuitemradio', {
        name: /^默认模型.*sonnet-5$/u
      })
    ).toBeInTheDocument()
    expect(
      screen.getByRole('menuitemradio', {
        name: /^OpenCode · 默认模型.*sonnet-5$/u
      })
    ).toBeInTheDocument()
    expect(
      screen.getByRole('menuitemradio', {
        name: /^Continue · 默认模型.*sonnet-5$/u
      })
    ).toBeInTheDocument()
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^默认模型.*sonnet-5$/u
      })
    )

    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenLastCalledWith({
        provider: 'model',
        profileId: modelProfileId
      })
    )
    expect(api.settings.updateRuntime).not.toHaveBeenCalled()
    expect(
      screen.queryByRole('heading', { name: '设置中心' })
    ).not.toBeInTheDocument()
  })

  it('shows Runtime switches globally without replacing composer guidance', async () => {
    render(<App />)
    const runtimeButton = await screen.findByRole('button', {
      name: /sonnet-5/u
    })
    vi.useFakeTimers()
    try {
      fireEvent.click(runtimeButton)
      fireEvent.click(
        screen.getByRole('menuitemradio', {
          name: /^OpenCode · 默认模型.*sonnet-5$/u
        })
      )
      await act(async () => {
        await Promise.resolve()
      })

      const notification = screen.getByRole('status')
      expect(notification).toHaveTextContent(
        '当前对话已切换到 OpenCode · 默认模型'
      )
      expect(screen.getByText(/Ask 模式：只读问答/)).toBeInTheDocument()

      fireEvent.click(
        screen.getByRole('button', {
          name: /OpenCode · 默认模型/u
        })
      )
      fireEvent.click(
        screen.getByRole('menuitemradio', {
          name: /^Continue · 默认模型.*sonnet-5$/u
        })
      )
      await act(async () => {
        await Promise.resolve()
      })
      expect(screen.getAllByRole('status')).toHaveLength(1)
      expect(screen.getByRole('status')).toHaveTextContent(
        '当前对话已切换到 Continue · 默认模型'
      )

      act(() => vi.advanceTimersByTime(4_500))
      expect(
        screen.queryByText(
          '当前对话已切换到 Continue · 默认模型'
        )
      ).not.toBeInTheDocument()
      expect(screen.getByText(/Ask 模式：只读问答/)).toBeInTheDocument()
    } finally {
      vi.useRealTimers()
    }
  })

  it('shows one configured choice per Agent Runtime in a flat keyboard menu', async () => {
    const settings = await api.settings.getRuntime()
    const secondProfileId =
      '00000000-0000-4000-8000-000000000002'
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      provider: 'model',
      modelProfiles: [
        ...settings.modelProfiles,
        {
          id: secondProfileId,
          name: '第二模型',
          baseUrl: 'http://127.0.0.1:11434/v1',
          modelName: 'qwen3',
          protocol: 'openai-chat-completions',
          authentication: 'none',
          imageGenerationQuality: 'auto',
          apiKeyConfigured: false,
          credentialSource: 'none'
        }
      ]
    })
    render(<App />)

    const runtimeButton = await screen.findByRole('button', {
      name: /sonnet-5/u
    })
    fireEvent.click(runtimeButton)
    const runtimeMenu = screen.getByRole('menu', {
      name: 'Runtime 和模型'
    })
    const directModel = screen.getByRole('menuitemradio', {
      name: /^默认模型.*sonnet-5$/u
    })
    const secondDirectModel = screen.getByRole('menuitemradio', {
      name: /^第二模型.*qwen3$/u
    })
    const openCodeModel = screen.getByRole('menuitemradio', {
      name: /^OpenCode · 默认模型.*sonnet-5$/u
    })
    const continueModel = screen.getByRole('menuitemradio', {
      name: /^Continue · 默认模型.*sonnet-5$/u
    })
    expect(directModel).toBeEnabled()
    expect(secondDirectModel).toBeEnabled()
    expect(openCodeModel).toBeEnabled()
    expect(continueModel).toBeEnabled()
    expect(screen.getAllByRole('menuitemradio')).toHaveLength(4)
    expect(within(runtimeMenu).getAllByRole('separator')).toHaveLength(3)
    expect(within(runtimeMenu).queryByRole('menu')).not.toBeInTheDocument()
    expect(
      screen.queryByRole('menuitemradio', {
        name: /^OpenCode · 第二模型/u
      })
    ).not.toBeInTheDocument()
    expect(
      screen.queryByRole('menuitemradio', {
        name: /^Continue · 第二模型/u
      })
    ).not.toBeInTheDocument()
    expect(
      screen.queryByRole('menuitem', { name: /Agent Runtime/u })
    ).not.toBeInTheDocument()

    await waitFor(() => expect(directModel).toHaveFocus())
    expect(directModel).toHaveAttribute('tabindex', '0')
    expect(secondDirectModel).toHaveAttribute('tabindex', '-1')
    fireEvent.keyDown(directModel, { key: 'ArrowDown' })
    expect(secondDirectModel).toHaveFocus()
    expect(directModel).toHaveAttribute('tabindex', '-1')
    expect(secondDirectModel).toHaveAttribute('tabindex', '0')
    fireEvent.keyDown(secondDirectModel, { key: 'ArrowDown' })
    expect(openCodeModel).toHaveFocus()
    fireEvent.keyDown(document.activeElement!, { key: 'Escape' })
    expect(runtimeButton).toHaveFocus()
    expect(
      screen.queryByRole('menu', { name: 'Runtime 和模型' })
    ).not.toBeInTheDocument()

    fireEvent.click(runtimeButton)
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^OpenCode · 默认模型.*sonnet-5$/u
      })
    )
    expect(runtimeButton).toHaveFocus()
    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenLastCalledWith({
        provider: 'opencode',
        profileId: modelProfileId
      })
    )

    const selectedRuntimeButton = await screen.findByRole('button', {
      name: /OpenCode · 默认模型/u
    })
    fireEvent.click(selectedRuntimeButton)
    const selectedOpenCodeModel = screen.getByRole('menuitemradio', {
      name: /^OpenCode · 默认模型.*sonnet-5$/u
    })
    await waitFor(() => expect(selectedOpenCodeModel).toHaveFocus())
    expect(selectedOpenCodeModel).toHaveAttribute('aria-checked', 'true')
    expect(selectedOpenCodeModel).toHaveAttribute('tabindex', '0')
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^Continue · 默认模型.*sonnet-5$/u
      })
    )
    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenLastCalledWith({
        provider: 'continue',
        profileId: modelProfileId
      })
    )
  })

  it('dismisses the Runtime menu on outside pointer and focus changes', async () => {
    render(<App />)

    const runtimeButton = await screen.findByRole('button', {
      name: /sonnet-5/u
    })
    const composer = screen.getByLabelText('向 GoodBuddy 提问')

    fireEvent.click(runtimeButton)
    expect(
      screen.getByRole('menu', { name: 'Runtime 和模型' })
    ).toBeInTheDocument()
    fireEvent.pointerDown(composer)
    expect(
      screen.queryByRole('menu', { name: 'Runtime 和模型' })
    ).not.toBeInTheDocument()

    fireEvent.click(runtimeButton)
    const selectedModel = screen.getByRole('menuitemradio', {
      name: /^默认模型.*sonnet-5$/u
    })
    await waitFor(() => expect(selectedModel).toHaveFocus())
    fireEvent.keyDown(selectedModel, { key: 'Tab' })
    composer.focus()
    expect(composer).toHaveFocus()
    await waitFor(() =>
      expect(
        screen.queryByRole('menu', { name: 'Runtime 和模型' })
      ).not.toBeInTheDocument()
    )
  })

  it('labels explicitly configured Runtime-owned model sources', async () => {
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      provider: 'model',
      opencodeModelSource: { kind: 'platform' },
      continueModelSource: { kind: 'platform' }
    })
    render(<App />)

    fireEvent.click(
      await screen.findByRole('button', { name: /sonnet-5/u })
    )
    expect(
      screen.getByRole('menuitemradio', {
        name: /^OpenCode · 自身配置.*使用 OpenCode 自身配置$/u
      })
    ).toBeInTheDocument()
    const continueChoice = screen.getByRole('menuitemradio', {
      name: /^Continue · 自身配置.*使用 Continue 自身配置$/u
    })
    fireEvent.click(continueChoice)
    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenLastCalledWith({
        provider: 'continue'
      })
    )
  })

  it('normalizes a legacy Auto conversation to the explicit default Runtime', async () => {
    vi.mocked(api.conversations.list).mockResolvedValueOnce([
      {
        id: '00000000-0000-4000-8000-000000000020',
        runtimeSelection: { provider: 'auto' },
        title: '旧自动对话',
        updatedAt: 1,
        messages: [
          {
            id: '00000000-0000-4000-8000-000000000021',
            role: 'assistant',
            content: '旧消息',
            createdAt: 1,
            state: 'complete'
          }
        ]
      }
    ])
    render(<App />)

    expect(
      await screen.findByRole('button', { name: /默认模型.*sonnet-5/u })
    ).toBeInTheDocument()
    await waitFor(() =>
      expect(api.conversations.replace).toHaveBeenLastCalledWith(
        expect.arrayContaining([
          expect.objectContaining({
            id: '00000000-0000-4000-8000-000000000020',
            runtimeSelection: {
              provider: 'model',
              profileId: modelProfileId
            }
          })
        ])
      )
    )
  })

  it('rebinds a loaded conversation when its model profile was removed', async () => {
    const removedProfileId =
      '00000000-0000-4000-8000-000000000099'
    vi.mocked(api.conversations.list).mockResolvedValueOnce([
      {
        id: '00000000-0000-4000-8000-000000000022',
        runtimeSelection: {
          provider: 'model',
          profileId: removedProfileId
        },
        title: '旧模型对话',
        updatedAt: 1,
        messages: [
          {
            id: '00000000-0000-4000-8000-000000000023',
            role: 'assistant',
            content: '旧消息',
            createdAt: 1,
            state: 'complete'
          }
        ]
      }
    ])
    render(<App />)

    expect(
      await screen.findByRole('button', { name: /默认模型.*sonnet-5/u })
    ).toBeInTheDocument()
    await waitFor(() =>
      expect(api.conversations.replace).toHaveBeenLastCalledWith(
        expect.arrayContaining([
          expect.objectContaining({
            id: '00000000-0000-4000-8000-000000000022',
            runtimeSelection: {
              provider: 'model',
              profileId: modelProfileId
            }
          })
        ])
      )
    )
  })

  it('keeps model Runtime selection scoped to its conversation', async () => {
    const secondProfileId =
      '00000000-0000-4000-8000-000000000002'
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      modelProfiles: [
        ...settings.modelProfiles,
        {
          id: secondProfileId,
          name: '第二模型',
          baseUrl: 'http://127.0.0.1:11434/v1',
          modelName: 'qwen3',
          protocol: 'openai-chat-completions',
          authentication: 'none',
          imageGenerationQuality: 'auto',
          apiKeyConfigured: false,
          credentialSource: 'none'
        }
      ]
    })
    vi.mocked(api.agent.getStatus).mockImplementation(
      async (selection) => ({
        id: 'model',
        label:
          selection?.provider === 'model' &&
          selection.profileId === secondProfileId
            ? 'qwen3'
            : 'sonnet-5',
        available: true,
        supportsToolExecution: true,
        detail: 'Ready'
      })
    )
    render(<App />)

    fireEvent.click(
      await screen.findByRole('button', { name: /sonnet-5/u })
    )
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^第二模型.*qwen3$/u
      })
    )
    expect(
      await screen.findByRole('button', { name: /第二模型/u })
    ).toBeInTheDocument()

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '第二模型对话' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    expect(request?.runtimeSelection).toEqual({
      provider: 'model',
      profileId: secondProfileId
    })
    if (!request) {
      throw new Error('Missing request')
    }
    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'done'
      })
    })

    fireEvent.click(
      screen.getByRole('button', { name: /新建对话/u })
    )
    expect(
      await screen.findByRole('button', { name: /默认模型/u })
    ).toBeInTheDocument()
    const previousConversation = screen
      .getAllByText('第二模型对话')
      .map((element) => element.closest('button'))
      .find((button) => button?.classList.contains('conversation-item'))
    if (!previousConversation) {
      throw new Error('Missing previous conversation')
    }
    fireEvent.click(previousConversation)
    await waitFor(() =>
      expect(
        screen
          .getAllByRole('button', { name: /第二模型/u })
          .find((button) => button.classList.contains('model-button'))
      ).toBeInTheDocument()
    )

    await waitFor(
      () =>
        expect(api.conversations.replace).toHaveBeenLastCalledWith(
          expect.arrayContaining([
            expect.objectContaining({
              title: '第二模型对话',
              runtimeSelection: {
                provider: 'model',
                profileId: secondProfileId
              }
            })
          ])
        ),
      { timeout: 2_000 }
    )
  })

  it('ignores a stale picker status after rapidly changing conversations', async () => {
    const secondProfileId =
      '00000000-0000-4000-8000-000000000002'
    const thirdProfileId =
      '00000000-0000-4000-8000-000000000003'
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      modelProfiles: [
        ...settings.modelProfiles,
        {
          id: secondProfileId,
          name: '第二模型',
          baseUrl: 'http://127.0.0.1:11434/v1',
          modelName: 'qwen3',
          protocol: 'openai-chat-completions',
          authentication: 'none',
          imageGenerationQuality: 'auto',
          apiKeyConfigured: false,
          credentialSource: 'none'
        },
        {
          id: thirdProfileId,
          name: '第三模型',
          baseUrl: 'http://127.0.0.1:11435/v1',
          modelName: 'llama3',
          protocol: 'openai-chat-completions',
          authentication: 'none',
          imageGenerationQuality: 'auto',
          apiKeyConfigured: false,
          credentialSource: 'none'
        }
      ]
    })
    let resolveThird!: (status: {
      id: 'model'
      label: string
      available: boolean
      supportsToolExecution: boolean
      detail: string
    }) => void
    const thirdStatus = new Promise<{
      id: 'model'
      label: string
      available: boolean
      supportsToolExecution: boolean
      detail: string
    }>((resolve) => {
      resolveThird = resolve
    })
    vi.mocked(api.agent.getStatus).mockImplementation(async (selection) => {
      if (
        selection?.provider === 'model' &&
        selection.profileId === thirdProfileId
      ) {
        return thirdStatus
      }
      return {
        id: 'model',
        label:
          selection?.provider === 'model' &&
          selection.profileId === secondProfileId
            ? 'qwen3'
            : 'sonnet-5',
        available: true,
        supportsToolExecution: true,
        detail: 'Ready'
      }
    })
    render(<App />)

    fireEvent.click(
      await screen.findByRole('button', { name: /sonnet-5/u })
    )
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^第二模型.*qwen3$/u
      })
    )
    await screen.findByRole('button', { name: /第二模型/u })
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '保留第二模型会话' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }
    act(() => {
      agentListener?.({ requestId: request.requestId, type: 'done' })
    })

    const secondModelButton = screen
      .getAllByRole('button', { name: /第二模型/u })
      .find((button) => button.classList.contains('model-button'))
    if (!secondModelButton) {
      throw new Error('Missing second model picker')
    }
    fireEvent.click(secondModelButton)
    fireEvent.click(
      screen.getByRole('menuitemradio', {
        name: /^第三模型.*llama3$/u
      })
    )
    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenCalledWith({
        provider: 'model',
        profileId: thirdProfileId
      })
    )
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '状态未完成时不能发送' }
    })
    expect(screen.getByLabelText('发送')).toBeDisabled()
    fireEvent.click(screen.getByRole('button', { name: /新建对话/u }))
    await screen.findByRole('button', { name: /默认模型/u })

    await act(async () => {
      resolveThird({
        id: 'model',
        label: 'llama3',
        available: true,
        supportsToolExecution: true,
        detail: 'Ready'
      })
      await thirdStatus
    })
    expect(
      screen.getByRole('button', { name: /默认模型/u })
    ).toBeInTheDocument()
    expect(
      screen.queryByRole('button', { name: /第三模型/u })
    ).not.toBeInTheDocument()
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '新对话仍可发送' }
    })
    await waitFor(() => expect(screen.getByLabelText('发送')).toBeEnabled())
  })

  it('opens project creation as an unobscured dialog', async () => {
    render(<App />)

    const newProjectButton = await screen.findByLabelText('新建项目')
    fireEvent.click(newProjectButton)
    let dialog = screen.getByRole('dialog', { name: '新建项目' })
    expect(dialog).toHaveClass('project-create-card')
    expect(within(dialog).getByRole('button', { name: '创建' }))
      .toBeDisabled()
    expect(within(dialog).getByLabelText('名称')).toHaveFocus()

    fireEvent.keyDown(document, { key: 'Escape' })
    expect(
      screen.queryByRole('dialog', { name: '新建项目' })
    ).not.toBeInTheDocument()
    expect(newProjectButton).toHaveFocus()

    fireEvent.click(newProjectButton)
    dialog = screen.getByRole('dialog', { name: '新建项目' })

    fireEvent.change(within(dialog).getByLabelText('名称'), {
      target: { value: '新项目' }
    })
    fireEvent.click(within(dialog).getByRole('button', { name: '创建' }))

    await waitFor(() =>
      expect(api.projects.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: '新项目',
          rootPath: ''
        })
      )
    )
  })

  it('keeps project input when creation fails', async () => {
    vi.mocked(api.projects.create).mockRejectedValueOnce(
      new Error('项目目录不可用')
    )
    render(<App />)

    fireEvent.click(await screen.findByLabelText('新建项目'))
    const dialog = screen.getByRole('dialog', { name: '新建项目' })
    const nameInput = within(dialog).getByLabelText('名称')
    fireEvent.change(nameInput, {
      target: { value: '保留的项目名称' }
    })
    fireEvent.click(
      within(dialog).getByRole('button', { name: '创建' })
    )

    expect(await within(dialog).findByRole('alert')).toHaveTextContent(
      '项目目录不可用'
    )
    expect(nameInput).toHaveValue('保留的项目名称')
    expect(
      screen.getByRole('dialog', { name: '新建项目' })
    ).toBeInTheDocument()
  })

  it('edits and safely deletes the current project from project settings', async () => {
    const secondProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000102',
      name: '第二项目',
      rootPath: 'C:\\Second'
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      secondProject
    ])
    render(<App />)

    fireEvent.click(await screen.findByLabelText('项目设置'))
    let dialog = screen.getByRole('dialog', { name: '项目设置' })
    expect(within(dialog).getByLabelText('名称')).toHaveValue(
      project.name
    )
    expect(within(dialog).getByLabelText('根目录')).toHaveValue(
      project.rootPath
    )
    fireEvent.change(within(dialog).getByLabelText('说明'), {
      target: { value: '更新后的说明' }
    })
    fireEvent.click(
      within(dialog).getByRole('button', { name: '保存项目' })
    )
    await waitFor(() =>
      expect(api.projects.update).toHaveBeenCalledWith(
        project.id,
        expect.objectContaining({
          description: '更新后的说明',
          rootPath: project.rootPath
        })
      )
    )

    fireEvent.click(screen.getByLabelText('项目设置'))
    dialog = screen.getByRole('dialog', { name: '项目设置' })
    expect(dialog).toHaveTextContent('不会删除磁盘上的项目目录或文件')
    fireEvent.click(
      within(dialog).getByRole('button', { name: '删除项目' })
    )
    const confirmation = within(dialog).getByLabelText(
      `输入“${project.name}”确认删除`
    )
    const deleteButton = within(dialog).getByRole('button', {
      name: '永久删除项目'
    })
    expect(deleteButton).toBeDisabled()
    fireEvent.change(confirmation, {
      target: { value: project.name }
    })
    expect(deleteButton).toBeEnabled()
    fireEvent.click(deleteButton)

    await waitFor(() =>
      expect(api.projects.delete).toHaveBeenCalledWith(
        project.id,
        project.name
      )
    )
    expect(screen.getByLabelText('当前项目')).toHaveValue(
      secondProject.id
    )
  })

  it('uses a message icon for conversation navigation', async () => {
    render(<App />)

    const conversationNavigation = await screen.findByRole('button', {
      name: '对话'
    })
    expect(
      conversationNavigation.querySelector('.lucide-message-square')
    ).not.toBeNull()
    expect(
      conversationNavigation.querySelector('.lucide-history')
    ).toBeNull()
  })

  it('marks an image model and renders its generated artifact', async () => {
    const anchorClick = vi
      .spyOn(HTMLAnchorElement.prototype, 'click')
      .mockImplementation(() => {})
    vi.mocked(api.agent.getStatus).mockResolvedValueOnce({
      id: 'model',
      label: 'gpt-image-2',
      available: true,
      supportsToolExecution: false,
      detail: 'OpenAI Images Generations',
      capability: 'image-generation'
    })
    render(<App />)

    expect((await screen.findAllByText('生图')).length).toBeGreaterThan(0)
    expect(screen.getByLabelText('向 GoodBuddy 提问')).toHaveAttribute(
      'placeholder',
      '描述你想生成的图片…'
    )
    await waitFor(() =>
      expect(api.artifacts.list).toHaveBeenCalled()
    )

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '生成一只蓝色的猫' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }
    const artifactId = '00000000-0000-4000-8000-000000000301'
    vi.mocked(api.artifacts.get).mockResolvedValueOnce(
      {
        id: artifactId,
        projectId,
        taskId: request.requestId,
        kind: 'image',
        title: '生成一只蓝色的猫',
        mimeType: 'image/png',
        content:
          'data:image/png;base64,iVBORw0KGgoAAAAAAAAAAAAA',
        byteSize: 42,
        createdAt: '2026-08-01T00:00:00.000Z',
        updatedAt: '2026-08-01T00:00:00.000Z'
      }
    )

    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'artifact',
        artifactId,
        kind: 'image',
        title: '生成一只蓝色的猫'
      })
    })

    expect(
      await screen.findByRole('img', { name: '生成一只蓝色的猫' })
    ).toHaveAttribute('src', expect.stringMatching(/^data:image\/png/u))
    fireEvent.click(
      screen.getByRole('button', {
        name: '下载图片 生成一只蓝色的猫'
      })
    )
    expect(anchorClick).toHaveBeenCalledOnce()

    fireEvent.click(
      screen.getByRole('button', {
        name: '查看图片 生成一只蓝色的猫'
      })
    )
    const imageDialog = await screen.findByRole('dialog', {
      name: '生成一只蓝色的猫'
    })
    expect(
      within(imageDialog).getByRole('img', {
        name: '生成一只蓝色的猫'
      })
    ).toHaveAttribute('src', expect.stringMatching(/^data:image\/png/u))
    fireEvent.click(
      within(imageDialog).getByRole('button', { name: '下载图片' })
    )
    expect(anchorClick).toHaveBeenCalledTimes(2)
    fireEvent.keyDown(imageDialog, { key: 'Escape' })
    expect(
      screen.queryByRole('dialog', { name: '生成一只蓝色的猫' })
    ).not.toBeInTheDocument()
    anchorClick.mockRestore()
  })

  it('can dispatch a request to the parallel expert team', async () => {
    render(<App />)

    fireEvent.change(screen.getByLabelText('专家角色'), {
      target: { value: 'team' }
    })
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '制定发布计划' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          teamMode: true,
          expertId: undefined,
          prompt: '制定发布计划'
        })
      )
    )
  })

  it('requests smart routing only when enabled without an explicit expert', async () => {
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      subagentSmartRoutingEnabled: true
    })
    render(<App />)

    fireEvent.change(await screen.findByLabelText('向 GoodBuddy 提问'), {
      target: { value: '分析发布风险' }
    })
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          smartRouting: true,
          expertId: undefined,
          teamMode: false,
          workMode: 'ask'
        })
      )
    )
  })

  it('gives an explicitly selected expert priority over smart routing', async () => {
    const expertId = '00000000-0000-4000-8000-000000000501'
    const settings = await api.settings.getRuntime()
    vi.mocked(api.settings.getRuntime).mockResolvedValueOnce({
      ...settings,
      subagentSmartRoutingEnabled: true
    })
    vi.mocked(api.experts.list).mockResolvedValueOnce([
      {
        id: expertId,
        name: '发布专家',
        description: '检查发布风险',
        systemInstructions: 'Review release risks.',
        routingKeywords: ['发布', '风险'],
        enabled: true,
        createdAt: '2026-08-01T00:00:00.000Z',
        updatedAt: '2026-08-01T00:00:00.000Z'
      }
    ])
    render(<App />)

    await screen.findByRole('option', { name: '发布专家' })
    fireEvent.change(screen.getByLabelText('专家角色'), {
      target: { value: expertId }
    })
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '检查发布方案' }
    })
    fireEvent.click(screen.getByLabelText('发送'))

    await waitFor(() =>
      expect(run).toHaveBeenCalledWith(
        expect.objectContaining({
          expertId,
          smartRouting: undefined,
          teamMode: false
        })
      )
    )
  })

  it('shows bounded Subagent states and records child expert activity', async () => {
    render(<App />)
    fireEvent.change(await screen.findByLabelText('向 GoodBuddy 提问'), {
      target: { value: '分析复杂问题' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }

    const events = [
      {
        childTaskId: '00000000-0000-4000-8000-000000000601',
        expertId: '00000000-0000-4000-8000-000000000701',
        expertName: '研究专家',
        routingMode: 'smart' as const,
        state: 'queued' as const
      },
      {
        childTaskId: '00000000-0000-4000-8000-000000000602',
        expertId: '00000000-0000-4000-8000-000000000702',
        expertName: '代码专家',
        routingMode: 'manual' as const,
        state: 'running' as const
      },
      {
        childTaskId: '00000000-0000-4000-8000-000000000603',
        expertId: '00000000-0000-4000-8000-000000000703',
        expertName: '安全专家',
        routingMode: 'smart' as const,
        state: 'failed' as const,
        error: '无法读取必要上下文'
      },
      {
        childTaskId: '00000000-0000-4000-8000-000000000604',
        expertId: '00000000-0000-4000-8000-000000000704',
        expertName: '第四位专家',
        routingMode: 'smart' as const,
        state: 'completed' as const
      }
    ]
    act(() => {
      for (const event of events) {
        agentListener?.({
          requestId: request.requestId,
          type: 'subagent',
          ...event
        })
      }
    })

    const statusRegion = await screen.findByLabelText('子专家状态')
    expect(within(statusRegion).getByText('研究专家')).toBeInTheDocument()
    expect(within(statusRegion).getByText('等待中')).toBeInTheDocument()
    expect(within(statusRegion).getByText('代码专家')).toBeInTheDocument()
    expect(within(statusRegion).getByText('进行中')).toBeInTheDocument()
    expect(within(statusRegion).getByText('安全专家')).toBeInTheDocument()
    expect(within(statusRegion).getByText('失败')).toBeInTheDocument()
    expect(
      within(statusRegion).getByText('无法读取必要上下文')
    ).toBeInTheDocument()
    expect(
      within(statusRegion).queryByText('第四位专家')
    ).not.toBeInTheDocument()

    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'subagent',
        ...events[0]!,
        state: 'completed'
      })
      agentListener?.({
        requestId: request.requestId,
        type: 'subagent',
        ...events[1]!,
        state: 'cancelled',
        reason: '父任务已停止'
      })
    })
    expect(within(statusRegion).getByText('已完成')).toBeInTheDocument()
    expect(within(statusRegion).getByText('已取消')).toBeInTheDocument()
    expect(within(statusRegion).getByText('父任务已停止')).toBeInTheDocument()

    fireEvent.click(screen.getByText('任务与活动'))
    expect(await screen.findAllByText('子专家')).toHaveLength(4)
    expect(screen.getAllByText(/智能路由/u).length).toBeGreaterThan(0)
    expect(screen.getAllByText(/手动指定/u).length).toBeGreaterThan(0)
  })

  it('offers once, session, permanent, and deny for a tool call', async () => {
    render(<App />)

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '运行工具' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }

    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'approval',
        approvalId: crypto.randomUUID(),
        title: 'Continue 请求调用 Bash',
        description: '确认工具调用',
        toolName: 'Bash',
        argumentSummary: 'echo safe',
        allowPermanent: true
      })
    })

    expect(await screen.findByText('仅此次')).toBeInTheDocument()
    expect(screen.getByText('此会话')).toBeInTheDocument()
    expect(screen.getByText('永久允许')).toBeInTheDocument()
    expect(screen.getAllByText('拒绝')).toHaveLength(2)
    fireEvent.click(screen.getByText('此会话'))
    await waitFor(() =>
      expect(api.agent.respondApproval).toHaveBeenCalledWith(
        expect.any(String),
        'session'
      )
    )
  })

  it('renders and answers an OpenCode question request', async () => {
    render(<App />)
    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '需要确认的任务' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const request = run.mock.calls[0]?.[0]
    if (!request) {
      throw new Error('Missing request')
    }

    act(() => {
      agentListener?.({
        requestId: request.requestId,
        type: 'question',
        questionId: 'question-1',
        questions: [
          {
            header: '实现方式',
            question: '请选择实现方式',
            options: [
              {
                label: '直接修改',
                description: '立即更新现有实现'
              },
              {
                label: '先写测试',
                description: '先增加回归测试'
              }
            ],
            multiple: false,
            custom: true
          }
        ]
      })
    })

    expect(
      await screen.findByText('OpenCode 需要补充信息')
    ).toBeInTheDocument()
    fireEvent.click(screen.getByLabelText(/先写测试/u))
    fireEvent.click(screen.getByRole('button', { name: '提交回答' }))
    await waitFor(() =>
      expect(api.agent.respondQuestion).toHaveBeenCalledWith(
        'question-1',
        [['先写测试']]
      )
    )
    expect(
      screen.queryByText('OpenCode 需要补充信息')
    ).not.toBeInTheDocument()
  })

  it('configures a runtime without reading an existing API key', async () => {
    render(<App />)

    fireEvent.click(await screen.findByText('本地工作区'))
    expect(
      await screen.findByRole('heading', {
        name: '设置中心'
      })
    ).toBeInTheDocument()
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
    expect(screen.getByRole('region', { name: '设置中心' }))
      .toBeInTheDocument()
    expect(
      screen.getByRole('tab', { name: 'Agent Runtime' })
    ).toBeInTheDocument()
    expect(
      screen.getByRole('tab', { name: '安全与数据' })
    ).toBeInTheDocument()
    fireEvent.click(screen.getByRole('tab', { name: '模型连接' }))

    const apiKeyInput = screen.getByLabelText('API Key')
    expect(apiKeyInput).toHaveValue('')
    fireEvent.change(apiKeyInput, {
      target: { value: 'test-api-key' }
    })
    fireEvent.click(screen.getByRole('button', { name: '保存设置' }))

    await waitFor(() =>
      expect(api.settings.updateRuntime).toHaveBeenCalledWith(
        expect.objectContaining({
          apiKey: {
            action: 'replace',
            value: 'test-api-key'
          }
        })
      )
    )
    await waitFor(() => expect(apiKeyInput).toHaveValue(''))
  })

  it('opens the global assistant sidebar and switches work tabs', async () => {
    render(<App />)

    const sidebar = screen.getByLabelText('助手工作栏')
    expect(sidebar).not.toHaveClass('assistant-sidebar--open')
    fireEvent.click(screen.getByLabelText('切换助手工作栏'))
    expect(sidebar).toHaveClass('assistant-sidebar--open')

    expect(
      screen.getByRole('tab', { name: '任务中心' })
    ).toHaveAttribute('aria-selected', 'true')
    expect(screen.getByText('自动化')).toBeInTheDocument()
    expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
    fireEvent.click(screen.getByRole('tab', { name: '上下文' }))
    expect(
      screen.getByText('尚未添加文件、截图或剪贴板内容。')
    ).toBeInTheDocument()
    fireEvent.click(screen.getByRole('tab', { name: '工作区' }))
    expect(
      screen.getByText(/选择文件后在当前工作区内预览/)
    ).toBeInTheDocument()
    fireEvent.click(screen.getByRole('tab', { name: '浏览器' }))
    expect(
      screen.getByText(/Agent 打开网页后/)
    ).toBeInTheDocument()
    fireEvent.click(screen.getByRole('tab', { name: '成果' }))
    expect(screen.getByText('对话与导入成果')).toBeInTheDocument()
    expect(
      screen.getByText(/查看并预览由对话生成或手动导入/)
    ).toBeInTheDocument()
    expect(
      screen.queryByRole('tab', { name: '预览' })
    ).not.toBeInTheDocument()
    fireEvent.click(screen.getByLabelText('关闭助手工作栏'))
    expect(sidebar).not.toHaveClass('assistant-sidebar--open')
  })

  it('opens the live browser tab for the active conversation and can stop it', async () => {
    render(<App />)

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '打开示例网页' }
    })
    fireEvent.click(await screen.findByLabelText('发送'))
    await waitFor(() => expect(run).toHaveBeenCalledOnce())
    const conversationId = run.mock.calls[0]?.[0].conversationId
    expect(conversationId).toBeTruthy()

    act(() => {
      browserListener?.({
        conversationId: conversationId ?? '',
        status: 'ready',
        url: 'https://example.com/',
        frameDataUrl: 'data:image/jpeg;base64,/9j/2Q==',
        updatedAt: Date.now()
      })
    })

    expect(screen.getByLabelText('助手工作栏')).toHaveClass(
      'assistant-sidebar--open'
    )
    expect(
      screen.getByRole('tab', { name: '浏览器' })
    ).toHaveAttribute('aria-selected', 'true')
    expect(
      screen.getByAltText('Agent 实时浏览器画面')
    ).toHaveAttribute(
      'src',
      'data:image/jpeg;base64,/9j/2Q=='
    )
    fireEvent.click(
      screen.getByRole('button', { name: '交互' })
    )
    await waitFor(() =>
      expect(api.browser.interact).toHaveBeenCalledWith(conversationId)
    )
    fireEvent.click(
      screen.getByRole('button', { name: '停止浏览器' })
    )
    await waitFor(() =>
      expect(api.browser.stop).toHaveBeenCalledWith(conversationId)
    )
  })

  it('opens Smart Heartbeat as a first-class workspace', async () => {
    render(<App />)

    fireEvent.click(
      screen.getByRole('button', { name: '智能心跳' })
    )

    expect(
      await screen.findByRole('heading', { name: '智能心跳' })
    ).toBeInTheDocument()
    expect(
      await screen.findByRole('tab', { name: '成长概览' })
    ).toBeInTheDocument()
    expect(
      screen.getByRole('button', { name: '配置智能心跳' })
    ).toBeInTheDocument()
    expect(
      screen.queryByLabelText('切换助手工作栏')
    ).not.toBeInTheDocument()
  })

  it('shows retryable page-local knowledge errors without an empty-state flash', async () => {
    vi.mocked(api.knowledge.getSnapshot).mockRejectedValueOnce(
      new Error('知识数据库暂时不可用')
    )
    render(<App />)

    fireEvent.click(screen.getByRole('button', { name: '知识库' }))
    expect(
      await screen.findByText('知识库加载失败')
    ).toBeInTheDocument()
    expect(screen.getAllByText('知识数据库暂时不可用')).toHaveLength(1)
    expect(screen.queryByText('建立第一个知识库')).not.toBeInTheDocument()

    fireEvent.click(screen.getByRole('button', { name: '重试' }))
    expect(
      await screen.findByText('建立第一个知识库')
    ).toBeInTheDocument()
  })

  it('retries the knowledge library whose selection failed', async () => {
    const firstLibraryId = '11111111-1111-4111-8111-111111111111'
    const secondLibraryId = '22222222-2222-4222-8222-222222222222'
    const libraries = [
      {
        id: firstLibraryId,
        name: '产品知识',
        description: '',
        storageMode: 'managed' as const,
        graphEnabled: false,
        graphStrategy: 'rules' as const,
        sourceCount: 0,
        documentCount: 0,
        indexedDocumentCount: 0
      },
      {
        id: secondLibraryId,
        name: '工程知识',
        description: '',
        storageMode: 'managed' as const,
        graphEnabled: false,
        graphStrategy: 'rules' as const,
        sourceCount: 0,
        documentCount: 0,
        indexedDocumentCount: 0
      }
    ]
    const snapshot = {
      libraries,
      sources: [],
      documents: [],
      graphNodes: [],
      graphRelations: [],
      evidence: []
    }
    vi.mocked(api.knowledge.getSnapshot)
      .mockResolvedValueOnce({
        ...snapshot,
        selectedLibraryId: firstLibraryId
      })
      .mockRejectedValueOnce(new Error('工程知识暂时不可用'))
      .mockResolvedValueOnce({
        ...snapshot,
        selectedLibraryId: secondLibraryId
      })
    render(<App />)

    fireEvent.click(screen.getByRole('button', { name: '知识库' }))
    fireEvent.click(
      await screen.findByRole('button', {
        name: /^工程知识 0 个文档/u
      })
    )
    expect(
      await screen.findByText('知识库刷新失败')
    ).toBeInTheDocument()

    fireEvent.click(screen.getByRole('button', { name: '重试' }))
    await waitFor(() =>
      expect(api.knowledge.getSnapshot).toHaveBeenLastCalledWith(
        secondLibraryId
      )
    )
    expect(
      screen.getByRole('button', {
        name: /^工程知识 0 个文档/u
      })
    ).toHaveAttribute('aria-current', 'page')
  })

  it('shows retryable page-local heartbeat errors without first-time guidance', async () => {
    vi.mocked(api.heartbeats.list).mockRejectedValue(
      new Error('心跳数据库暂时不可用')
    )
    render(<App />)

    fireEvent.click(
      screen.getByRole('button', { name: '智能心跳' })
    )
    expect(
      await screen.findByText('智能心跳加载失败')
    ).toBeInTheDocument()
    expect(
      screen.queryByRole('button', { name: '配置智能心跳' })
    ).not.toBeInTheDocument()

    const retry = await screen.findByRole('button', { name: '重试' })
    vi.mocked(api.heartbeats.list).mockResolvedValue([])
    fireEvent.click(retry)
    expect(
      await screen.findByRole('button', { name: '配置智能心跳' })
    ).toBeInTheDocument()
  })

  it('does not expose the previous project heartbeat after a switch fails', async () => {
    const secondProject = {
      ...project,
      id: '00000000-0000-4000-8000-000000000102',
      name: '第二项目',
      rootPath: 'C:\\Second'
    }
    vi.mocked(api.projects.list).mockResolvedValueOnce([
      project,
      secondProject
    ])
    vi.mocked(api.heartbeats.list).mockResolvedValue([
      {
        id: '00000000-0000-4000-8000-000000000701',
        projectId,
        name: '旧项目心跳',
        timezone: 'Asia/Shanghai',
        recurrence: { type: 'daily', localTime: '09:00' },
        enabled: true,
        lookbackHours: 24,
        retentionDays: 30,
        nextRunAt: '2026-08-05T01:00:00.000Z',
        createdAt: '2026-08-01T00:00:00.000Z',
        updatedAt: '2026-08-01T00:00:00.000Z'
      }
    ])
    render(<App />)

    fireEvent.click(
      screen.getByRole('button', { name: '智能心跳' })
    )
    fireEvent.click(
      await screen.findByRole('tab', { name: '心跳计划' })
    )
    expect(await screen.findAllByText('旧项目心跳')).not.toHaveLength(0)
    vi.mocked(api.heartbeats.list).mockRejectedValue(
      new Error('第二项目心跳读取失败')
    )
    fireEvent.change(screen.getByLabelText('当前项目'), {
      target: { value: secondProject.id }
    })
    fireEvent.click(
      screen.getByRole('button', { name: '智能心跳' })
    )

    expect(
      await screen.findByText('智能心跳加载失败')
    ).toBeInTheDocument()
    expect(screen.queryAllByText('旧项目心跳')).toHaveLength(0)
    expect(
      screen.queryByRole('button', { name: '立即运行旧项目心跳' })
    ).not.toBeInTheDocument()
  })

  it('automatically snapshots the conversation project on new activity', async () => {
    render(<App />)
    await screen.findByText('项目：默认项目')

    fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
      target: { value: '记录项目范围' }
    })
    fireEvent.click(screen.getByLabelText('发送'))
    await waitFor(() =>
      expect(
        loadActivityRecords().find(
          (record) =>
            record.kind === 'request' &&
            record.title === '记录项目范围'
        )?.scope
      ).toEqual({
        kind: 'project',
        projectId,
        projectName: project.name
      })
    )
  })

  it('marks the current primary navigation page and hides decorative icons', async () => {
    render(<App />)

    const navigation = screen.getByRole('navigation', {
      name: '主导航'
    })
    const chat = within(navigation).getByRole('button', { name: '对话' })
    const knowledge = within(navigation).getByRole('button', {
      name: '知识库'
    })

    expect(chat).toHaveAttribute('aria-current', 'page')
    expect(knowledge).not.toHaveAttribute('aria-current')
    for (const button of within(navigation).getAllByRole('button')) {
      expect(button.querySelector('svg')).toHaveAttribute(
        'aria-hidden',
        'true'
      )
    }

    fireEvent.click(knowledge)
    expect(knowledge).toHaveAttribute('aria-current', 'page')
    expect(chat).not.toHaveAttribute('aria-current')
  })

  it('collapses the primary sidebar into an inert narrow-window overlay', async () => {
    const originalWidth = window.innerWidth
    const { container } = render(<App />)
    const sidebar = container.querySelector<HTMLElement>('.sidebar')
    const workspace = container.querySelector<HTMLElement>('.workspace')
    expect(sidebar).not.toBeNull()
    expect(workspace).not.toBeNull()
    if (!sidebar || !workspace) {
      return
    }

    try {
      Object.defineProperty(window, 'innerWidth', {
        configurable: true,
        value: 680
      })
      window.dispatchEvent(new Event('resize'))

      await waitFor(() =>
        expect(sidebar).toHaveClass('sidebar--closed')
      )
      expect(sidebar).toHaveAttribute('aria-hidden', 'true')
      expect(sidebar).toHaveAttribute('inert')

      const toggle = screen.getByRole('button', { name: '切换侧栏' })
      fireEvent.click(toggle)
      expect(sidebar).not.toHaveClass('sidebar--closed')
      expect(sidebar).toHaveAttribute('aria-hidden', 'false')
      expect(sidebar).toHaveAttribute('aria-modal', 'true')
      expect(sidebar).not.toHaveAttribute('inert')
      expect(workspace).toHaveAttribute('aria-hidden', 'true')
      expect(workspace).toHaveAttribute('inert')
      await waitFor(() =>
        expect(
          within(sidebar).getByRole('button', { name: '对话' })
        ).toHaveFocus()
      )

      fireEvent.click(
        screen.getByRole('button', { name: '关闭侧栏' })
      )
      await waitFor(() => expect(toggle).toHaveFocus())
      expect(sidebar).toHaveClass('sidebar--closed')
      expect(workspace).not.toHaveAttribute('aria-hidden')
      expect(workspace).not.toHaveAttribute('inert')
    } finally {
      Object.defineProperty(window, 'innerWidth', {
        configurable: true,
        value: originalWidth
      })
    }
  })

  it('opens Magic Notes as a global first-class workspace', async () => {
    api.updates = {
      getSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: false,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      updateSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: false,
        magicNotesEnabled: true,
        magicNoteCommentMode: 'immediate' as const
      })),
      check: vi.fn(),
      openReleasePage: vi.fn(async () => {}),
      onResult: vi.fn(() => () => {})
    }
    try {
      render(<App />)
      await screen.findByText('项目：默认项目')
      const magicNotesEntry = await screen.findByRole('button', {
        name: '魔法笔记'
      })

      fireEvent.click(magicNotesEntry)

      expect(
        await screen.findByRole('heading', { name: '魔法笔记' })
      ).toBeInTheDocument()
      expect(screen.getByText('全局')).toBeInTheDocument()
      expect(
        screen.getByRole('button', { name: '新建笔记' })
      ).toBeInTheDocument()
      expect(api.magicNotes.list).toHaveBeenCalled()
      expect(
        screen.queryByLabelText('切换助手工作栏')
      ).not.toBeInTheDocument()
    } finally {
      delete api.updates
    }
  })

  it('hides Magic Notes when the platform feature is disabled', async () => {
    api.updates = {
      getSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: false,
        magicNotesEnabled: false,
        magicNoteCommentMode: 'immediate' as const
      })),
      updateSettings: vi.fn(async () => ({
        checkUpdatesOnStartup: false,
        magicNotesEnabled: false,
        magicNoteCommentMode: 'immediate' as const
      })),
      check: vi.fn(),
      openReleasePage: vi.fn(async () => {}),
      onResult: vi.fn(() => () => {})
    }
    try {
      render(<App />)
      await waitFor(() =>
        expect(
          screen.queryByRole('button', { name: '魔法笔记' })
        ).not.toBeInTheDocument()
      )
    } finally {
      delete api.updates
    }
  })

  it('hides Magic Notes by default without an explicit setting', () => {
    render(<App />)

    expect(
      screen.queryByRole('button', { name: '魔法笔记' })
    ).not.toBeInTheDocument()
  })

  it('keeps platform-feature switches in Settings without navigating', async () => {
    let applicationSettings: ApplicationSettings = {
      checkUpdatesOnStartup: false,
      magicNotesEnabled: false,
      magicNoteCommentMode: 'immediate'
    }
    api.updates = {
      getSettings: vi.fn(async () => ({ ...applicationSettings })),
      updateSettings: vi.fn(async (input) => {
        applicationSettings = {
          ...applicationSettings,
          ...input
        }
        return { ...applicationSettings }
      }),
      check: vi.fn(),
      openReleasePage: vi.fn(async () => {}),
      onResult: vi.fn(() => () => {})
    }
    try {
      const { container } = render(<App />)
      fireEvent.click(await screen.findByText('本地工作区'))
      fireEvent.click(
        screen.getByRole('tab', { name: '平台功能' })
      )
      const toggle = await screen.findByRole('switch', {
        name: '显示魔法笔记入口'
      })

      fireEvent.click(toggle)
      await waitFor(() => expect(toggle).toBeChecked())
      expect(
        screen.getByRole('heading', { name: '设置中心' })
      ).toBeInTheDocument()
      expect(
        screen.getByRole('button', { name: '魔法笔记' })
      ).toBeInTheDocument()
      expect(
        container.querySelector('.magic-notes-page')
      ).not.toBeInTheDocument()

      fireEvent.click(toggle)
      await waitFor(() => expect(toggle).not.toBeChecked())
      expect(
        screen.getByRole('heading', { name: '设置中心' })
      ).toBeInTheDocument()
      expect(
        screen.queryByRole('button', { name: '魔法笔记' })
      ).not.toBeInTheDocument()
    } finally {
      delete api.updates
    }
  })

  it('gives the knowledge workspace the full content width', async () => {
    render(<App />)

    fireEvent.click(screen.getByRole('button', { name: '知识库' }))

    expect(
      await screen.findByLabelText('知识工作区')
    ).toBeInTheDocument()
    expect(
      screen.queryByLabelText('切换助手工作栏')
    ).not.toBeInTheDocument()
    expect(
      screen.queryByLabelText('专家角色')
    ).not.toBeInTheDocument()
  })

  it('keeps Smart Heartbeat available when the runtime is not configured', async () => {
    vi.mocked(api.agent.getStatus).mockResolvedValue({
      id: 'setup',
      label: '需要配置模型',
      available: false,
      supportsToolExecution: false,
      detail: '请配置模型'
    })
    render(<App />)

    expect(
      await screen.findByRole('heading', { name: '设置中心' })
    ).toBeInTheDocument()
    await waitFor(() =>
      expect(api.agent.getStatus).toHaveBeenCalledOnce()
    )
    fireEvent.click(
      screen.getByRole('button', { name: '智能心跳' })
    )

    expect(
      await screen.findByRole('heading', { name: '智能心跳' })
    ).toBeInTheDocument()
    expect(api.agent.getStatus).toHaveBeenCalledOnce()
  })
})
