Fuzzy-find and resume any AI coding session across multiple AI assistants
Project description
fzf-ai
Fuzzy-find and resume any AI coding session across claude-code, codex, opencode, droid (factory), and pi from a single fzf picker.
┌──── AI coding sessions ──────────────────────────────────────────────────┐
│ claude 2026-04-17 03:16 6 ~/Work/fzfai refactor preview │
│▶ codex 2026-04-16 12:01 53 ~/Work/kimi-ai/... ai-missions impl │
│ pi 2026-04-16 11:16 30 ~/Work/kimi-ai/... hi │
│ droid 2026-04-04 17:47 20 ~/Work/droid-ai/... tui todo app │
│ opencode 2025-08-11 20:03 2 ~/ Greeting message │
└──────────────────────────────────────────────────────────────────────────┘
What it does
- Indexes every session on disk:
- claude ─
~/.claude/projects/<proj>/<uuid>.jsonl - codex ─
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl - opencode ─ the global sqlite db at
~/.local/share/opencode/opencode.db(tablessession/message/part). Legacy per-project<repo>/.opencode/opencode.dbfiles are deliberately skipped — those sessions are not resumable by current opencode and just created ghost rows. - droid ─
~/.factory/sessions/<proj>/<uuid>.jsonl - pi ─
~/.pi/agent/sessions/<proj>/<iso>_<uuid>.jsonl
- claude ─
- Sorts newest-first and shows a rich ANSI preview of the real conversation (first real prompt, last user prompt, recent replies, tool calls, reasoning, model, cwd). Boilerplate environment / policy blocks are hidden from the preview when possible.
- Smart-case exact matching by default across agent / cwd / title /
hidden session content snippets.
--exactkeeps matches high-signal because the hidden content blob is a 4000-char concatenation of prompts where fuzzy matching would otherwise flood the picker with incidental letter-order hits. Recency breaks ties via--scheme=history+--tiebreak=index. Prefix a word with'to fuzzy-match that term,!wordto exclude,^word/word$to anchor, ora | bfor OR. - Uses newer
fzffeatures to make the picker behave like a small TUI:reload-syncfor clean initial load and reindex without flickerchange-nth+FZF_NTHto switch search scopes on the flyclick-headerso the scope tags in the header are clickablebg-transform-*+{*f}to render a live footer summary of matcheszeroevent to show a clear "no matches" hint with a recovery tip--historyfor persistent query history between runs--tmuxso it opens in a popup automatically when run inside tmux--id-nthfor cross-reload tracking by session identity--cyclefor wrap-around list navigation--keep-rightso the title field stays visible on long rows--exact+--scheme=history+--tiebreak=indexso matches are literal (precise) and recency wins ties--smart-caseso queries only become case-sensitive when you type an uppercase letter--accept-nthfor clean output parsing without visible fields leaking
- On enter, fzf exits cleanly, then the launcher
execs the correct CLI from the session's original cwd. Usingexec-after-exit (rather than fzf'sbecomeaction) avoids a terminal-state race that caused codex to hang and opencode's input to freeze on hand-off.claude --resume <id>codex resume -C <cwd> <id>(explicit-Csuppresses codex's interactive "change directory?" prompt)opencode <cwd> --session <id>(passes the project dir as positional so opencode opens the correct db and finds the session)droid --resume <id>pi --session <path>(preferred) orpi --resume <id>
Install
Option 1: Using pip (recommended)
# Install from the source distribution
pip install fzf_ai-1.0.0.tar.gz
# Or install from PyPI when published
pip install fzf-ai
The pip installation will automatically add the scripts to your PATH if you have
the Python bin directory in your PATH (typically ~/.local/bin or ~/Library/Python/3.x/bin).
Option 2: Using Make (traditional method)
# Clone or download the repository
cd fzf-ai
make install-symlink
# Make sure ~/.local/bin is in your PATH
export PATH="$HOME/.local/bin:$PATH"
Option 3: Manual installation
# Add the bin directory to your PATH
export PATH="$PWD/bin:$PATH"
# Or create symlinks to a directory in your PATH
ln -s "$PWD/bin/fzf-ai" ~/.local/bin/fzf-ai
ln -s "$PWD/bin/fzf-ai-index" ~/.local/bin/fzf-ai-index
ln -s "$PWD/bin/fzf-ai-preview" ~/.local/bin/fzf-ai-preview
ln -s "$PWD/bin/fzf-ai-resume" ~/.local/bin/fzf-ai-resume
Option 3: Manual installation
# put the scripts on your PATH
ln -s "$PWD/bin/fzf-ai" ~/.local/bin/fzf-ai
ln -s "$PWD/bin/fzf-ai-index" ~/.local/bin/fzf-ai-index
ln -s "$PWD/bin/fzf-ai-preview" ~/.local/bin/fzf-ai-preview
ln -s "$PWD/bin/fzf-ai-resume" ~/.local/bin/fzf-ai-resume
Requires: fzf ≥ 0.63, python3, each AI CLI you want to
resume on your $PATH.
Homebrew
brew tap tuxcanfly/fzf-ai https://github.com/tuxcanfly/fzf-ai
brew install fzf-ai
Performance tuning
The indexer reads session stores in parallel across agents and within each
agent. Set FZFAI_INDEX_JOBS=<n> to override the per-agent worker count.
The default is min(32, cpu_count + 4).
PyPI Publishing
GitHub Actions trusted publishing is configured in
.github/workflows/pypi-publish.yml.
Regular validation for pushes and pull requests runs in
.github/workflows/ci.yml.
To enable it on PyPI, add a Trusted Publisher for project fzf-ai with:
- owner:
tuxcanfly - repository:
fzf-ai - workflow:
.github/workflows/pypi-publish.yml - environment:
pypi
If fzf-ai does not exist on PyPI yet, create a pending publisher under
your PyPI account with the same values.
Publishing flow:
git tag v1.0.0
git push origin v1.0.0
The workflow will build the sdist/wheel, run twine check, and publish to
PyPI using GitHub OIDC instead of a long-lived API token.
Usage
fzf-ai # browse everything
fzf-ai claude codex # only these agents
Search syntax (exact smart-case by default)
| query | meaning |
|---|---|
noita |
contains noita |
noita webgpu |
contains noita AND contains webgpu |
'word |
fuzzy-match for this term (unquote) |
^use |
starts with use |
.md$ |
ends with .md |
!draft |
excludes items containing draft |
| `a | b |
Queries are case-insensitive until you type an uppercase letter
(--smart-case). Sessions are pre-sorted by last-modified time and
--tiebreak=index preserves that order when match scores are equal.
The default is --exact because the hidden content blob is long
enough that fuzzy matching tends to return unrelated sessions whose
letters happen to appear in the right order.
Keys
| key | action |
|---|---|
enter |
resume session in its native CLI (cd to its cwd) |
ctrl-o |
open a shell in the session's working directory |
ctrl-e |
open the raw .jsonl / sqlite source in $EDITOR |
ctrl-y |
copy session id to clipboard |
ctrl-k |
copy the exact resume command to clipboard |
ctrl-p |
move up |
ctrl-n |
move down |
ctrl-r |
rebuild the index in place |
ctrl-s |
cycle search scope: all → cwd → title → content → agent |
? |
toggle preview pane |
alt-p |
cycle preview position / hide preview |
alt-w |
toggle preview wrap |
shift-up/down |
scroll preview by half a page |
ctrl-shift-up/down |
scroll preview by a full page |
pgup/pgdn |
page through the session list |
alt-< / alt-> |
jump to first / last session |
alt-1..5 |
filter to claude / codex / opencode / droid / pi |
alt-0 |
clear the temporary agent filter |
Scope Switching
The header is clickable. Click [all], [cwd], [title], [content],
or [agent] to change what fzf searches against. The current
scope is reflected in the prompt and in the dynamic list label.
Query History
Search history is persisted at:
${XDG_STATE_HOME:-~/.local/state}/fzf-ai/query-history
fzf-ai rebinds ctrl-p / ctrl-n back to list navigation, so query
history remains available through fzf's alternate history actions only if
you bind them yourself.
Footer Summary
The footer is computed asynchronously from the current match set using
fzf's {*f} placeholder. It shows:
- matched session count
- distinct project count
- summed message count
- per-agent counts for the current match set
How it fits together
bin/fzf-ai ─ bash launcher wiring advanced fzf bindings
bin/fzf-ai-index ─ python: walks every session store, emits 9-col TSV
bin/fzf-ai-preview ─ python: renders a conversation preview for fzf
bin/fzf-ai-resume ─ bash: cd into cwd and exec the right AI CLI
bin/fzf-ai-ui ─ bash: dynamic labels / scope switching / footer stats
The index format (TAB-separated):
1 agent(raw) 2 session_id 3 source ← hidden, machine only
4 agent(ui) 5 updated 6 msgs ← visible, padded + ANSI
7 cwd 8 title ← visible
9 search blob (hidden session content) ← pushed past the right edge,
used for content search only
fzf runs with:
--with-nth=4,5,6,7,8,9
That keeps columns 1-3 hidden from the list while still exposing:
- transformed field
1= agent - transformed field
4= cwd - transformed field
5= title - transformed field
6= hidden content blob
So change-nth can dynamically retarget the search scope without
rebuilding the list.
Additional Commands
Usage analytics
fzf-ai-stats # terminal dashboard
fzf-ai-stats --json # machine-readable JSON
fzf-ai-stats --days 7 # last 7 days only
Output:
fzf-ai Usage Analytics
────────────────────────────────────────────────────────────────
Sessions: 1,247 Messages: 89,432 Projects: 23
By Agent
claude ████████████████████ 534 (42.8%)
codex ██████████████ 412 (33.0%)
opencode ██████ 187 (15.0%)
pi ████ 114 (9.1%)
Daily Activity (last 14 days)
05-17 █████████████████████████ 12
05-18 ████████████████████████████████████ 20
...
Top Projects
fzf-ai ████████████████████ 45
kimi-ai ██████████████ 32
...
Metadata
Starred sessions: 8
Tagged sessions: 15 (42 total tags)
Session management
fzf-ai-actions tag <sid> <source> <tag> # add/remove tag
fzf-ai-actions star <sid> <source> # toggle star
fzf-ai-actions delete <sid> <source> # trash session
fzf-ai-actions export <sid> <source> # export to markdown
fzf-ai-actions rename <sid> <source> <tit> # custom title
fzf-ai-actions list-tags # list all tags
Keybindings (in picker)
| key | action |
|---|---|
alt-s |
star / unstar a session |
ctrl-d |
delete (trash) a session |
ctrl-t |
tag a session |
Plugin system
Add support for new AI coding assistants by creating a file in
bin/stores/:
# stores/copilot.py
from fzf_ai_index import Record
from pathlib import Path
SESSION_DIR = Path.home() / ".github" / "copilot" / "sessions"
def walk(cache: dict | None = None):
for path in SESSION_DIR.glob("*.jsonl"):
rec = Record(agent="copilot", session_id=path.stem, source=str(path))
rec.title = path.stem
yield rec
Then run: fzf-ai copilot
Syntax-highlighted preview
The preview window now highlights code blocks using Pygments with the Monokai colour scheme. Any fenced code block (```python, ```js, …) in a session message is rendered with syntax-coloured ANSI output.
Performance
| Scenario | v1.x | v2.0 | Speedup |
|---|---|---|---|
| Cold index, 100 sessions | ~1.2s | ~300ms | 4x |
| Cold index, 1000 sessions | ~12s | ~3s | 4x |
| opencode SQLite (100 sessions) | ~200ms | ~80ms | 2.5x |
| Preview load, cached | ~50ms | ~15ms | 3x |
Key optimisations:
- orjson — compiled JSON decoder, 2-3x faster than stdlib
- ProcessPoolExecutor — true multi-core parallelism for file parsing
- Substring pre-filter — skip JSON decode on irrelevant lines (5x)
- SQLite JOIN — single query replaces 3 for opencode
- Index cache — avoid re-parsing unchanged files
Notes
- The indexer reads JSONL session stores in parallel across agents and
within each agent. Set
FZFAI_INDEX_JOBS=<n>to override the per-agent worker count. - The launcher now requires
fzf >= 0.63.0because it relies onreload-sync, footer sections, async transforms, and{*f}.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fzf_ai-2.0.0.tar.gz.
File metadata
- Download URL: fzf_ai-2.0.0.tar.gz
- Upload date:
- Size: 41.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b65ba90be0507ec964355731004451fcaeaa7d6d43101dab74789172cecdd841
|
|
| MD5 |
df0c4596f1dacf1dd7fa82d9a7106b5f
|
|
| BLAKE2b-256 |
20c5edc2835434fef308b169b6f82e4069d4e1c5bd5e31c5da47576bd58dc140
|
Provenance
The following attestation bundles were made for fzf_ai-2.0.0.tar.gz:
Publisher:
pypi-publish.yml on tuxcanfly/fzf-ai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fzf_ai-2.0.0.tar.gz -
Subject digest:
b65ba90be0507ec964355731004451fcaeaa7d6d43101dab74789172cecdd841 - Sigstore transparency entry: 1682332101
- Sigstore integration time:
-
Permalink:
tuxcanfly/fzf-ai@5fa57399a6fd1483905ddd9634d0306e4085d1f2 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/tuxcanfly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@5fa57399a6fd1483905ddd9634d0306e4085d1f2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fzf_ai-2.0.0-py3-none-any.whl.
File metadata
- Download URL: fzf_ai-2.0.0-py3-none-any.whl
- Upload date:
- Size: 39.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
095ddcfa0eed8837ce4ddceae7afacc21b346bac4ba2fc72fcb2432f2b727172
|
|
| MD5 |
da56a7e6950cfcfcdf9937c6b590d6ef
|
|
| BLAKE2b-256 |
d9a3c7a397373fd9b5bc0c90dfb332f57a478b70709711fe869977f3a3d403fd
|
Provenance
The following attestation bundles were made for fzf_ai-2.0.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on tuxcanfly/fzf-ai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fzf_ai-2.0.0-py3-none-any.whl -
Subject digest:
095ddcfa0eed8837ce4ddceae7afacc21b346bac4ba2fc72fcb2432f2b727172 - Sigstore transparency entry: 1682332169
- Sigstore integration time:
-
Permalink:
tuxcanfly/fzf-ai@5fa57399a6fd1483905ddd9634d0306e4085d1f2 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/tuxcanfly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@5fa57399a6fd1483905ddd9634d0306e4085d1f2 -
Trigger Event:
push
-
Statement type: