Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Design System — Spool

## Product Context
- **What this is:** A local AI session library — an Electron macOS app that collects, organizes, and lets you revisit every Claude Code, Codex, and Gemini session you've ever had.
- **What this is:** A local AI session library — an Electron macOS app that collects, organizes, and lets you revisit every Claude Code, Codex, Gemini, Antigravity, and OpenCode session you've ever had.
- **Who it's for:** Developers who think with AI daily and have accumulated hundreds of sessions across multiple tools. The persona is overwhelmed by the archive itself, not only by re-explaining context.
- **Space/industry:** Developer productivity / local-first tooling. Peers: Raycast, Spotlight, Obsidian, DevonThink — but none of them treat AI sessions as first-class library items.
- **Project type:** macOS Electron app — sidebar + main pane shell, the shape of a library client.
Expand Down Expand Up @@ -89,6 +89,7 @@ Only currently-supported agent sources are listed. New sources arrive via the da
| Claude Code | `#C26A4E` | `#E89A7C` |
| Codex CLI | `#4A9670` | `#7CC9A2` |
| Gemini | `#5887D0` | `#8AB0E5` |
| Antigravity | `#6B5CA5` | `#9B8FD0` |
| OpenCode | `#8A6F3D` | `#C9A761` |

### Semantic States
Expand Down Expand Up @@ -282,5 +283,5 @@ In list contexts (Library Home, Project View, search results), trust the surroun
| 2026-05-08 | Warm-tuned status colors replace Tailwind defaults | `green-400 / amber-400 / red-400` were the only un-tuned colors in the system. Replaced with warm green `#6BAF6B`, warm amber `#E4A640`, terracotta `#C95A4F` so status reads as part of the same palette. |
| 2026-05-08 | Page title type added (20px) | Library Home and Settings need a real h1 — previous scale topped at sidebar wordmark 16px, leaving the home pane labelless. |
| 2026-05-08 | First-person rule softened from "all metadata" to "where it adds signal" | Literal "You discussed this · Mar 15" prefix on every row was repetitive once a user had hundreds of sessions. Library context already conveys ownership; reserve first-person for empty states, detail headers, and confirmations. |
| 2026-05-08 | Source badge list trimmed to active agents only | Twitter / GitHub / YouTube / ChatGPT were carryover from the connector era. Spool ships only Claude Code / Codex CLI / Gemini today; new sources arrive via the daemon and get a row when shipped, not preemptively. |
| 2026-05-08 | Source badge list trimmed to active agents only | Twitter / GitHub / YouTube / ChatGPT were carryover from the connector era. Spool ships only Claude Code / Codex CLI / Gemini / Antigravity today; new sources arrive via the daemon and get a row when shipped, not preemptively. |
| 2026-05-26 | Retire the "icons only 12/14/16/20, stroke 1.5" rule — describe reality instead | The 2026-05-08 lock never held: the renderer actually uses 11/12/13/14 (13 & 14 dominant) at strokes 1.6–1.8 (1.6 most common, 1.5 a minority). The aspirational whitelist was actively misleading new work into picking sizes that clashed with adjacent icons. Replaced with a role-based working set + a "match adjacent icons" rule. Sizes/strokes are now chosen for local consistency, not global uniformity. |
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Your local AI session library.
<img src="docs/spool-v1.png" alt="Spool" width="720">
</p>

Spool collects every Claude Code, Codex CLI, Gemini CLI, and OpenCode session you've ever had into a sidebar of projects you can browse, pin, and revisit. Press ⌘K to search across the whole archive.
Spool collects every Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, and OpenCode session you've ever had into a sidebar of projects you can browse, pin, and revisit. Press ⌘K to search across the whole archive.

