mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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:
parent
d3d7566f0c
commit
d9ce2f09e1
@ -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 />)
|
||||
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': 'ターミナル',
|
||||
|
||||
@ -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': '터미널',
|
||||
|
||||
@ -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': '終端',
|
||||
|
||||
@ -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': '终端',
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -41,6 +41,7 @@ export type SettingsTab =
|
||||
| 'memory'
|
||||
| 'plugins'
|
||||
| 'computerUse'
|
||||
| 'trace'
|
||||
| 'diagnostics'
|
||||
| 'about'
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user