feat: import ChatGPT/Claude/Gemini web chats into AI Vault#8744
feat: import ChatGPT/Claude/Gemini web chats into AI Vault#8744chang-in wants to merge 88 commits into
Conversation
Antigravity CLI sessions never appeared in the AI Vault "Agent session history" panel because `antigravity` was not a registered scanned agent. Its transcripts live in per-conversation SQLite DBs at ~/.gemini/antigravity-cli/conversations/<id>.db and store turns as protobuf blobs, unlike the JSONL/JSON sources Orca already handles. - Add a minimal, dependency-free protobuf wire-format reader that extracts only the fields the scanner needs and tolerates unknown or malformed data. - Parse each conversation DB into an AiVaultSession: title from the first user turn, user/assistant previews, cwd decoded from the workspace file:// URI, created/updated timestamps, and an `agy --conversation <id>` resume command. - Discover conversation DBs on local and WSL home roots. SQLite binary blobs are local-only, matching the existing OpenCode SQLite source. - Register `antigravity` in AI_VAULT_AGENTS with its label and resume invocation. - Bundle the official Antigravity favicon as an icon asset so sessions show the logo offline and over SSH, like Claude and Codex, instead of relying on the remote favicon fetch.
- TuiAgent 하위집합을 TUI_AI_VAULT_AGENTS로 분리하고 WEB_CHAT_AGENTS(chatgpt/claude-web/gemini-web)를 합쳐 AiVaultAgent를 구성 - isWebChatAgent 타입가드, 웹 agent 라벨, AiVaultSession.readOnly 필드 추가 - buildAiVaultResumeCommand는 웹 agent에 대해 빈 문자열(읽기 전용)을 조기 반환하고, defaultAiVaultResumeCommandBase/buildAgentResumeInvocation은 Exclude<AiVaultAgent, WebChatAgent>로 좁혀 exhaustive switch 유지
Task 3에서 필수 필드로 추가된 AiVaultSession.readOnly를 accumulator 파이프라인(createAccumulator/finalizeSession)과 런타임 스캐너 zod 스키마, 그리고 나머지 모든 세션 생성 지점(테스트 픽스처)에 전파했다. 전부 로컬/일반 세션이라 readOnly: false로 채웠고, 런타임 스캐너는 구버전 원격 호스트 하위호환을 위해 기존 필드들과 동일하게 z.boolean().default(false) 패턴을 따랐다.
- listWebChatCandidates가 chat-import SQLite DB의 대화마다 agent(chatgpt/ claude-web/gemini-web)를 소스 컬럼으로 도출해 후보를 만든다. 대화별로 agent가 달라 discovery(단일 agent) 방식 대신 session-scanner의 candidates 배열에 직접 concat한다. - agent-parser가 <dbPath>#<source>/<externalId> 합성 경로를 파싱해 parseWebChatSqliteSession으로 라우팅하고, parse-cache는 웹 후보를 재파싱 전용(null) 그룹에 추가해 exhaustive switch 2건을 해소한다. - AiVaultScanOptions.webchatDbPath로 테스트가 임시 DB를 주입할 수 있게 하고, 같은 옵션이 없을 때의 기본값 누락으로 다른 ai-vault 테스트가 실제 사용자 chats.db를 건드리지 않도록 테스트 픽스처에도 반영한다.
옵션 객체를 직접 만드는 3개 테스트가 webchatDbPath를 빠뜨려 실제 chat-import chats.db로 폴백, 임포트된 웹 대화가 정확일치 단언에 샐 수 있던 격리 구멍을 막음.
AgentIcon이 TuiAgent 카탈로그를 건드리지 않고 웹 채팅 agent(chatgpt/claude-web/gemini-web)를 파비콘으로 렌더하도록 확장하고, launchAiVaultSessionInNewTab의 agent 타입을 웹 제외로 좁혀 CLI 이어하기 경로에서 웹 세션이 도달하지 않도록 두 caller(handleResume, 드롭 핸들러)에 가드를 추가했다. aiVaultSessionRowResumeGating은 readOnly 세션의 이어하기·복사 액션을 단일 게이트에서 숨긴다.
웹 대화 import 세션은 readOnly:true인데 콘텐츠가 있어 이어하기 버튼이 그려졌다. 클릭해도 handleResume의 isWebChatAgent 가드가 막아 죽은 버튼이 노출되던 문제를 canResume 게이트로 해결. 대화 미리보기는 hasResumableContent 그대로 유지해 트랜스크립트는 계속 보이게 했다.
chats.db에 저장된 웹 대화가 scanAiVaultSessions를 통해 읽기전용 AiVaultSession으로 노출되는지 end-to-end로 검증한다.
델타 동기화를 위해 지정된 source의 외부 ID 목록을 조회하는 listIngestedExternalIds 함수를 추가했다. CLI 빌드가 chat-import 모듈을 참조할 수 있도록 tsconfig.cli.json의 include에 필요한 5개 파일을 등록했다.
오버사이즈 프레임 에러 이후 자연히 오는 end 이벤트가 finish()를 다시 호출해 이미 닫힌 db에 close()를 호출, node:sqlite가 ERR_INVALID_STATE로 호스트를 크래시시켰다. finish()에 1회 실행 가드와 리스너 해제를 추가하고, 프레이밍 에러는 ERROR 프레임 전송 후 reject 대신 정상 종료(resolve)하도록 변경했다.
archive web-extension의 normalize.js·util.js를 verbatim 이식하고, node:test 기반 정규화 테스트 9개를 vitest로 변환한다. extensions/** 디렉터리를 vitest include에 추가하고, 웹익스텐션 전역(chrome, fetch 등)과 verbatim 이식 코드의 스타일 규칙 충돌을 흡수하는 oxlint override를 추가한다.
앞선 커밋에서 lint-staged의 oxfmt --write가 verbatim 이식 대상인 normalize.js·util.js를 프로젝트 스타일(싱글쿼트·세미콜론 제거)로 재포맷해버려 아카이브 원본과 바이트 단위로 달라졌다. .oxfmtrc.json에 extensions/chat-import/lib/**를 ignorePatterns로 추가해 verbatim 상태를 복원한다. 이 두 파일만 스테이징되면 oxfmt 대상이 전부 제외돼 미매치 에러로 커밋이 막히므로, lint-staged의 oxfmt 작업에 --no-error-on-unmatched-pattern을 추가해 대응한다.
sharp/resvg/jimp가 설치돼 있지 않아 macOS sips로 아이콘을 16/32/48/128 사이즈로 생성하는 일회성 개발 스크립트를 추가하고, manifest.json에 icons/action.default_icon을 연결했다.
background가 PUSH 성공 시 recentSyncs 링버퍼(cap 30)에 기록하고, 팝업이 최근 10개를 날짜순으로 렌더링한다. service worker/popup을 ESM 모듈로 전환해 순수 함수 recent-syncs.js를 양쪽에서 공유한다.
electron-builder commonExtraResources에 extensions/chat-import를 copy하는 항목이 없어 프로덕션 빌드에서 확장 폴더가 존재하지 않던 문제를 고치고, afterPack에 존재 검증 가드를 추가했다. readLastSynced()가 DB open/쿼리 실패 시(SQLITE_BUSY, 손상 등) throw하던 것을 try/catch로 감싸 all-null로 degrade하도록 바꾸고, 렌더러 mount effect에도 .catch를 추가해 IPC reject가 설정 패널을 로딩 스피너에 영구히 멈추게 하지 않도록 했다.
- recentSyncs dedup을 (source, title) 대신 (source, externalId)로 변경해 제목이 같은 서로 다른 대화가 최근 목록에서 합쳐지는 문제 수정 - background.js의 recentSyncs storage get→set을 체인으로 직렬화해 대량/동시 동기화 시 기록 유실 레이스 방지 - WebChatBrowserLinkSection의 browserBadge가 detected를 hostInstalled보다 먼저 확인하도록 순서 변경(삭제된 브라우저에 "연동됨" 오표시 방지) - 아이콘 생성 스크립트(generate-icons.mjs)에 macOS 가드 추가 - 확장 ID는 언패킹/자체배포 전제라는 CWS 게시 caveat 주석 추가
파란 그라디언트 점(.dot)을 Orca 아이콘(icon-48.png)으로 교체. '앱 실행 필요'→'Orca 연동 필요'/'연동 필요', 실제 필요한 동작(설정 → 브라우저 연동 설치)을 가리키게 문구·푸터 정정.
extension이 보내는 첨부 바이트를 청크 단위로 받아 재조립·sha256·blobstore 저장하는 STORE_BLOB 핸들러를 추가하고, parseConversation이 메시지별 attachments를 화이트리스트 검증 후 보존하도록 확장했다. 업로드 상태는 연결 단위(runChatImportHost)에서 소유해 blobCtx로 주입한다.
STORE_BLOB 핸들러에서 3가지 결함을 수정했다.
1. seq/total을 정수 범위로 검증하지 않아 NaN 등 비정상 seq가 배열의
비인덱스 프로퍼티로 들어가면서 chunks.reduce 합산에서 누락되고,
25MB 캡이 무력화되며 업로드가 영구히 완결·정리되지 않는 문제 (누수)
2. total을 메시지마다 다시 읽어, 더 작은 total을 가진 나중 청크가
조기에 완결시켜 잘린 blob이 해시와 함께 저장되는 문제
3. Buffer.concat 이후에 uploads.delete를 호출해, 청크 사이 구멍(hole)
때문에 concat이 던지면 catch로 ERROR는 반환되지만 정리는 건너뛰어
부분 업로드가 누수되는 문제
업로드 상태를 Buffer[]에서 { chunks, total, received, size } 형태로
바꿔 total을 최초 청크에서 고정하고, received 카운트로 구멍 없는
완결만 허용하도록 재작성했다. TDD로 실패하는 테스트를 먼저 추가한 뒤
수정했다.
v2에서 NativeChatApi에 추가됐으나 웹 preload 미러가 빠져 tc.web 타입체크 실패했다. 웹은 로컬 파일 쓰기가 불가하므로 error 반환 스텁으로 채움.
- shell.openPath는 실패해도 reject하지 않고 에러 메시지 문자열을 resolve한다.
기존 코드는 반환값을 무시해 실패해도 항상 {ok:true}를 반환했다.
이제 반환된 에러 문자열을 검사해 비어있지 않으면 {ok:false,error}를 반환한다.
- sanitizeAttachmentFileName이 fileName의 기존 확장자를 그대로 보존하던 문제를 수정.
web-chat 메타데이터(fileName/mime)는 신뢰할 수 없는 입력이라
fileName='invoice.exe', mime='image/png' 같은 첨부가 그대로 .exe로 저장되어
shell.openPath로 실행될 수 있었다(실행파일 위장 취약점). 이제 출력 확장자는
항상 mime→ext allowlist에서만 가져오고, 알 수 없는 mime은 .bin으로 처리한다.
- 열기 로직을 openStoredAttachment로 분리해 electron 없이 단위 테스트 가능하게 함.
웹 클라이언트의 window.api는 fallback Proxy라 chatImportAttachment.open이 undefined를 resolve한다. result.ok 직접 접근이 unhandled rejection을 내 크래시 진단을 오염시켰다. result?.ok로 방어하고 실패 토스트를 띄운다.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
Splitting this up, per my note above — +10,842 across 186 files is a lot to ask of a reviewer. Part 1 is up: #8794 — storage + native messaging host + read-only AI Vault sessions (+1,977 / 64 files). Planned order. I can't push branches to this repo, so these can't be stacked on each other — each one opens once the previous merges and its diff shrinks to just its own slice:
Each cut point is self-contained: I re-ran lint / typecheck / the affected suites at part 1's exact commit, not just at this branch's tip. I'm keeping this PR open as the full-context reference — its tip is the identical commit to the end of part 4, so the final state is unchanged either way. Happy to close it if you'd rather not have the duplicate sitting in the queue. |
Summary
Adds end-to-end web chat import: capture ChatGPT / Claude.ai / Gemini conversations from the browser and surface them in Orca's AI Vault as read-only sessions, with the option to resume them in a local agent.
Pipeline: browser extension → native messaging host → SQLite (
chats.db) + content-addressed blob store → AI Vault session scanner → read-only native-chat viewer.What's included
extensions/chat-import/): MV3 extension with per-service adapters (ChatGPT/Claude/Gemini), popup with sync controls, and a deterministic extension ID (fixedkey). Verbatim upstream normalizers live underlib/.src/cli/chat-import-host/):orca chat-import-host— length-prefixed framing, request whitelist/validation, chunkedSTORE_BLOBprotocol, and a cross-platform installer that registers the host per browser.src/main/chat-import/):chats.dbschema (conversations / messages / attachments), a content-addressed blob store (sha256, atomic writes, traversal-guarded reads), and a startup GC that reclaims orphan blobs and stale temp files (with a grace window so an in-flight sync is never disturbed).src/main/native-chat/,native-chatcomponents): renders an imported transcript, including attachment chips (image/file) that open the stored blob with the OS default handler.claude --resume) or seed it into Codex, launched in a new tab.out/main/chat-importunpacked from the asar; the host launcher pinsORCA_USER_DATA_PATHso the dev and packaged apps read/write the same store.enand translated forko/es/ja/zh; catalog parity verified.Testing
verify:localization-catalogpasses (parity across locales).PING→PONG), with the extension folder and unpackedchat-importpresent inOrca.app.chats.db.Notes