feat(desktop): information architecture overhaul — home, records, meetings first

Wave 2 of the 2026-09-28 design overhaul ("records are the main
character, a quiet instrument"). Decisions: design.md.

- Shell: sidebar with primary Home / Records / Meetings (Ctrl+1..3) and a
  secondary "Tools" group; every item is a button with aria-current.
  The status bar is removed — its "100% LOCAL PRIVACY · ZERO CLOUD SYNC"
  claim contradicted device sync; the sidebar shows only queried state.
- Navigation context (routes, params, back, useRouteRequest) so Home can
  open a specific meeting or record.
- Home replaces the dashboard: dictation key, start actions, only real
  problems, one-line summary, recent records and meetings, file
  transcription, free-tier limits.
- Settings become a full-screen route with vertical tabs (tabs no longer
  clip) and a shared row grammar; long-standing bugs fixed: team and
  enterprise tiers crashed the license tab (currentTier undefined),
  first-run onboarding could not be left, Pro+ saw "upgrade to Pro+".
- Records: date-grouped single list with in-place expansion, search and
  tags together, mode/favorite filters, load more past 50, undoable
  delete. Meetings: list with search/rename/delete, summary-first detail
  (summary | transcript | documents | my notes), notes persisted locally
  instead of being lost. Tools pages cleaned up; stale-closure example
  chips and a 1550x mouse-distance bug fixed.
- Shared pieces: PageHeader, EmptyStateCard, SearchInput ("/" works),
  ConfirmDialog, notify with undo; display-labels maps domain values to
  typed i18n keys so missing keys fail to compile.
- Mono font stack falls back to Pretendard for Hangul (Korean labels no
  longer render spaced out).
- i18n: new ko/en keys via scripts/i18n-add-keys.mjs (one read-merge-
  write so parallel editors cannot clobber locale files).

Some renderer files also carried pre-existing in-flight changes (error
recovery deep links, meeting document hooks); they are included as-is.
This commit is contained in:
Yun Chan 2026-09-28 20:46:08 +09:00
parent ba9ef9741e
commit f064c93197
96 changed files with 12886 additions and 8924 deletions

View file

@ -0,0 +1,97 @@
// tests/unit/renderer-tools-model.test.ts
// 도구 화면(명령어·AI 대화·지식 베이스·사전)의 순수 판단:
// - 명령어 요약·라디오 선택은 main 의 경로 규칙과 같다(지시문은 기본 액션 'custom' 일 때만)
// - 시험 실행은 실제 경로를 따른다(원문 삽입 설정에서 'refine' 을 돌리지 않는다)
// - AI 대화 상태 표지는 세션이 꺼져 있으면 없다
// - 지연 삭제 중인 항목은 목록에서 빠진다
import { describe, expect, it } from 'vitest'
import {
COMMAND_NONE,
commandSelection,
commandTestRequest,
conversationBadge,
documentIndexLabelKey,
llmActionLabelKey,
resolveCommandPipeline,
withoutHidden,
} from '../../src/renderer/pages/tools/tools-model'
const polish = { id: 'i1', name: '정중하게', prompt: '{{text}} 를 정중하게' }
const list = [polish, { id: 'i2', name: '요약', prompt: '{{text}} 요약' }]
describe('resolveCommandPipeline', () => {
it("기본 액션 'none' 이면 원문 그대로 — 활성 지시문 id 가 남아 있어도", () => {
const p = resolveCommandPipeline({ defaultLLMAction: 'none', activeInstructionId: 'i1' }, list)
expect(p.kind).toBe('raw')
expect(commandSelection(p)).toBe(COMMAND_NONE)
})
it("'custom' + 활성 지시문이면 그 지시문", () => {
const p = resolveCommandPipeline({ defaultLLMAction: 'custom', activeInstructionId: 'i1' }, list)
expect(p).toEqual({ kind: 'instruction', instruction: polish })
expect(commandSelection(p)).toBe('i1')
})
it("'custom' 인데 지시문이 지워졌으면 '선택 안 됨'으로 정직하게", () => {
const p = resolveCommandPipeline({ defaultLLMAction: 'custom', activeInstructionId: 'gone' }, list)
expect(p.kind).toBe('instruction-missing')
expect(commandSelection(p)).toBe('')
})
it('일반 액션이 기본이면 활성 지시문이 있어도 그 액션이다(main 규칙과 같다) — 라디오는 아무것도 고르지 않는다', () => {
const p = resolveCommandPipeline({ defaultLLMAction: 'refine', activeInstructionId: 'i1' }, list)
expect(p).toEqual({ kind: 'action', action: 'refine' })
expect(commandSelection(p)).toBe('')
expect(llmActionLabelKey('refine')).toBe('settings.action.refine')
})
})
describe('commandTestRequest', () => {
it('지시문 경로는 custom + 지시문 원문으로 요청한다(치환은 main 이 한다)', () => {
const r = commandTestRequest({ kind: 'instruction', instruction: polish }, ' 안녕 ')
expect(r).toEqual({ ok: true, params: { text: '안녕', action: 'custom', customPrompt: polish.prompt } })
})
it('원문 삽입 설정에서는 실행하지 않는다 — 예전엔 refine 결과를 보여 줬다', () => {
expect(commandTestRequest({ kind: 'raw' }, '안녕')).toEqual({ ok: false, reason: 'raw' })
})
it('체인은 한 번의 LLM 호출로 흉내 내지 않는다', () => {
expect(commandTestRequest({ kind: 'action', action: 'chain' }, '안녕')).toEqual({ ok: false, reason: 'chain' })
})
it('빈 입력은 실행하지 않는다', () => {
expect(commandTestRequest({ kind: 'action', action: 'refine' }, ' ')).toEqual({ ok: false, reason: 'empty' })
})
})
describe('conversationBadge', () => {
it('세션이 꺼져 있으면 표지가 없다 — 켜지지 않은 대화를 대기라 쓰지 않는다', () => {
expect(conversationBadge({ active: false, state: 'idle', connecting: false })).toBeNull()
})
it('강조색은 듣는 중에만', () => {
expect(conversationBadge({ active: true, state: 'listening', connecting: false })?.tone).toBe('accent')
expect(conversationBadge({ active: true, state: 'thinking', connecting: false })?.tone).toBe('default')
})
it('연결 중이면 세션 상태보다 먼저 연결 중이라 쓴다', () => {
expect(conversationBadge({ active: true, state: 'idle', connecting: true })?.labelKey).toBe(
'conversation.realtime.connecting',
)
})
})
describe('withoutHidden / documentIndexLabelKey', () => {
it('지연 삭제 중인 id 를 목록에서 뺀다', () => {
expect(withoutHidden(list, new Set(['i1'])).map((i) => i.id)).toEqual(['i2'])
expect(withoutHidden(list, new Set())).toHaveLength(2)
})
it('색인이 끝나지 않은 문서는 진행 중일 때만 색인 중, 아니면 색인 안 됨', () => {
expect(documentIndexLabelKey({ id: 'd', indexed: false }, 'd')).toBe('rag.indexing')
expect(documentIndexLabelKey({ id: 'd', indexed: false }, null)).toBe('tools.knowledge.notIndexed')
expect(documentIndexLabelKey({ id: 'd', indexed: true }, null)).toBe('rag.indexed')
})
})