> **Early stage.** Spool is under active development — expect rough edges. Feedback, bug reports, and ideas are very welcome via [Issues](https://github.com/paperboytm/spool/issues) or [Discord](https://discord.gg/aqeDxQUs5E).

Expand All @@ -29,7 +29,7 @@ pnpm build
Spool turns the pile of AI sessions sitting on your disk into a browsable library.

- **Library shell** — sidebar of projects (derived from working-dir paths across agents) and a main pane that shows recent + pinned sessions for whatever you've selected
- **Session indexing** — watches Claude/Codex/Gemini session dirs and OpenCode's SQLite database in real time, including profile-based paths like `~/.claude-profiles/*/projects`, `~/.codex-profiles/*/sessions`, Gemini's project temp dirs under `~/.gemini/tmp/*/chats`, and `~/.local/share/opencode/opencode.db`
- **Session indexing** — watches Claude/Codex/Gemini/Antigravity session dirs and OpenCode's SQLite database in real time, including profile-based paths like `~/.claude-profiles/*/projects`, `~/.codex-profiles/*/sessions`, Gemini/Antigravity project temp dirs under `~/.gemini/tmp/*/chats` and `~/.gemini/antigravity-cli/brain/`, and `~/.local/share/opencode/opencode.db`
- **Pin** — keep important sessions on top of their project and on the global Library Home
- **⌘K search** — fast full-text search scoped to All or the current project; AI mode synthesizes answers across fragments
- **Agent search** — a `/spool` skill inside Claude Code (and any ACP agent) feeds matching fragments back into your conversation
Expand Down
6 changes: 3 additions & 3 deletions docs/spool-positioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

> **The missing search engine for your own AI sessions.**

Search your `[Claude Code sessions · Codex history · Gemini chats]` — locally.
Search your `[Claude Code sessions · Codex history · Gemini chats · Antigravity transcripts]` — locally.

---

## Landing Page

### Your coding agent is already the best search engine you have.

Spool lets Claude Code, Codex, Gemini CLI, and any coding agent search your past sessions from a single search box.
Spool lets Claude Code, Codex, Gemini CLI, Antigravity CLI, and any coding agent search your past sessions from a single search box.

### Every agent session, indexed automatically.

Spool watches `~/.claude/`, `~/.codex/`, and Gemini CLI's `~/.gemini/tmp/*/chats` in real time. Every conversation you have with Claude Code, Codex, or Gemini CLI — searchable the moment it's written.
Spool watches `~/.claude/`, `~/.codex/`, Gemini CLI's `~/.gemini/tmp/*/chats`, and Antigravity CLI's `~/.gemini/antigravity-cli/brain/` in real time. Every conversation you have with Claude Code, Codex, Gemini CLI, or Antigravity CLI — searchable the moment it's written.

### Context that flows back in.

Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/main/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function ensureAgentSearchCwd(): string {
* the Spool-authored session write.
*/
function agentIdToSource(agentId: string): SessionSource | null {
if (agentId === 'claude' || agentId === 'codex' || agentId === 'gemini' || agentId === 'opencode') return agentId
if (agentId === 'claude' || agentId === 'codex' || agentId === 'gemini' || agentId === 'antigravity' || agentId === 'opencode') return agentId
return null
}

Expand Down Expand Up @@ -873,12 +873,12 @@ export class AcpManager {
// after stripping the prelude only the bare query remains as the first
// user message — clean derived title, clean FTS, clean session detail.
const systemBody = [
'You have access to a local knowledge base called Spool that indexes the user\'s AI coding sessions (Claude Code, Codex CLI, Gemini CLI, OpenCode).',
'You have access to a local knowledge base called Spool that indexes the user\'s AI coding sessions (Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode).',
'',
'The database is at ~/.spool/spool.db (SQLite with FTS5). You can query it directly with the `sqlite3` CLI.',
'',
'── Schema ──',
' sources(id, name TEXT, base_path TEXT) -- "claude", "codex", "gemini", or "opencode"',
' sources(id, name TEXT, base_path TEXT) -- "claude", "codex", "gemini", "antigravity", or "opencode"',
' projects(id, source_id, slug, display_path, display_name, last_synced)',
' sessions(id, project_id, source_id, session_uuid TEXT, title TEXT, started_at TEXT, ended_at TEXT, message_count INT, has_tool_use INT)',
' messages(id, session_id, source_id, role TEXT, content_text TEXT, timestamp TEXT, tool_names TEXT)',
Expand All @@ -893,7 +893,7 @@ export class AcpManager {
'',
'Important:',
'- Interpret the user\'s intent and decide what to search. Don\'t just match their exact words.',
'- If the user names a specific source (claude/codex/gemini/opencode), only return results from that source unless they explicitly ask for cross-source search.',
'- If the user names a specific source (claude/codex/gemini/antigravity/opencode), only return results from that source unless they explicitly ask for cross-source search.',
'- For cross-source questions, first identify the relevant sources, then query each source separately, confirm hits or no-hits per source, and only then merge them into one answer.',
'- For temporal queries ("what did I do recently"), use explicit date filters and be conservative when comparing times across different sources.',
'- You may run multiple queries to find relevant information.',
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ ipcMain.handle('spool:search', (_e, { query, limit = 10, source, onlyPinned, ide
if (cached) return cached
}

const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'opencode'
const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'antigravity' || source === 'opencode'
? source
: undefined
const results = searchFragments(db, query, {
Expand All @@ -824,7 +824,7 @@ ipcMain.handle('spool:search-preview', (_e, { query, limit = 5, source }: { quer
const cached = searchCache.get(cacheKey)
if (cached) return cached

const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'opencode'
const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'antigravity' || source === 'opencode'
? source
: undefined
const fragments = searchSessionPreview(db, query, {
Expand Down Expand Up @@ -983,11 +983,11 @@ ipcMain.handle('spool:force-resync-session', (_e, { sessionUuid }: { sessionUuid

ipcMain.handle('spool:resume-cli', (_e, { sessionUuid, source, cwd }: { sessionUuid: string; source: string; cwd?: string }) => {
try {
const session = getSessionWithMessages(db, sessionUuid)?.session
const command = getSessionResumeCommand(source, sessionUuid)
if (!command) {
return { ok: false, error: `Session source "${source}" cannot be resumed from the CLI.` }
}
const session = getSessionWithMessages(db, sessionUuid)?.session
const resumeCwd = session
? resolveResumeWorkingDirectory(session)
: resolveResumeWorkingDirectory({
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,7 @@ export default function App() {
claudeCount={status?.claudeSessions ?? null}
codexCount={status?.codexSessions ?? null}
geminiCount={status?.geminiSessions ?? null}
antigravityCount={status?.antigravitySessions ?? null}
opencodeCount={status?.opencodeSessions ?? null}
themeEditor={themeEditor}
onThemeEditorChange={setThemeEditor}
Expand Down Expand Up @@ -1103,6 +1104,7 @@ export default function App() {
claudeCount={status?.claudeSessions ?? null}
codexCount={status?.codexSessions ?? null}
geminiCount={status?.geminiSessions ?? null}
antigravityCount={status?.antigravitySessions ?? null}
opencodeCount={status?.opencodeSessions ?? null}
themeEditor={themeEditor}
onThemeEditorChange={setThemeEditor}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/components/AiAnswerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default function AiAnswerCard({ answer, streaming, agentName, sources, er
)}

{/* CTA — only shown when a resume target is wired (i.e. agent has a
source we persisted a session row for: claude/codex/gemini/opencode). */}
source we persisted a session row for: claude/codex/gemini/antigravity/opencode). */}
{!streaming && answer && onResume && (
<button
onClick={onResume}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/components/FragmentResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export default function FragmentResults({ results, query, onOpenSession, default
}

function formatSourceFilterLabel(source: string): string {
if (source === 'claude' || source === 'codex' || source === 'gemini' || source === 'opencode') {
if (source === 'claude' || source === 'codex' || source === 'gemini' || source === 'antigravity' || source === 'opencode') {
return getSessionSourceLabel(source)
}
return source
Expand Down
7 changes: 6 additions & 1 deletion packages/app/src/renderer/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface Props {
claudeCount: number | null
codexCount: number | null
geminiCount: number | null
antigravityCount: number | null
opencodeCount: number | null
themeEditor: ThemeEditorStateV1
onThemeEditorChange: (next: ThemeEditorStateV1) => void
Expand Down Expand Up @@ -154,6 +155,7 @@ export default function SettingsPanel({
claudeCount,
codexCount,
geminiCount,
antigravityCount,
opencodeCount,
themeEditor,
onThemeEditorChange,
Expand Down Expand Up @@ -252,7 +254,7 @@ export default function SettingsPanel({
<AppearanceTab themeEditor={themeEditor} onThemeEditorChange={onThemeEditorChange} />
)}
{activeTab === 'shortcuts' && <ShortcutsTab />}
{activeTab === 'sources' && <SourcesTab claudeCount={claudeCount} codexCount={codexCount} geminiCount={geminiCount} opencodeCount={opencodeCount} />}
{activeTab === 'sources' && <SourcesTab claudeCount={claudeCount} codexCount={codexCount} geminiCount={geminiCount} antigravityCount={antigravityCount} opencodeCount={opencodeCount} />}
{activeTab === 'agent' && <AgentTab />}
{activeTab === 'account' && <SettingsAccount />}
{activeTab === 'labs' && <LabsTab />}
Expand Down Expand Up @@ -439,11 +441,13 @@ function SourcesTab({
claudeCount,
codexCount,
geminiCount,
antigravityCount,
opencodeCount,
}: {
claudeCount: number | null
codexCount: number | null
geminiCount: number | null
antigravityCount: number | null
opencodeCount: number | null
}) {
const { t } = useTranslation()
Expand All @@ -453,6 +457,7 @@ function SourcesTab({
<BuiltInSource name={getSessionSourceLabel('claude')} color={getSessionSourceColor('claude')} count={claudeCount} />
<BuiltInSource name={getSessionSourceLabel('codex')} color={getSessionSourceColor('codex')} count={codexCount} />
<BuiltInSource name={getSessionSourceLabel('gemini')} color={getSessionSourceColor('gemini')} count={geminiCount} />
<BuiltInSource name={getSessionSourceLabel('antigravity')} color={getSessionSourceColor('antigravity')} count={antigravityCount} />
<BuiltInSource name={getSessionSourceLabel('opencode')} color={getSessionSourceColor('opencode')} count={opencodeCount} />
</Section>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"pinnedSessions": "Angeheftet",
"viewAll": "Alle anzeigen",
"empty_title": "Noch keine Sitzungen",
"empty_body": "Öffnen Sie Claude Code, Codex, Gemini oder OpenCode, um eine Sitzung zu starten – Spool indexiert sie automatisch.",
"empty_body": "Öffnen Sie Claude Code, Codex, Gemini, Antigravity oder OpenCode, um eine Sitzung zu starten – Spool indexiert sie automatisch.",
"section_pinned": "Angeheftet · {{count}} Sitzung",
"section_pinned_other": "Angeheftet · {{count}} Sitzungen",
"section_recent": "ZULETZT",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"pinnedSessions": "Pinned",
"viewAll": "View all",
"empty_title": "No sessions yet",
"empty_body": "Open Claude Code, Codex, Gemini, or OpenCode to start a session — Spool indexes it automatically.",
"empty_body": "Open Claude Code, Codex, Gemini, Antigravity, or OpenCode to start a session — Spool indexes it automatically.",
"section_pinned": "Pinned · {{count}} session",
"section_pinned_other": "Pinned · {{count}} sessions",
"section_recent": "RECENT",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"pinnedSessions": "Épinglés",
"viewAll": "Tout afficher",
"empty_title": "Aucune session pour l'instant",
"empty_body": "Ouvrez Claude Code, Codex, Gemini ou OpenCode pour démarrer une session — Spool l'indexe automatiquement.",
"empty_body": "Ouvrez Claude Code, Codex, Gemini, Antigravity ou OpenCode pour démarrer une session — Spool l'indexe automatiquement.",
"section_pinned": "Épinglé · {{count}} session",
"section_pinned_other": "Épinglés · {{count}} sessions",
"section_recent": "RÉCENT",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/renderer/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
"pinnedSessions": "고정됨",
"viewAll": "전체 보기",
"empty_title": "아직 세션이 없습니다",
"empty_body": "Claude Code, Codex, Gemini, OpenCode를 열어 세션을 시작하세요 — Spool이 자동으로 인덱싱합니다.",
"empty_body": "Claude Code, Codex, Gemini, Antigravity, OpenCode를 열어 세션을 시작하세요 — Spool이 자동으로 인덱싱합니다.",
"section_pinned_other": "고정됨 · {{count}}개",
"section_recent": "최근",
"bucket_today": "오늘",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/renderer/lib/compose-from-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const SOURCE_LABEL: Record<SessionSource, string> = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
antigravity: 'Antigravity',
opencode: 'OpenCode',
}

Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/shared/resumeCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('getSessionResumeCommandPrefix', () => {
expect(getSessionResumeCommandPrefix('claude')).toBe('claude --resume')
expect(getSessionResumeCommandPrefix('codex')).toBe('codex resume')
expect(getSessionResumeCommandPrefix('gemini')).toBe('gemini --resume')
expect(getSessionResumeCommandPrefix('antigravity')).toBe('agy --conversation')
})

it('returns null for unsupported sources', () => {
Expand All @@ -18,6 +19,7 @@ describe('getSessionResumeCommand', () => {
expect(getSessionResumeCommand('claude', 'test-session-uuid')).toBe("claude --resume 'test-session-uuid'")
expect(getSessionResumeCommand('codex', '11111111-2222-4333-8444-555555555555')).toBe("codex resume '11111111-2222-4333-8444-555555555555'")
expect(getSessionResumeCommand('gemini', '99999999-2222-4333-8444-555555555555')).toBe("gemini --resume '99999999-2222-4333-8444-555555555555'")
expect(getSessionResumeCommand('antigravity', 'conv-uuid-123')).toBe("agy --conversation 'conv-uuid-123'")
})

it('escapes embedded single quotes safely', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/shared/resumeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const RESUME_COMMAND_PREFIXES: Record<string, string> = {
claude: 'claude --resume',
codex: 'codex resume',
gemini: 'gemini --resume',
antigravity: 'agy --conversation',
opencode: 'opencode --session',
}

Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/shared/sessionSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const SESSION_SOURCE_META = {
color: '#5887D0',
colorDark: '#8AB0E5',
},
antigravity: {
label: 'Antigravity',
shortLabel: 'agy',
color: '#6B5CA5',
colorDark: '#9B8FD0',
},
opencode: {
label: 'OpenCode',
shortLabel: 'opencode',
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { Command } from 'commander'
import { getDB, listRecentSessionsPage } from '@spool-lab/core'
import { printSession } from '../format.js'

const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'opencode'])
const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'antigravity', 'opencode'])

export const listCommand = new Command('list')
.description('List recent AI sessions')
.option('-n, --limit <n>', 'Max results', '20')
.option('-s, --source <name>', 'Filter by source: claude|codex|gemini|opencode')
.option('-s, --source <name>', 'Filter by source: claude|codex|gemini|antigravity|opencode')
.option('-p, --project <path>', 'Filter by project path substring')
.option('--json', 'Output as JSON')
.action((opts: { limit: string; source?: string; project?: string; json?: boolean }) => {
Expand Down
Loading