refactor(desktop): move trace entry into Settings

Trace is a low-frequency diagnostic feature, so its entry points no
longer sit in primary chrome:

- Remove the Trace nav item above the session list in the sidebar
- Remove the trace open/open-window buttons from the session header
- Add a Trace tab in Settings between Token usage and Diagnostics,
  embedding the existing TraceList page full-bleed
- Add settings.tab.trace and drop unused sidebar.traces across locales
This commit is contained in:
程序员阿江(Relakkes) 2026-06-11 01:42:40 +08:00
parent d3d7566f0c
commit d9ce2f09e1
11 changed files with 31 additions and 122 deletions

View File

@ -94,6 +94,10 @@ vi.mock('../pages/ActivitySettings', () => ({
ActivitySettings: () => <div>Activity Settings Mock</div>,
}))
vi.mock('../pages/TraceList', () => ({
TraceList: () => <div>Trace List Mock</div>,
}))
vi.mock('../stores/agentStore', () => ({
useAgentStore: () => ({
activeAgents: [],
@ -677,6 +681,20 @@ describe('Settings > General tab', () => {
expect(screen.getByText('Activity Settings Mock')).toBeInTheDocument()
})
it('opens the Trace tab from Settings navigation between Token usage and Diagnostics', () => {
render(<Settings />)
const usageTab = screen.getByText('Token usage')
const traceTab = screen.getByText('Trace')
const diagnosticsTab = screen.getByText('Diagnostics')
expect((usageTab.compareDocumentPosition(traceTab) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
expect((traceTab.compareDocumentPosition(diagnosticsTab) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
fireEvent.click(traceTab)
expect(screen.getByText('Trace List Mock')).toBeInTheDocument()
})
it('lets the user disable WebFetch preflight skipping', () => {
render(<Settings />)

View File

@ -5,7 +5,7 @@ import { useUIStore } from '../../stores/uiStore'
import { useTranslation, type TranslationKey } from '../../i18n'
import { ConfirmDialog } from '../shared/ConfirmDialog'
import type { SessionListItem } from '../../types/session'
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID, TRACE_LIST_TAB_ID } from '../../stores/tabStore'
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore'
import { useChatStore } from '../../stores/chatStore'
import { useOpenTargetStore } from '../../stores/openTargetStore'
import { desktopUiPreferencesApi, type SidebarProjectPreferences } from '../../api/desktopUiPreferences'
@ -686,21 +686,6 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
{t('sidebar.scheduled')}
</NavItem>
)}
{!isMobile && (
<NavItem
active={activeTabId === TRACE_LIST_TAB_ID}
collapsed={!expanded}
label={t('sidebar.traces')}
touchFriendly={isMobile}
onClick={() => {
useTabStore.getState().openTracesTab(t('sidebar.traces'))
closeMobileDrawer()
}}
icon={<span className="material-symbols-outlined text-[18px]">account_tree</span>}
>
{t('sidebar.traces')}
</NavItem>
)}
</div>
{expanded ? (

View File

@ -21,7 +21,6 @@ export const en = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': 'New session',
'sidebar.scheduled': 'Scheduled',
'sidebar.traces': 'Trace',
'sidebar.terminal': 'Terminal',
'sidebar.settings': 'Settings',
'sidebar.searchPlaceholder': 'Search sessions...',
@ -171,6 +170,7 @@ export const en = {
'settings.tab.providers': 'Providers',
'settings.tab.permissions': 'Permissions',
'settings.tab.activity': 'Token usage',
'settings.tab.trace': 'Trace',
'settings.tab.general': 'General',
'settings.tab.h5Access': 'H5 Access',
'settings.tab.terminal': 'Terminal',

View File

@ -23,7 +23,6 @@ export const jp: Record<TranslationKey, string> = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '新しいセッション',
'sidebar.scheduled': 'スケジュール',
'sidebar.traces': 'Trace',
'sidebar.terminal': 'ターミナル',
'sidebar.settings': '設定',
'sidebar.searchPlaceholder': 'セッションを検索...',
@ -173,6 +172,7 @@ export const jp: Record<TranslationKey, string> = {
'settings.tab.providers': 'プロバイダー',
'settings.tab.permissions': '権限',
'settings.tab.activity': 'トークン使用量',
'settings.tab.trace': 'Trace',
'settings.tab.general': '一般',
'settings.tab.h5Access': 'H5 アクセス',
'settings.tab.terminal': 'ターミナル',

View File

@ -23,7 +23,6 @@ export const kr: Record<TranslationKey, string> = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '새 세션',
'sidebar.scheduled': '예약 작업',
'sidebar.traces': 'Trace',
'sidebar.terminal': '터미널',
'sidebar.settings': '설정',
'sidebar.searchPlaceholder': '세션 검색...',
@ -173,6 +172,7 @@ export const kr: Record<TranslationKey, string> = {
'settings.tab.providers': '공급자',
'settings.tab.permissions': '권한',
'settings.tab.activity': '토큰 사용량',
'settings.tab.trace': 'Trace',
'settings.tab.general': '일반',
'settings.tab.h5Access': 'H5 액세스',
'settings.tab.terminal': '터미널',

View File

@ -23,7 +23,6 @@ export const zh: Record<TranslationKey, string> = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '新建會話',
'sidebar.scheduled': '定時任務',
'sidebar.traces': 'Trace',
'sidebar.terminal': '終端',
'sidebar.settings': '設定',
'sidebar.searchPlaceholder': '搜尋會話...',
@ -173,6 +172,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.tab.providers': '服務商',
'settings.tab.permissions': '許可權',
'settings.tab.activity': 'Token 用量',
'settings.tab.trace': 'Trace',
'settings.tab.general': '通用',
'settings.tab.h5Access': 'H5 訪問',
'settings.tab.terminal': '終端',

View File

@ -23,7 +23,6 @@ export const zh: Record<TranslationKey, string> = {
// ─── Sidebar ──────────────────────────────────────
'sidebar.newSession': '新建会话',
'sidebar.scheduled': '定时任务',
'sidebar.traces': 'Trace',
'sidebar.terminal': '终端',
'sidebar.settings': '设置',
'sidebar.searchPlaceholder': '搜索会话...',
@ -173,6 +172,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.tab.providers': '服务商',
'settings.tab.permissions': '权限',
'settings.tab.activity': 'Token 用量',
'settings.tab.trace': 'Trace',
'settings.tab.general': '通用',
'settings.tab.h5Access': 'H5 访问',
'settings.tab.terminal': '终端',

View File

@ -186,67 +186,6 @@ describe('ActiveSession task polling', () => {
expect(screen.getByTestId('chat-input')).toHaveAttribute('data-variant', 'default')
})
it('opens the current session trace in a separate live window', () => {
const sessionId = 'trace-window-session'
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
useSessionStore.setState({
sessions: [{
id: sessionId,
title: 'Trace Window Session',
createdAt: '2026-05-07T00:00:00.000Z',
modifiedAt: '2026-05-07T00:00:00.000Z',
messageCount: 1,
projectPath: '/workspace/project',
workDir: '/workspace/project',
workDirExists: true,
}],
activeSessionId: sessionId,
isLoading: false,
error: null,
})
useTabStore.setState({
tabs: [{ sessionId, title: 'Trace Window Session', type: 'session', status: 'idle' }],
activeTabId: sessionId,
})
useChatStore.setState({
sessions: {
[sessionId]: {
messages: [{ id: 'msg-1', type: 'assistant_text', content: 'hello', timestamp: 1 }],
chatState: 'idle',
connectionState: 'connected',
streamingText: '',
streamingToolInput: '',
activeToolUseId: null,
activeToolName: null,
activeThinkingId: null,
pendingPermission: null,
pendingComputerUsePermission: null,
tokenUsage: { input_tokens: 0, output_tokens: 0 },
elapsedSeconds: 0,
statusVerb: '',
slashCommands: [],
agentTaskNotifications: {},
elapsedTimer: null,
},
},
})
try {
render(<ActiveSession />)
fireEvent.click(screen.getByLabelText(/独立|separate/i))
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining(`traceSessionId=${sessionId}`),
'_blank',
'noopener,noreferrer',
)
expect(openSpy.mock.calls[0]?.[0]).toContain('traceWindow=1')
} finally {
openSpy.mockRestore()
}
})
it('renders the current goal as a lightweight header strip without a page-level panel', () => {
const sessionId = 'goal-visible-session'

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { ExternalLink, RadioTower, Target } from 'lucide-react'
import { Target } from 'lucide-react'
import {
SCHEDULED_TAB_ID,
SETTINGS_TAB_ID,
@ -32,8 +32,6 @@ import type { ActiveGoalState } from '../types/chat'
import { useMobileViewport } from '../hooks/useMobileViewport'
import { isDesktopRuntime } from '../lib/desktopRuntime'
import { publicAssetPath } from '../lib/publicAsset'
import { getDesktopHost } from '../lib/desktopHost'
import { buildTraceWindowUrl } from '../lib/traceLaunch'
const TASK_POLL_INTERVAL_MS = 1000
const WORKSPACE_RESIZE_STEP = 32
@ -364,22 +362,6 @@ export function ActiveSession() {
return t('session.timeDays', { n: Math.floor(diff / 86400000) })
}, [session?.modifiedAt, t])
const openTrace = () => {
if (!activeTabId) return
const title = session?.title || t('session.untitled')
useTabStore.getState().openTraceTab(activeTabId, `${t('trace.title')}: ${title}`)
}
const openTraceWindow = () => {
if (!activeTabId) return
const host = getDesktopHost()
if (host.trace) {
void host.trace.openWindow(activeTabId)
return
}
window.open(buildTraceWindowUrl(activeTabId), '_blank', 'noopener,noreferrer')
}
if (!activeTabId) return null
return (
@ -480,25 +462,6 @@ export function ActiveSession() {
>
{session?.title || t('session.untitled')}
</h1>
<button
type="button"
aria-label={t('trace.open')}
title={t('trace.open')}
onClick={openTrace}
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-[8px] border border-[var(--color-border)] px-2.5 text-xs font-semibold text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-focus)] hover:text-[var(--color-text-primary)] active:scale-[0.98]"
>
<RadioTower size={14} strokeWidth={2} aria-hidden="true" />
<span className={showRightPanel ? 'sr-only' : 'hidden sm:inline'}>{t('trace.open')}</span>
</button>
<button
type="button"
aria-label={t('trace.openWindow')}
title={t('trace.openWindow')}
onClick={openTraceWindow}
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[8px] border border-[var(--color-border)] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-focus)] hover:text-[var(--color-text-primary)] active:scale-[0.98]"
>
<ExternalLink size={14} strokeWidth={2} aria-hidden="true" />
</button>
</div>
<div
className={

View File

@ -28,6 +28,7 @@ import { ComputerUseSettings } from './ComputerUseSettings'
import { McpSettings } from './McpSettings'
import { TerminalSettings } from './TerminalSettings'
import { DiagnosticsSettings } from './DiagnosticsSettings'
import { TraceList } from './TraceList'
import { ActivitySettings } from './ActivitySettings'
import { MemorySettings } from './MemorySettings'
import { useUIStore, type SettingsTab } from '../stores/uiStore'
@ -166,6 +167,7 @@ export function Settings() {
<TabButton icon="extension" label={t('settings.tab.plugins')} active={activeTab === 'plugins'} onClick={() => setActiveTab('plugins')} />
<TabButton icon="mouse" label={t('settings.tab.computerUse')} active={activeTab === 'computerUse'} onClick={() => setActiveTab('computerUse')} />
<TabButton icon="monitoring" label={t('settings.tab.activity')} active={activeTab === 'activity'} onClick={() => setActiveTab('activity')} />
<TabButton icon="account_tree" label={t('settings.tab.trace')} active={activeTab === 'trace'} onClick={() => setActiveTab('trace')} />
<TabButton icon="monitor_heart" label={t('settings.tab.diagnostics')} active={activeTab === 'diagnostics'} onClick={() => setActiveTab('diagnostics')} />
</div>
<div className="border-t border-[var(--color-border)]/40 pt-1">
@ -173,8 +175,8 @@ export function Settings() {
</div>
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto px-8 py-6">
{/* Tab content; trace embeds a full-bleed page that manages its own scroll */}
<div className={activeTab === 'trace' ? 'flex-1 flex min-h-0 flex-col overflow-hidden' : 'flex-1 overflow-y-auto px-8 py-6'}>
{activeTab === 'providers' && <ProviderSettings />}
{activeTab === 'activity' && <ActivitySettings />}
{activeTab === 'general' && <GeneralSettings />}
@ -187,6 +189,7 @@ export function Settings() {
{activeTab === 'memory' && <MemorySettings />}
{activeTab === 'plugins' && <PluginSettings />}
{activeTab === 'computerUse' && <ComputerUseSettings />}
{activeTab === 'trace' && <TraceList />}
{activeTab === 'diagnostics' && <DiagnosticsSettings />}
{activeTab === 'about' && <AboutSettings />}
</div>

View File

@ -41,6 +41,7 @@ export type SettingsTab =
| 'memory'
| 'plugins'
| 'computerUse'
| 'trace'
| 'diagnostics'
| 'about'