mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
fix(desktop): index session history without narrating it
The sidebar reported "Optimizing history N/M" above the session list while the SQLite index built. Indexing is background housekeeping the user cannot act on, and the counter sat there for the whole build. Drop the visible progress row and narrow the live region to `degraded` — the one state a user can perceive, where history really is served the slow way. Building/ready/off now stay silent for screen readers too, so the behavior is the same regardless of how the sidebar is read. The four locale strings this leaves unused are deleted in all five languages, with a resurrection guard alongside the existing one for the removed installed-skills keys.
This commit is contained in:
parent
8bad67e590
commit
617a0a334d
@ -35,10 +35,6 @@ vi.mock('../../i18n', () => ({
|
||||
'sidebar.noMatching': 'No matching sessions',
|
||||
'sidebar.sessionListFailed': 'Session list failed',
|
||||
'sidebar.refreshSessions': 'Refresh sessions',
|
||||
'sidebar.indexBuilding': 'Optimizing history',
|
||||
'sidebar.indexBuildingProgress': 'Optimizing history {indexed}/{discovered}',
|
||||
'sidebar.indexReady': 'History optimization complete',
|
||||
'sidebar.indexOff': 'History optimization inactive',
|
||||
'sidebar.indexDegraded': 'Using standard history loading',
|
||||
'search.global.trigger': 'Search chats',
|
||||
'sidebar.projects': 'Projects',
|
||||
@ -1294,7 +1290,9 @@ describe('Sidebar', () => {
|
||||
expect(screen.queryByText('No sessions')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows quiet building progress while keeping indexed rows visible', () => {
|
||||
// Indexing is background housekeeping the user cannot act on, so a partially
|
||||
// built index must look exactly like a finished one: rows visible, no counter.
|
||||
it('keeps indexed rows visible while building without surfacing progress', () => {
|
||||
useSessionStore.setState({
|
||||
sessions: [makeSession('indexed-row', 'Indexed row', '/workspace/alpha', '2026-07-15T00:00:00.000Z')],
|
||||
indexStatus: {
|
||||
@ -1313,7 +1311,9 @@ describe('Sidebar', () => {
|
||||
render(<Sidebar />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Indexed row/ })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('sidebar-index-progress')).toHaveTextContent('Optimizing history 2/10')
|
||||
expect(screen.queryByTestId('sidebar-index-progress')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/2\s*\/\s*10/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('status')).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it.each(['ready', 'off'] as const)('hides visible index status when state is %s', (state) => {
|
||||
@ -1565,14 +1565,17 @@ describe('Sidebar', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('announces state transitions without exposing numeric progress to the live region', () => {
|
||||
// The live region exists for the one transition a user can perceive: history
|
||||
// is being served the slow way. Building/ready/off are silent there too, so a
|
||||
// screen reader is not told about work that needs no reaction.
|
||||
it.each(['building', 'ready', 'off'] as const)('stays silent in the live region while %s', (state) => {
|
||||
useSessionStore.setState({
|
||||
sessions: [makeSession('live-row', 'Live row', '/workspace/alpha', '2026-07-15T00:00:00.000Z')],
|
||||
indexStatus: {
|
||||
mode: 'on',
|
||||
state: 'building',
|
||||
mode: state === 'off' ? 'off' : 'on',
|
||||
state,
|
||||
discovered: 10,
|
||||
indexed: 2,
|
||||
indexed: state === 'building' ? 2 : 10,
|
||||
degradedSources: 0,
|
||||
databaseBytes: 4096,
|
||||
walBytes: 0,
|
||||
@ -1583,10 +1586,31 @@ describe('Sidebar', () => {
|
||||
|
||||
render(<Sidebar />)
|
||||
|
||||
expect(screen.getByRole('status')).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('announces the degraded fallback in the live region', () => {
|
||||
useSessionStore.setState({
|
||||
sessions: [makeSession('live-row', 'Live row', '/workspace/alpha', '2026-07-15T00:00:00.000Z')],
|
||||
indexStatus: {
|
||||
mode: 'on',
|
||||
state: 'degraded',
|
||||
discovered: 10,
|
||||
indexed: 2,
|
||||
degradedSources: 1,
|
||||
databaseBytes: 4096,
|
||||
walBytes: 0,
|
||||
lastUpdatedAt: '2026-07-15T00:00:00.000Z',
|
||||
lastErrorCode: 'source_unreadable',
|
||||
},
|
||||
})
|
||||
|
||||
render(<Sidebar />)
|
||||
|
||||
const liveRegion = screen.getByRole('status')
|
||||
expect(liveRegion).toHaveTextContent('Optimizing history')
|
||||
expect(liveRegion).toHaveTextContent('Using standard history loading')
|
||||
expect(liveRegion).not.toHaveTextContent('2/10')
|
||||
expect(screen.getByTestId('sidebar-index-progress')).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(screen.getByTestId('sidebar-index-degraded')).toHaveAttribute('aria-hidden', 'true')
|
||||
})
|
||||
|
||||
it('refreshes sessions manually and through low-frequency visible polling', async () => {
|
||||
|
||||
@ -190,17 +190,12 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
||||
}, [hiddenProjectKeys, orderedProjectGroups])
|
||||
const showInitialLoading = isLoading && sessions.length === 0
|
||||
const showRefreshLoading = showInitialLoading
|
||||
const showIndexBuilding = indexStatus?.state === 'building' && sessions.length > 0
|
||||
// Index building/ready/off are implementation details of how the list is
|
||||
// loaded, not something the user acts on, so they stay silent in both the
|
||||
// visible sidebar and the live region. Only `degraded` is announced: there
|
||||
// the list really is served a different way, which the user can perceive.
|
||||
const showIndexDegraded = indexStatus?.state === 'degraded'
|
||||
const indexAnnouncement = indexStatus
|
||||
? t(indexStatus.state === 'building'
|
||||
? 'sidebar.indexBuilding'
|
||||
: indexStatus.state === 'ready'
|
||||
? 'sidebar.indexReady'
|
||||
: indexStatus.state === 'degraded'
|
||||
? 'sidebar.indexDegraded'
|
||||
: 'sidebar.indexOff')
|
||||
: ''
|
||||
const indexAnnouncement = showIndexDegraded ? t('sidebar.indexDegraded') : ''
|
||||
const filteredSessionIds = useMemo(() => filteredSessions.map((session) => session.id), [filteredSessions])
|
||||
const selectedCount = selectedSessionIds.size
|
||||
const sessionsById = useMemo(
|
||||
@ -889,18 +884,6 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
||||
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
||||
{indexAnnouncement}
|
||||
</div>
|
||||
{showIndexBuilding && (
|
||||
<div
|
||||
data-testid="sidebar-index-progress"
|
||||
aria-hidden="true"
|
||||
className="mx-4 mb-1 flex-none text-[11px] leading-5 text-[var(--color-text-tertiary)]"
|
||||
>
|
||||
{t('sidebar.indexBuildingProgress', {
|
||||
indexed: indexStatus.indexed,
|
||||
discovered: indexStatus.discovered,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{showIndexDegraded && (
|
||||
<div
|
||||
data-testid="sidebar-index-degraded"
|
||||
|
||||
@ -77,4 +77,16 @@ describe('useTranslation', () => {
|
||||
expect(resurrected, `${name} still defines removed installed-skills keys`).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
// Index building/ready/off went silent: the sidebar no longer tells anyone
|
||||
// that history is being indexed. Keeping the strings around invites a future
|
||||
// change to wire them back into the UI, so they must stay deleted.
|
||||
it('carries no key for the silent index states', () => {
|
||||
for (const [name, locale] of Object.entries(locales)) {
|
||||
const resurrected = Object.keys(locale).filter(
|
||||
key => key.startsWith('sidebar.index') && key !== 'sidebar.indexDegraded',
|
||||
)
|
||||
expect(resurrected, `${name} still defines silent index-status keys`).toEqual([])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,10 +48,6 @@ export const en = {
|
||||
'sidebar.noMatching': 'No matching sessions',
|
||||
'sidebar.sessionListFailed': 'Session list failed to load',
|
||||
'sidebar.refreshSessions': 'Refresh sessions',
|
||||
'sidebar.indexBuilding': 'Optimizing history',
|
||||
'sidebar.indexBuildingProgress': 'Optimizing history {indexed}/{discovered}',
|
||||
'sidebar.indexReady': 'History optimization complete',
|
||||
'sidebar.indexOff': 'History optimization inactive',
|
||||
'sidebar.indexDegraded': 'Using standard history loading',
|
||||
'search.global.trigger': 'Search chats',
|
||||
'search.global.placeholder': 'Search all chats…',
|
||||
|
||||
@ -50,10 +50,6 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'sidebar.noMatching': '一致するセッションがありません',
|
||||
'sidebar.sessionListFailed': 'セッション一覧の読み込みに失敗しました',
|
||||
'sidebar.refreshSessions': 'セッションを更新',
|
||||
'sidebar.indexBuilding': '履歴を最適化しています',
|
||||
'sidebar.indexBuildingProgress': '履歴を最適化しています {indexed}/{discovered}',
|
||||
'sidebar.indexReady': '履歴の最適化が完了しました',
|
||||
'sidebar.indexOff': '履歴の最適化は無効です',
|
||||
'sidebar.indexDegraded': '標準の履歴読み込みを使用しています',
|
||||
'search.global.trigger': 'チャットを検索',
|
||||
'search.global.placeholder': 'すべてのチャットを検索…',
|
||||
|
||||
@ -50,10 +50,6 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'sidebar.noMatching': '일치하는 세션이 없습니다',
|
||||
'sidebar.sessionListFailed': '세션 목록을 불러오지 못했습니다',
|
||||
'sidebar.refreshSessions': '세션 새로 고침',
|
||||
'sidebar.indexBuilding': '기록을 최적화하는 중',
|
||||
'sidebar.indexBuildingProgress': '기록을 최적화하는 중 {indexed}/{discovered}',
|
||||
'sidebar.indexReady': '기록 최적화가 완료되었습니다',
|
||||
'sidebar.indexOff': '기록 최적화가 비활성화되었습니다',
|
||||
'sidebar.indexDegraded': '표준 기록 불러오기를 사용하고 있습니다',
|
||||
'search.global.trigger': '대화 검색',
|
||||
'search.global.placeholder': '모든 대화 검색…',
|
||||
|
||||
@ -50,10 +50,6 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'sidebar.noMatching': '沒有匹配的會話',
|
||||
'sidebar.sessionListFailed': '會話列表載入失敗',
|
||||
'sidebar.refreshSessions': '重新整理會話列表',
|
||||
'sidebar.indexBuilding': '正在最佳化歷史記錄',
|
||||
'sidebar.indexBuildingProgress': '正在最佳化歷史記錄 {indexed}/{discovered}',
|
||||
'sidebar.indexReady': '歷史記錄最佳化完成',
|
||||
'sidebar.indexOff': '歷史記錄最佳化未啟用',
|
||||
'sidebar.indexDegraded': '正在使用標準歷史記錄載入',
|
||||
'search.global.trigger': '搜尋聊天',
|
||||
'search.global.placeholder': '搜尋全部聊天內容…',
|
||||
|
||||
@ -50,10 +50,6 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'sidebar.noMatching': '没有匹配的会话',
|
||||
'sidebar.sessionListFailed': '会话列表加载失败',
|
||||
'sidebar.refreshSessions': '刷新会话列表',
|
||||
'sidebar.indexBuilding': '正在优化历史记录',
|
||||
'sidebar.indexBuildingProgress': '正在优化历史记录 {indexed}/{discovered}',
|
||||
'sidebar.indexReady': '历史记录优化完成',
|
||||
'sidebar.indexOff': '历史记录优化未启用',
|
||||
'sidebar.indexDegraded': '正在使用标准历史记录加载',
|
||||
'search.global.trigger': '搜索聊天',
|
||||
'search.global.placeholder': '搜索全部聊天内容…',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user