mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): configure Codex exploration collapse
Default Codex exploration groups to collapsed and expose a persisted chat setting for users who prefer them expanded.\n\nvia [HAPI](https://hapi.run)\n\nCo-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -170,7 +170,7 @@ describe('Codex activity headings', () => {
|
||||
})
|
||||
|
||||
describe('buildVisibleChatBlocks', () => {
|
||||
it('renders one or more structured Codex exploration commands as an open exploration group', () => {
|
||||
it('renders one or more structured Codex exploration commands as a collapsed exploration group', () => {
|
||||
const read = makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
command_source: 'agent',
|
||||
@@ -198,10 +198,25 @@ describe('buildVisibleChatBlocks', () => {
|
||||
expect(isToolGroupBlock(visible[0])).toBe(true)
|
||||
if (!isToolGroupBlock(visible[0])) throw new Error('expected exploration group')
|
||||
expect(visible[0].presentationMode).toBe('codex-exploration')
|
||||
expect(visible[0].defaultOpen).toBe(true)
|
||||
expect(visible[0].defaultOpen).toBe(false)
|
||||
expect(visible[0].tools.map((tool) => tool.id)).toEqual(['codex-read', 'codex-search'])
|
||||
})
|
||||
|
||||
it('opens exploration groups when the collapse preference is disabled', () => {
|
||||
const visible = buildVisibleChatBlocks([
|
||||
makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
command_actions: [{ type: 'read', command: 'cat package.json', name: 'package.json', path: '/repo/package.json' }]
|
||||
}),
|
||||
makeToolBlock('codex-search', 'CodexBash', {
|
||||
command: 'rg nativeTitle web/src',
|
||||
command_actions: [{ type: 'search', command: 'rg nativeTitle web/src', query: 'nativeTitle' }]
|
||||
})
|
||||
], { hasMoreMessages: false, codexExplorationCollapsed: false })
|
||||
|
||||
expect(isToolGroupBlock(visible[0]) && visible[0].defaultOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps structured general Codex commands separate from exploration groups', () => {
|
||||
const read = makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
|
||||
@@ -55,6 +55,7 @@ export function visibleBlockRole(block: VisibleChatBlock): VisibleChatBlockRole
|
||||
type ToolGroupingOptions = {
|
||||
hasMoreMessages: boolean
|
||||
previousGroups?: ToolGroupBlock[]
|
||||
codexExplorationCollapsed?: boolean
|
||||
}
|
||||
|
||||
const PLAN_TOOL_NAMES = new Set([
|
||||
@@ -305,7 +306,7 @@ export function buildVisibleChatBlocks(
|
||||
firstToolId: tools[0].id,
|
||||
lastToolId: tools[tools.length - 1].id,
|
||||
tools,
|
||||
defaultOpen: groupingFamily === 'codex-exploration',
|
||||
defaultOpen: groupingFamily === 'codex-exploration' && options.codexExplorationCollapsed === false,
|
||||
historyState: needsOlderHistory ? 'needs-older-history' : 'complete',
|
||||
needsOlderHistory,
|
||||
activityTitle,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { reconcileChatBlocks } from '@/chat/reconcile'
|
||||
import { buildConversationOutline } from '@/chat/outline'
|
||||
import { buildVisibleChatBlocks, isToolGroupBlock, visibleBlockRole, type ToolGroupBlock } from '@/chat/toolGroups'
|
||||
import { useUnseenBlockCount } from '@/hooks/useUnseenBlockCount'
|
||||
import { useCodexExplorationCollapse } from '@/hooks/useCodexExplorationCollapse'
|
||||
import { isQueuedForInvocation } from '@/lib/messages'
|
||||
import { inactiveSessionCanResume } from '@/lib/sessionResume'
|
||||
import {
|
||||
@@ -480,6 +481,7 @@ export function SessionChat(props: SessionChatProps) {
|
||||
function SessionChatInner(props: SessionChatProps) {
|
||||
const { haptic } = usePlatform()
|
||||
const { t } = useTranslation()
|
||||
const { codexExplorationCollapsed } = useCodexExplorationCollapse()
|
||||
const navigate = useNavigate()
|
||||
const [historyActionPending, setHistoryActionPending] = useState(false)
|
||||
|
||||
@@ -1164,9 +1166,10 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
const visibleBlocks = useMemo(
|
||||
() => buildVisibleChatBlocks(reconciled.blocks, {
|
||||
hasMoreMessages: props.hasMoreMessages,
|
||||
previousGroups: visibleGroupsRef.current
|
||||
previousGroups: visibleGroupsRef.current,
|
||||
codexExplorationCollapsed
|
||||
}),
|
||||
[reconciled.blocks, props.hasMoreMessages]
|
||||
[reconciled.blocks, props.hasMoreMessages, codexExplorationCollapsed]
|
||||
)
|
||||
|
||||
// Fork-current must compare against assistant-ui message ids (`kind:id`),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_CODEX_EXPLORATION_COLLAPSED,
|
||||
getInitialCodexExplorationCollapsed,
|
||||
} from './useCodexExplorationCollapse'
|
||||
|
||||
describe('useCodexExplorationCollapse helpers', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('defaults to collapsed', () => {
|
||||
expect(getInitialCodexExplorationCollapsed()).toBe(DEFAULT_CODEX_EXPLORATION_COLLAPSED)
|
||||
})
|
||||
|
||||
it('reads valid stored values and ignores invalid values', () => {
|
||||
window.localStorage.setItem('hapi-codex-exploration-collapsed', 'false')
|
||||
expect(getInitialCodexExplorationCollapsed()).toBe(false)
|
||||
|
||||
window.localStorage.setItem('hapi-codex-exploration-collapsed', 'invalid')
|
||||
expect(getInitialCodexExplorationCollapsed()).toBe(DEFAULT_CODEX_EXPLORATION_COLLAPSED)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const DEFAULT_CODEX_EXPLORATION_COLLAPSED = true
|
||||
|
||||
const CODEX_EXPLORATION_COLLAPSED_STORAGE_KEY = 'hapi-codex-exploration-collapsed'
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
}
|
||||
|
||||
function safeGetItem(): string | null {
|
||||
if (!isBrowser()) return null
|
||||
try {
|
||||
return localStorage.getItem(CODEX_EXPLORATION_COLLAPSED_STORAGE_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function safeSetItem(value: string): void {
|
||||
if (!isBrowser()) return
|
||||
try {
|
||||
localStorage.setItem(CODEX_EXPLORATION_COLLAPSED_STORAGE_KEY, value)
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
function safeRemoveItem(): void {
|
||||
if (!isBrowser()) return
|
||||
try {
|
||||
localStorage.removeItem(CODEX_EXPLORATION_COLLAPSED_STORAGE_KEY)
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}
|
||||
|
||||
function parseCodexExplorationCollapsed(raw: string | null): boolean {
|
||||
if (raw === 'true') return true
|
||||
if (raw === 'false') return false
|
||||
return DEFAULT_CODEX_EXPLORATION_COLLAPSED
|
||||
}
|
||||
|
||||
export function getInitialCodexExplorationCollapsed(): boolean {
|
||||
return parseCodexExplorationCollapsed(safeGetItem())
|
||||
}
|
||||
|
||||
export function useCodexExplorationCollapse(): {
|
||||
codexExplorationCollapsed: boolean
|
||||
setCodexExplorationCollapsed: (value: boolean) => void
|
||||
} {
|
||||
const [codexExplorationCollapsed, setCodexExplorationCollapsedState] = useState<boolean>(getInitialCodexExplorationCollapsed)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser()) return
|
||||
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== CODEX_EXPLORATION_COLLAPSED_STORAGE_KEY) return
|
||||
setCodexExplorationCollapsedState(parseCodexExplorationCollapsed(event.newValue))
|
||||
}
|
||||
|
||||
window.addEventListener('storage', onStorage)
|
||||
return () => window.removeEventListener('storage', onStorage)
|
||||
}, [])
|
||||
|
||||
const setCodexExplorationCollapsed = useCallback((value: boolean) => {
|
||||
setCodexExplorationCollapsedState(value)
|
||||
if (value === DEFAULT_CODEX_EXPLORATION_COLLAPSED) {
|
||||
safeRemoveItem()
|
||||
} else {
|
||||
safeSetItem(String(value))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { codexExplorationCollapsed, setCodexExplorationCollapsed }
|
||||
}
|
||||
@@ -752,6 +752,8 @@ export default {
|
||||
'settings.chat.terminalToolDisplay': 'Terminal Tool Cards',
|
||||
'settings.chat.terminalToolDisplay.compact': 'Compact (command only)',
|
||||
'settings.chat.terminalToolDisplay.detailed': 'Detailed (show output preview)',
|
||||
'settings.chat.codexExplorationCollapsed': 'Collapse explored tool groups by default',
|
||||
'settings.chat.codexExplorationCollapsed.desc': 'Keep Codex read and search activity collapsed until you open it.',
|
||||
'settings.chat.groupedToolBackground': 'Grouped Tool Use Background',
|
||||
'settings.chat.userMessageBackground': 'User Message Background',
|
||||
'settings.chat.surfaceColor.default': 'Default color',
|
||||
|
||||
@@ -756,6 +756,8 @@ export default {
|
||||
'settings.chat.terminalToolDisplay': '终端工具卡片',
|
||||
'settings.chat.terminalToolDisplay.compact': '简洁(仅命令)',
|
||||
'settings.chat.terminalToolDisplay.detailed': '详细(显示输出预览)',
|
||||
'settings.chat.codexExplorationCollapsed': '已探索工具组默认收起',
|
||||
'settings.chat.codexExplorationCollapsed.desc': 'Codex 的读取和搜索活动默认收起,点击后查看详情。',
|
||||
'settings.chat.groupedToolBackground': '聚合 Tool Use 背景',
|
||||
'settings.chat.userMessageBackground': '用户消息背景',
|
||||
'settings.chat.surfaceColor.default': '默认颜色',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { getComposerEnterBehaviorOptions, useComposerEnterBehavior } from '@/hooks/useComposerEnterBehavior'
|
||||
import { getTerminalToolDisplayModeOptions, useTerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
|
||||
import { useCodexExplorationCollapse } from '@/hooks/useCodexExplorationCollapse'
|
||||
import {
|
||||
getChatSurfaceColorPickerValue,
|
||||
getChatSurfaceColorPresetOptions,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
type ChatSurfaceColorPreference,
|
||||
type ChatSurfaceColorPreset,
|
||||
} from '@/hooks/useChatSurfaceColors'
|
||||
import { SettingsChoiceGroup, SettingsFieldLabel, SettingsPageContent, SettingsSection } from '@/components/settings/SettingsPrimitives'
|
||||
import { SettingsChoiceGroup, SettingsFieldLabel, SettingsPageContent, SettingsSection, SettingsSwitch } from '@/components/settings/SettingsPrimitives'
|
||||
import { ComposerToolbarLayoutControl } from '@/components/settings/ComposerToolbarLayoutControl'
|
||||
|
||||
function ChatSurfaceColorControl(props: {
|
||||
@@ -48,6 +49,7 @@ export default function SettingsChatPage() {
|
||||
const { t } = useTranslation()
|
||||
const { composerEnterBehavior, setComposerEnterBehavior } = useComposerEnterBehavior()
|
||||
const { terminalToolDisplayMode, setTerminalToolDisplayMode } = useTerminalToolDisplayMode()
|
||||
const { codexExplorationCollapsed, setCodexExplorationCollapsed } = useCodexExplorationCollapse()
|
||||
const { toolGroupBackground, userMessageBackground, setToolGroupBackground, setUserMessageBackground } = useChatSurfaceColors()
|
||||
return (
|
||||
<SettingsPageContent description={t('settings.chat.description')}>
|
||||
@@ -67,6 +69,12 @@ export default function SettingsChatPage() {
|
||||
options={getTerminalToolDisplayModeOptions().map((option) => ({ value: option.value, label: t(option.labelKey) }))}
|
||||
onChange={setTerminalToolDisplayMode}
|
||||
/>
|
||||
<SettingsSwitch
|
||||
label={t('settings.chat.codexExplorationCollapsed')}
|
||||
description={t('settings.chat.codexExplorationCollapsed.desc')}
|
||||
checked={codexExplorationCollapsed}
|
||||
onChange={setCodexExplorationCollapsed}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<SettingsSection title={t('settings.chat.colors')}>
|
||||
<ChatSurfaceColorControl label={t('settings.chat.groupedToolBackground')} preference={toolGroupBackground} onPresetChange={(preset) => setToolGroupBackground(toPresetChatSurfaceColorPreference(preset))} onCustomChange={(value) => setToolGroupBackground(toCustomChatSurfaceColorPreference(value))} />
|
||||
|
||||
@@ -10,7 +10,7 @@ import SettingsVoicePage from './voice'
|
||||
import SettingsVoiceVoicesPage from './voice-voices'
|
||||
import SettingsVoiceAdvancedPage from './voice-advanced'
|
||||
|
||||
const { context, navigate, setAppearance, setColorTheme, setFontScale, setTerminalFontSize, setComposerEnterBehavior, setVoice } = vi.hoisted(() => ({
|
||||
const { context, navigate, setAppearance, setColorTheme, setFontScale, setTerminalFontSize, setComposerEnterBehavior, setCodexExplorationCollapsed, setVoice } = vi.hoisted(() => ({
|
||||
context: { token: '' },
|
||||
navigate: vi.fn(),
|
||||
setAppearance: vi.fn(),
|
||||
@@ -18,6 +18,7 @@ const { context, navigate, setAppearance, setColorTheme, setFontScale, setTermin
|
||||
setFontScale: vi.fn(),
|
||||
setTerminalFontSize: vi.fn(),
|
||||
setComposerEnterBehavior: vi.fn(),
|
||||
setCodexExplorationCollapsed: vi.fn(),
|
||||
setVoice: vi.fn(),
|
||||
}))
|
||||
|
||||
@@ -129,6 +130,10 @@ vi.mock('@/hooks/useTerminalToolDisplayMode', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useCodexExplorationCollapse', () => ({
|
||||
useCodexExplorationCollapse: () => ({ codexExplorationCollapsed: true, setCodexExplorationCollapsed }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useChatSurfaceColors', () => ({
|
||||
useChatSurfaceColors: () => ({
|
||||
toolGroupBackground: 'default',
|
||||
@@ -261,6 +266,14 @@ describe('responsive settings pages', () => {
|
||||
expect(screen.getByText('Grouped Tool Use Background')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the default-collapse switch for Codex exploration groups', () => {
|
||||
renderPage(<SettingsChatPage />)
|
||||
const toggle = screen.getByRole('checkbox', { name: 'Collapse explored tool groups by default' })
|
||||
expect(toggle).toBeChecked()
|
||||
fireEvent.click(toggle)
|
||||
expect(setCodexExplorationCollapsed).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('renders About metadata on its own route page', () => {
|
||||
renderPage(<SettingsAboutPage />)
|
||||
expect(screen.queryByText('Companion')).not.toBeInTheDocument()
|
||||
|
||||
Reference in New Issue
Block a user