Skip to content

feat: import ChatGPT/Claude/Gemini web chats into AI Vault#8744

Open
chang-in wants to merge 88 commits into
stablyai:mainfrom
chang-in:feat/web-chat-import
Open

feat: import ChatGPT/Claude/Gemini web chats into AI Vault#8744
chang-in wants to merge 88 commits into
stablyai:mainfrom
chang-in:feat/web-chat-import

Conversation

@chang-in

Copy link
Copy Markdown

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

  • Browser extension (extensions/chat-import/): MV3 extension with per-service adapters (ChatGPT/Claude/Gemini), popup with sync controls, and a deterministic extension ID (fixed key). Verbatim upstream normalizers live under lib/.
  • Native messaging host (src/cli/chat-import-host/): orca chat-import-host — length-prefixed framing, request whitelist/validation, chunked STORE_BLOB protocol, and a cross-platform installer that registers the host per browser.
  • Storage (src/main/chat-import/): chats.db schema (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).
  • AI Vault display: web conversations are scanned into read-only sessions, grouped by folder via per-agent import directories (configurable in Settings).
  • Read-only viewer (src/main/native-chat/, native-chat components): renders an imported transcript, including attachment chips (image/file) that open the stored blob with the OS default handler.
  • Resume: convert an imported conversation into a Claude session (claude --resume) or seed it into Codex, launched in a new tab.
  • Web extension setup pane (Settings): detects installed browsers, one-click installs the native messaging host, and guides loading the unpacked extension; popup shows recent syncs.
  • Packaging: extension folder shipped as an extra resource; CLI-host out/main/chat-import unpacked from the asar; the host launcher pins ORCA_USER_DATA_PATH so the dev and packaged apps read/write the same store.
  • i18n: strings added to en and translated for ko / es / ja / zh; catalog parity verified.

Testing

  • Unit tests across the host protocol, blob store, schema/store, storage GC, transcript reader, attachment IPC, and the renderer components.
  • verify:localization-catalog passes (parity across locales).
  • Verified against a real packaged build: the packaged CLI host launches and responds (PINGPONG), with the extension folder and unpacked chat-import present in Orca.app.
  • Manually smoke-tested the capture loop end-to-end (ChatGPT/Claude/Gemini) into a real chats.db.

Notes

  • Large branch spanning several milestones; happy to split or squash if preferred.
  • The attachment click-to-open path can only be exercised with a logged-in browser + connected host, so it is covered by manual smoke rather than unit tests.

chang-in added 30 commits July 13, 2026 14:01
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을 추가해 대응한다.
chang-in added 24 commits July 14, 2026 16:51
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로 방어하고 실패 토스트를 띄운다.
@chang-in

Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@chang-in

Copy link
Copy Markdown
Author

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:

  1. feat(chat-import): storage, native messaging host, and read-only AI Vault sessions (1/4) #8794 — chat-import storage, native messaging host, read-only AI Vault sessions (+1,977)
  2. browser extension + settings pane (+2,111)
  3. resume web chats as Claude/Codex sessions (+3,053)
  4. browser integration UI + attachments (+3,888)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants