feat: 대화 내용 전문 검색 - #42
Conversation
프로젝트 루트를 읽지 못했을 때 빈 목록과 구분하지 않아, 일시적 오류 한 번으로 정리 단계가 인덱스를 통째로 지우고 빈 인덱스를 디스크에 덮어썼다. 이후 모든 검색이 아무 신호 없이 '결과 없음'이 됐다. 세션 목록을 못 읽은 프로젝트의 기존 문서도 같은 이유로 사라졌다. 본문이 없는 세션을 인덱스에서 빼던 것도 되돌린다. 빠진 세션은 신선도 검사에 영원히 걸리지 않아 매 정합마다 재파싱되고, 바뀐 게 없는데도 인덱스 전체 재작성과 렌더러 재질의를 5초마다 유발했다. 대소문자 접기가 길이를 바꾸면(U+0130) 매칭 인덱스가 원문과 어긋나 엉뚱한 구간이 강조된다. 길이를 보존하는 foldCase로 질의와 본문에 같은 규칙을 적용한다.
IPC 질의가 실패해도 아무도 받지 않아, 첫 질의면 상태줄이 빈 채로 남고 두 번째 질의면 이전 결과가 새 질의의 결과처럼 남아 있었다. 결과 클릭 시 세션 목록 조회가 실패하면 클릭이 반응 없이 죽었고, 대화 로드 실패는 헤더만 남은 빈 패널이 됐다. 응답의 query를 입력과 대조해 낡은 결과를 그리지 않고, 왕복 중에는 '검색 중'을 보여준다. CapsLock이 켜지면 key가 'F'로 와서 ⌘F가 조용히 먹히지 않던 것도 고친다.
강조 테두리를 키프레임 안에만 두어, prefers-reduced-motion의 전역 animation:none이 이를 통째로 지웠다. 어느 메시지가 매칭됐는지 알 방법이 없어진다. 정적 테두리를 기본값으로 두고 애니메이션은 사라지는 연출만 맡는다. 두 파일에 흩어져 있던 강조 지속 시간은 커스텀 프로퍼티로 묶는다.
이 브랜치가 저장소 최초의 테스트를 들여왔는데 어떤 워크플로도 돌리지 않았다. electron 바이너리와 husky 훅은 검증에 필요 없어 설치에서 건너뛴다.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughWalkthrough로컬 JSONL 검색 색인과 IPC API를 추가했습니다. 대화 본문 검색, 검색 결과 팔레트, 세션 이동과 메시지 강조를 구현했습니다. 상단 바와 사이드바 검색 동작을 변경하고 테스트, 문서, CI 검증을 추가했습니다. Changes대화 내용 검색
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant 사용자
participant SearchPalette
participant App
participant Preload
participant MainIPC
participant SearchIndex
participant ConversationView
사용자->>SearchPalette: 검색어 입력
SearchPalette->>App: 질의 전달
App->>Preload: searchSessions(query)
Preload->>MainIPC: search:query
MainIPC->>SearchIndex: 검색 실행
SearchIndex-->>App: SearchResults
사용자->>SearchPalette: 결과 선택
SearchPalette->>App: 세션과 메시지 UUID 전달
App->>ConversationView: highlightRef와 함께 대화 열기
ConversationView->>ConversationView: 대상 메시지 스크롤 및 강조
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
.github/workflows/ci.yml (2)
15-15: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win체크아웃에서 자격 증명 유지를 끄세요.
actions/checkout은 기본적으로GITHUB_TOKEN을.git/config에 남깁니다. 이 잡은 타입 검사·린트·테스트만 실행하므로 토큰이 필요 없습니다.persist-credentials: false를 설정해 이후 스텝에서 토큰이 노출될 여지를 없애세요.🔒️ 제안 수정
- - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + persist-credentials: false🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml at line 15, Update the actions/checkout@v4 step in the CI workflow to disable credential persistence by setting persist-credentials to false, while leaving the existing checkout behavior unchanged.Source: Linters/SAST tools
3-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value동시 실행 제어를 추가하는 것을 고려하세요.
같은 브랜치에 연속으로 푸시하면 이전 실행이 계속 돌아 러너 시간을 씁니다.
concurrency그룹을 두면 오래된 실행을 취소합니다.♻️ 제안 수정
permissions: contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 3 - 6, Configure a concurrency group for the workflow near the existing on triggers, using the workflow or branch context to group runs and cancel in-progress runs when a newer run starts. Preserve the current pull_request and main push triggers.src/main/lib/searchIndex.ts (3)
262-267: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift질의마다 코퍼스 전체를 동기로 훑습니다. 큰 인덱스에서 메인 프로세스가 멈춥니다.
searchSessions는await없이 모든 문서의 모든 메시지에 대해indexOf를 돌립니다. 렌더러가 200 ms 디바운스로 질의를 보내므로, 타이핑 중에도 이 루프가 반복 실행됩니다.MAX_HITS는 결과 수만 제한하고 스캔 비용은 줄이지 않습니다. 코퍼스가 수백 MB면 질의 한 번이 메인 프로세스를 수백 ms 동안 붙잡고, 그동안 모든 IPC 응답이 지연됩니다.완화 방법입니다.
- 문서 루프 중간에 주기적으로 이벤트 루프에 양보한다.
- 문서를
updatedAt내림차순으로 미리 정렬해 두고,MAX_HITS에 도달하면 스캔을 중단한다. 지금은 전체를 스캔한 뒤 정렬하므로 조기 종료가 불가능하다.- 인덱싱과 검색을
utilityProcess로 옮긴다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/lib/searchIndex.ts` around lines 262 - 267, Update searchSessions around the documents.values() loop to avoid scanning the entire corpus synchronously: iterate documents pre-sorted by updatedAt descending, periodically yield to the event loop during matching, and stop once MAX_HITS is reached. Preserve the existing matchDocument filtering and result ordering while ensuring indexing/search work is isolated in a utility process if that architecture is already available.
221-241: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value실패가 반복될 때 재시도 간격을 늘리는 것을 고려하세요.
인덱싱이 실패하면
lastReconcileAt이 갱신되므로, 이후 검색마다 5초 간격으로 전체 정합을 다시 시도합니다. 루트 디렉터리 권한 문제처럼 원인이 지속되면 매 5초마다 전체 트리 스캔과 오류 로그가 반복됩니다.failed가 이어질 때 지수 백오프를 적용하면 부하와 로그 소음을 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/lib/searchIndex.ts` around lines 221 - 241, Update ensureIndex and the reconciliation scheduling logic to apply exponential backoff after repeated indexing failures: increase the retry delay while failed persists, cap it at a reasonable maximum, and reset it after a successful load/reconcile. Preserve the existing failed-state signaling and progress emission while preventing full scans and error logs from recurring every five seconds.
78-113: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff대용량 인덱스에서는 줄 단위 스트리밍을 고려하세요.
readFile은 인덱스 파일 전체를 한 문자열로 올립니다. 이후documents와lower사본이 추가로 쌓입니다. 세션이 많은 사용자는 시작 시 메모리 피크가 커집니다.readline인터페이스로 줄 단위로 읽으면 피크를 낮출 수 있습니다. 현재 규모에서 문제가 없다면 그대로 두어도 됩니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/lib/searchIndex.ts` around lines 78 - 113, 대용량 인덱스 로딩 시 전체 파일을 메모리에 읽는 loadFromDisk의 readFile 사용을 줄 단위 스트리밍으로 변경하세요. readline 또는 기존 프로젝트의 동등한 스트리밍 API로 첫 줄의 버전을 검증한 뒤 나머지 줄을 순차 처리하고, 잘못된 JSON·누락 파일·유효하지 않은 문서에 대한 현재 동작은 유지하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.en.md`:
- Around line 32-33: Update the shortcut documentation to show platform-specific
modifiers: in README.en.md lines 32-33 and 86-87, replace ⌘F and ⌘⇧F with
⌘/Ctrl+F and ⌘/Ctrl+Shift+F; in README.md lines 32 and 85-86, apply the same
replacements in the feature text and usage table.
In `@src/renderer/src/App.tsx`:
- Around line 161-184: Update the search useEffect to increment
searchRequestRef.current before the early returns, invalidating any in-flight
request when searchOpen closes or searchQuery becomes empty. In those
empty/closed states, reset searchFailed and clear searchResults; preserve the
existing debounced search behavior and stale-response checks for non-empty
queries.
- Around line 247-265: Update the session-selection flow centered on
selectSession and openHit to track a monotonically increasing selection request
ID. Increment it when openHit starts and when the sidebar directly invokes
selectSession, then verify the ID after listSessions and loadConversation
complete before applying setSelected, setConversation, or related UI updates, so
only the latest selection can affect the screen.
In `@src/renderer/src/components/SearchView.tsx`:
- Around line 40-58: Update the status logic after the results.degraded check so
any degraded SearchResults displays a partial or failure status even when
results.hits contains items, while preserving the result list. Use the existing
search.partial translation key if available, or add it if necessary, and keep
the normal search.summary path only for complete results.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Line 15: Update the actions/checkout@v4 step in the CI workflow to disable
credential persistence by setting persist-credentials to false, while leaving
the existing checkout behavior unchanged.
- Around line 3-6: Configure a concurrency group for the workflow near the
existing on triggers, using the workflow or branch context to group runs and
cancel in-progress runs when a newer run starts. Preserve the current
pull_request and main push triggers.
In `@src/main/lib/searchIndex.ts`:
- Around line 262-267: Update searchSessions around the documents.values() loop
to avoid scanning the entire corpus synchronously: iterate documents pre-sorted
by updatedAt descending, periodically yield to the event loop during matching,
and stop once MAX_HITS is reached. Preserve the existing matchDocument filtering
and result ordering while ensuring indexing/search work is isolated in a utility
process if that architecture is already available.
- Around line 221-241: Update ensureIndex and the reconciliation scheduling
logic to apply exponential backoff after repeated indexing failures: increase
the retry delay while failed persists, cap it at a reasonable maximum, and reset
it after a successful load/reconcile. Preserve the existing failed-state
signaling and progress emission while preventing full scans and error logs from
recurring every five seconds.
- Around line 78-113: 대용량 인덱스 로딩 시 전체 파일을 메모리에 읽는 loadFromDisk의 readFile 사용을 줄
단위 스트리밍으로 변경하세요. readline 또는 기존 프로젝트의 동등한 스트리밍 API로 첫 줄의 버전을 검증한 뒤 나머지 줄을 순차
처리하고, 잘못된 JSON·누락 파일·유효하지 않은 문서에 대한 현재 동작은 유지하세요.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b48b859d-1ea8-41da-833a-3320f4d89cc7
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
.github/workflows/ci.ymlREADME.en.mdREADME.mdpackage.jsonsrc/main/index.tssrc/main/lib/searchExtract.test.tssrc/main/lib/searchExtract.tssrc/main/lib/searchIndex.tssrc/main/lib/searchMatch.test.tssrc/main/lib/searchMatch.tssrc/preload/index.tssrc/renderer/src/App.tsxsrc/renderer/src/components/ConversationView.tsxsrc/renderer/src/components/MessageItems.tsxsrc/renderer/src/components/SearchView.tsxsrc/renderer/src/components/Sidebar.tsxsrc/renderer/src/i18n.tssrc/renderer/src/styles/app.csssrc/shared/types.tsvitest.config.ts
전용 화면 대신 창 전체 폭 타이틀바 중앙의 검색창에서 열고, 결과는 그 아래 드롭다운 팔레트로 떨어뜨린다. 뒤 대화를 가리지 않고 ↑↓·↵로 훑을 수 있다. 단축키는 ⌘K. 사이드바가 자기 타이틀바와 접기 버튼을 들고 있던 것도 상단 바로 합친다. 접힌 상태에서 펼치기 버튼을 화면마다 따로 얹던 처리가 사라진다. 사이드바 ⌘F는 폴더(프로젝트·워크트리) 이름도 매칭한다 — 이름이 걸리면 그 안의 세션을 통째로 보여준다.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/App.tsx (1)
198-202: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win사이드바 질의를 우선 적용하세요.
이전 팔레트 질의가 남아 있으면
previous || query가 이전 질의를 유지합니다. 그 상태에서 사이드바에 새 질의를 입력하고 콘텐츠 검색을 열면 새 질의가 전달되지 않습니다.사이드바 질의가 비어 있지 않으면 이를 우선 사용하세요.
수정 예시
- setSearchQuery((previous) => previous || query) + setSearchQuery((previous) => (query.trim() ? query : previous))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/src/App.tsx` around lines 198 - 202, Update openContentSearch so the current sidebar query takes precedence over the previous search state when it is non-empty. Replace the previous-first fallback in setSearchQuery while preserving the existing behavior of retaining the prior query only when the sidebar query is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/src/components/SearchPalette.tsx`:
- Around line 97-175: Update the SearchPalette dialog container to declare
aria-modal="true" and trap Tab/Shift+Tab focus within .palette, keeping focus on
the first or last focusable control when cycling. When the palette closes
through onClose, restore focus to the control that opened it, using the existing
inputRef and lifecycle/close handling without changing result navigation.
In `@src/renderer/src/components/TopBar.tsx`:
- Around line 42-56: Update the search shortcut hint in the TopBar search button
to represent both supported platforms, replacing the macOS-only “⌘K” display
with a platform-aware key or a combined “⌘/Ctrl K” label consistent with App.tsx
handling.
---
Outside diff comments:
In `@src/renderer/src/App.tsx`:
- Around line 198-202: Update openContentSearch so the current sidebar query
takes precedence over the previous search state when it is non-empty. Replace
the previous-first fallback in setSearchQuery while preserving the existing
behavior of retaining the prior query only when the sidebar query is empty.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 87689718-16ed-48fc-a876-72cdc25cf190
📒 Files selected for processing (10)
README.en.mdREADME.mdsrc/renderer/src/App.tsxsrc/renderer/src/components/ConversationView.tsxsrc/renderer/src/components/SearchPalette.tsxsrc/renderer/src/components/Sidebar.tsxsrc/renderer/src/components/SidebarExpand.tsxsrc/renderer/src/components/TopBar.tsxsrc/renderer/src/i18n.tssrc/renderer/src/styles/app.css
💤 Files with no reviewable changes (1)
- src/renderer/src/components/SidebarExpand.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- src/renderer/src/i18n.ts
- README.md
- README.en.md
세션을 빠르게 연달아 고르면 대화 로드가 역순으로 끝나 헤더와 다른 세션의 본문이 그려질 수 있었다. 선택마다 번호를 매겨 마지막 것만 반영한다. 질의를 지워도 이미 떠난 검색 요청이 무효화되지 않아, 그 요청이 실패하면 빈 질의 화면에 '검색 실패'가 떴다. 번호를 이른 시점에 올려 무효화하고, 질의가 비면 실패보다 안내를 먼저 보여준다. 팔레트에서 Tab을 누르면 포커스가 뒤 화면 컨트롤로 빠져나가 ↑↓·↵가 더는 듣지 않았다. Tab을 막아 입력창에 붙잡아 두고, 닫을 때는 팔레트를 연 컨트롤로 되돌린다. aria-modal도 함께 붙인다. 인덱싱이 실패해 인덱스가 불완전한데도 결과가 있으면 정상 요약을 보여줬다. 이제 불완전함을 알린다. 단축키 표기는 플랫폼을 따른다 — Windows·Linux에서는 Ctrl로 보인다.
|
리뷰 6건 전부 코드와 대조해 반영했습니다 ( 🟠 마지막으로 선택한 세션만 화면에 반영 — 유효한 지적이었습니다. 🟠 팔레트 내부에 포커스 유지 — 🟡 빈 질의에서도 이전 요청 무효화 — 유효했습니다. 요청 번호를 early return 앞으로 옮겼고, 재시도 시작 시점에 실패 표시를 걷습니다. 더해서 상태 문구 순서를 바꿔 질의가 비면 직전 실패보다 안내가 먼저 나오게 했습니다. 🟡 불완전한 색인 결과를 완전한 결과로 표시하지 않기 — 유효했습니다. 🟡 플랫폼 공통 단축키 표기 (상단 바 / README) — 유효했습니다. 다만 검증: |
|
@coderabbitai review |
✅ Action performedReview finished.
|
기억나는 단어 하나로 모든 프로젝트의 대화 본문을 찾고, 고르면 그 메시지로 바로 이동한다.
기존
⌘F는 세션 제목과 첫 메시지만 훑는다. 정작 찾고 싶은 건 "그때 그 얘기"인데 제목에는 남아 있지 않다.동작
⌘K(Windows·Linux는Ctrl+K)로 그 아래 팔레트가 내려온다. 뒤의 대화를 덮지 않아 보던 화면을 유지한 채 검색한다.↑↓로 훑고↵로 연다. 열면 해당 세션의 그 메시지로 스크롤·강조되고, 사이드바에서도 그 프로젝트(워크트리면 그룹 루트까지)가 펼쳐진다.Esc로 닫는다.⌘F사이드바 검색은 그대로 두되, 세션 제목·첫 메시지에 더해 폴더(프로젝트·워크트리) 이름도 매칭한다. 폴더 이름이 걸리면 그 안의 세션을 통째로 보여준다.곁들여 사이드바가 들고 있던 자체 타이틀바(앱 이름·접기 버튼)를 상단 바로 합쳤다. 사이드바를 접었을 때 화면마다 펼치기 버튼을 따로 얹던 처리가 사라지고, 접힘 상태에서도 상단 바 토글 하나로 항상 되돌릴 수 있다.
인덱스
main 프로세스가
userData/search-index.jsonl에 대화 텍스트만 담은 인덱스를 유지한다.parseConversation출력에서 한다. 엔트리 해석 규칙을 복제하지 않기 위해서이기도 하고, 파서가 연속 assistant 엔트리를 한 아이템으로 병합하기 때문에 원본 엔트리 uuid를 쓰면 화면에 없는 아이템을 가리키게 되기 때문이다.(마지막 엔트리 시각, 파일 크기)가 바뀐 세션만 다시 읽는다. 세션 JSONL이 append-only라 성립하는 판정이다.revision이 올라 렌더러가 같은 질의를 자동으로 다시 던진다. 검색 응답이 디스크 스캔에 묶이지 않게 하려는 것이다.n/N)을 보여주고, 끝나는 순간 결과가 저절로 채워진다.런타임 의존성은 추가하지 않았다.
검증
이 저장소에 테스트 러너가 없어 vitest를 devDependency로 들였다. Electron에 의존하지 않는 순수 로직(추출 규칙, 매칭·스니펫 절단, 신선도 판정, 대소문자 접기)에 27개 테스트를 붙였다.
테스트를 들여왔으니 PR에서 실제로 돌도록
ci워크플로도 함께 추가했다 — 타입 검사·린트·테스트. 검증에는 electron 바이너리가 필요 없어 설치에서 건너뛴다.실제 앱을 띄워 콜드 스타트(인덱스 없음) → 인덱싱 진행률 표시 → 완료 시 자동 재질의 →
⌘K→ 화살표 이동 →↵로 메시지 이동·강조 →Esc닫기와 포커스 복원까지 확인했다.리뷰에서 잡아 고친 것
구현 후 리뷰에서 나온 것 중 실제로 조용히 틀리던 것들이다.
toLowerCase()로 길어지는 코드포인트(U+0130)가 섞이면 이후 인덱스가 밀려 엉뚱한 글자가 하이라이트된다. 길이를 보존하는 접기로 질의와 본문에 같은 규칙을 쓴다.↑↓·↵가 듣지 않는다. Tab을 막아 입력창에 붙잡아 두고, 닫을 때 연 컨트롤로 되돌린다.animation: none이 이를 지웠다.⌘F가 먹지 않았다. 이건 기존 코드의 문제인데 같이 고쳤다.알려진 한계
!로 직접 실행한 셸 명령과 컨텍스트 요약은 화면에는 보이지만 검색에 잡히지 않는다. "대화 텍스트만" 범위를 따른 결과인데, 넓힐 여지는 있다.CHV_DATA_DIR로 데이터 루트를 바꿔도 인덱스 파일은 하나를 공유해서, 데모 데이터와 실제 데이터를 오가면 매번 전체 재구축한다. 자기 치유되므로 그대로 뒀다.🤖 Generated with Claude Code
Summary by CodeRabbit
새로운 기능
⌘K(Windows·Linux:Ctrl+K)로 모든 프로젝트의 대화 내용을 검색할 수 있습니다.버그 수정
문서
품질 개선