Skip to main content

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 (tables session / message / part). Legacy per-project <repo>/.opencode/opencode.db files 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
  • 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. --exact keeps 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, !word to exclude, ^word / word$ to anchor, or a | b for OR.
  • Uses newer fzf features to make the picker behave like a small TUI:
    • reload-sync for clean initial load and reindex without flicker
    • change-nth + FZF_NTH to switch search scopes on the fly
    • click-header so the scope tags in the header are clickable
    • bg-transform-* + {*f} to render a live footer summary of matches
    • zero event to show a clear "no matches" hint with a recovery tip
    • --history for persistent query history between runs
    • --tmux so it opens in a popup automatically when run inside tmux
    • --id-nth for cross-reload tracking by session identity
    • --cycle for wrap-around list navigation
    • --keep-right so the title field stays visible on long rows
    • --exact + --scheme=history + --tiebreak=index so matches are literal (precise) and recency wins ties
    • --smart-case so queries only become case-sensitive when you type an uppercase letter
    • --accept-nth for 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. Using exec-after-exit (rather than fzf's become action) 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 -C suppresses 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) or pi --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.0 because it relies on reload-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

fzf_ai-2.0.0.tar.gz (41.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fzf_ai-2.0.0-py3-none-any.whl (39.4 kB view details)

Uploaded Python 3

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

Hashes for fzf_ai-2.0.0.tar.gz
Algorithm Hash digest
SHA256 b65ba90be0507ec964355731004451fcaeaa7d6d43101dab74789172cecdd841
MD5 df0c4596f1dacf1dd7fa82d9a7106b5f
BLAKE2b-256 20c5edc2835434fef308b169b6f82e4069d4e1c5bd5e31c5da47576bd58dc140

See more details on using hashes here.

Provenance

The following attestation bundles were made for fzf_ai-2.0.0.tar.gz:

Publisher: pypi-publish.yml on tuxcanfly/fzf-ai

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fzf_ai-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 095ddcfa0eed8837ce4ddceae7afacc21b346bac4ba2fc72fcb2432f2b727172
MD5 da56a7e6950cfcfcdf9937c6b590d6ef
BLAKE2b-256 d9a3c7a397373fd9b5bc0c90dfb332f57a478b70709711fe869977f3a3d403fd

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page