A Chrome extension that turns ChatGPT conversations into a spatial map. Branching a chat normally scatters it across unrelated sidebar entries; GptTree reconstructs the tree and draws it as a git-graph style canvas, docked beside the live page.
It augments the visible chatgpt.com page from a closed Shadow Root. It stores metadata only — never message bodies, assistant text, or summaries.
The live chat stays on the left; the dock on the right is the map. Each coloured lane is one
conversation, FORK POINT marks the message a branch left from, and HEAD is the one you are
reading. Above, a single question about how a CPU works has fanned into five branches — cache, ALU,
clock, registers — each still hanging off the exact message that spawned it, instead of five
unrelated rows in ChatGPT's sidebar.
No store listing; load it unpacked.
pnpm install
pnpm build # -> .output/chrome-mv3Then open chrome://extensions, enable Developer mode, and Load unpacked from .output/chrome-mv3.
Requires Node 22 and pnpm 10.14.0.
pnpm dev # WXT dev server
pnpm build # -> .output/chrome-mv3 (load unpacked from here)
pnpm test # vitest run (unit)
pnpm test -- src/content/spine.test.ts # single file
pnpm test -- -t 'name of the test' # single test by name
pnpm typecheck # wxt prepare && tsc --noEmit
pnpm test:e2e:install # pinned Playwright Chromium, required once
pnpm test:e2e # Playwright; requires a fresh `pnpm build` first
pnpm verify:release # test + typecheck + build + e2e + artifact verifierA WXT + React Chrome MV3 extension. Chrome-only: it runs only on https://chatgpt.com/*, drives the visible site UI, and never calls private APIs or submits content without an explicit user action.
Two processes, one boundary:
- Background coordinator (
entrypoints/background.ts→src/background/start.ts→src/background/runtime.ts) is the sole reader/writer of graph storage.GraphRepositoryserializes all mutations through one promise queue, revalidates every command payload, and broadcasts the new snapshot to everychatgpt.comtab. - Content UI (
entrypoints/content.tsx→src/content/Dock.tsx) mounts a closed WXT Shadow Root and talks to the background only throughRuntimeCommand(src/shared/runtime.ts). It never touchesbrowser.storage.
Persisting something new means: a new RuntimeCommand union member → validation and handling in src/background/runtime.ts → a schema change in src/shared/schema.ts (bump GRAPH_SNAPSHOT_VERSION, keep older versions loadable) → the content call site.
src/shared/schema.ts is the trust boundary. Everything read from storage or arriving from a content script is re-validated there with exact-shape checks (exactObject), not casts. Anything malformed or version-incompatible loads as the current empty snapshot rather than throwing. The graph is versioned metadata held under a single key, gpttree.graph-snapshot (GRAPH_STORAGE_KEY), and storage models must never gain fields for message bodies, turn text, prompts, or summaries.
The adapter is a capability detector (src/content/chatgpt-adapter.ts). Every operation returns {status:'ready', …} or {status:'unavailable', reason:'ChatGPT UI changed'}. It only recognizes the canonical https://chatgpt.com/c/<id> URL — any query, hash, port, or credential makes it unavailable. It never guesses a selector or acts from an unrecognized state; it fails closed. getDiagnostics() reports selector match counts for debugging DOM drift.
Turn identity drives the tree. Turns are {key: 'turn:<message-id>', ordinal, role, fingerprint}, where the fingerprint is a SHA-256 of normalized visible text. sharedAssistantForkKey in src/content/graph-parent.ts decides parentage: a node is a child if the parent's turns are an exact key+ordinal+role+fingerprint prefix of the child's and the fork point is an assistant turn. mergeTurns exists because ChatGPT virtualizes the thread — an extraction is only a window, so stored turns merge with incoming ones and are never deleted by a scroll.
Layout is derived, never stored. src/content/spine.ts computes the lane layout from the tree on every render; GraphCanvas.tsx draws it.
GptTree persists conversation IDs and URLs, short labels, relationships, job checkpoints, and a bounded preview of the user's own prompt (≤120 chars). It never persists assistant text, full message bodies, or summaries.
pnpm verify:release enforces that the built manifest carries exactly the storage permission and exactly the https://chatgpt.com/* host permission, that the production Shadow Root stays closed, and that no test canaries ship. The e2e suite asserts metadata-only storage.
- Unit tests run in the
nodeenvironment by default; React tests opt in with a// @vitest-environment jsdomdocblock on line 1. Tests live beside their source as*.test.ts(x). - E2E runs the real unpacked build against a local HTTPS replay server serving the genuine
https://chatgpt.comorigin (tests/e2e/support/), so build before running e2e or you test a stale artifact. Specs run serially, and the harness refuses any Chromium revision other than the pinned one. - Behavior changes follow red-green-refactor: write the focused test first, run it to a meaningful failure, implement the smallest change, then run the focused suite followed by full verification.
- CONTEXT.md — domain language and product guardrails
