diff --git a/plugins/ai-session-search/README.md b/plugins/ai-session-search/README.md
new file mode 100644
index 000000000..42319b15e
--- /dev/null
+++ b/plugins/ai-session-search/README.md
@@ -0,0 +1,78 @@
+# ai-session-search
+
+fzf-style fuzzy search across the session histories of multiple AI CLIs:
+**Claude Code / Codex / Copilot / Gemini**. Hit a hotkey to pop up a list, type
+`claude` / `codex` / `copilot` / `gemini` to filter by tool, and press Enter to
+`resume` the session right in its original directory.
+
+To use it, add `ai-session-search` to the plugins array in your zshrc file:
+
+```zsh
+plugins=(... ai-session-search)
+```
+
+## Requirements
+
+This plugin requires [`fzf`](https://github.com/junegunn/fzf) (the picker, 0.35+
+recommended) and [`jq`](https://stedolan.github.io/jq/) (to parse the session
+files):
+
+```sh
+brew install fzf jq # macOS
+sudo apt install fzf jq # Debian/Ubuntu
+```
+
+Browsing and previewing work with just `fzf` + `jq` — scanning reads the session
+files directly. To actually *resume* a session you also need the matching CLI on
+your `PATH` (`claude` / `codex` / `copilot` / `gemini`).
+
+## Usage
+
+Default keybinding: Ctrl-X Ctrl-W (does not clobber zsh's
+built-in Ctrl-W backward-kill-word). You can also run it as a command:
+`ai-sessions` or `ai-sessions codex` (with an initial filter term).
+
+In the picker:
+
+| Key | Action |
+|---------|--------|
+| type | fuzzy-filter (the provider name is part of each row, so typing `claude` narrows to Claude) |
+| Enter | resume the selected session in its original directory |
+| Ctrl-O | open the raw `.jsonl` in `$EDITOR` |
+| Ctrl-/ | toggle the preview pane |
+| Ctrl-R | rescan |
+
+## How it works
+
+- **Claude**: scans `~/.claude/projects/
/.jsonl` (skips `subagents/`
+ sub-agent sessions); `cwd` is read from the `cwd` field inside the file;
+ resume uses `claude --resume `.
+- **Codex**: scans `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`; `cwd` and `id`
+ come from `session_meta`; resume uses `codex resume `.
+- **Copilot**: scans `~/.copilot/session-state//events.jsonl`; `cwd` /
+ model / version come from the `session.start` event, the `id` is the directory
+ name; resume uses `copilot --resume=`.
+- **Gemini**: scans `~/.gemini/tmp//logs.json` (gemini-cli format,
+ **experimental**; empty if gemini-cli isn't installed; gemini-cli has no
+ resume-by-id, so it just opens in the directory).
+
+Each row's preview is the first real user message, with injected context like
+`` and `AGENTS.md` filtered out. Sessions whose original
+directory no longer exists are hidden (set `AISS_SHOW_MISSING=1` to keep them).
+At this scale (dozens of files) live scanning is plenty — no cache needed.
+
+## Settings
+
+All these settings should go in your zshrc file, before Oh My Zsh is sourced.
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `AISS_KEYBIND` | `^X^W` | Keybinding for the picker widget (e.g. `^G`) |
+| `AISS_CLAUDE_DIR` | `~/.claude/projects` | Claude Code sessions directory |
+| `AISS_CODEX_DIR` | `~/.codex/sessions` | Codex sessions directory |
+| `AISS_COPILOT_DIR` | `~/.copilot/session-state` | Copilot CLI sessions directory |
+| `AISS_GEMINI_DIR` | `~/.gemini/tmp` | Gemini CLI sessions directory |
+| `AISS_PREVIEW_LEN` | `100` | Chars of the preview snippet shown in the list |
+| `AISS_SHOW_MISSING` | unset | Set to `1` to keep sessions whose directory was deleted |
+
+> **Note:** `AISS_KEYBIND` must be set *before* Oh My Zsh sources the plugin.
diff --git a/plugins/ai-session-search/ai-session-search.plugin.zsh b/plugins/ai-session-search/ai-session-search.plugin.zsh
new file mode 100644
index 000000000..075354071
--- /dev/null
+++ b/plugins/ai-session-search/ai-session-search.plugin.zsh
@@ -0,0 +1,117 @@
+# ai-session-search.plugin.zsh
+# fzf-style fuzzy search over AI CLI session histories (Claude / Codex / Gemini).
+#
+# Install: source this file from ~/.zshrc, e.g.
+# source ~/plugins/ai-session-search/ai-session-search.plugin.zsh
+# (oh-my-zsh: drop the folder in ~/.oh-my-zsh/custom/plugins/ and add
+# `ai-session-search` to your plugins=(...) array.)
+#
+# Default keybinding: Ctrl-X Ctrl-W (does NOT clobber Ctrl-W / backward-kill-word)
+# Change it by setting AISS_KEYBIND before sourcing, e.g. AISS_KEYBIND='^G'.
+#
+# In the picker:
+# - type to fuzzy-filter (the provider name is part of each row, so typing
+# "claude" / "codex" / "copilot" / "gemini" narrows to that tool)
+# - Enter resume the selected session in its original directory
+# - Ctrl-O open the raw .jsonl in $EDITOR
+# - Ctrl-/ toggle the preview pane
+# - Ctrl-R rescan
+#
+# Override scan locations with AISS_CLAUDE_DIR / AISS_CODEX_DIR / AISS_GEMINI_DIR.
+
+AISS_DIR=${0:A:h}
+AISS_HELPER="$AISS_DIR/aiss-helper.zsh"
+
+# Hard dependencies (fzf for the picker, jq for parsing the session files).
+# Checked lazily so we don't nag on every shell startup: nothing is printed at
+# load time, and the keybinding is still registered (so it works the moment the
+# user installs the missing tool — no re-source needed). `${+commands[x]}` reads
+# zsh's command hash table, so this never forks a subprocess.
+_aiss_missing_deps() {
+ local -a missing
+ (( ${+commands[fzf]} )) || missing+=(fzf)
+ (( ${+commands[jq]} )) || missing+=(jq)
+ (( ${#missing} )) || return 1
+ print -u2 "ai-session-search: missing required command(s): ${missing[*]}." \
+ "Install them first, e.g.: brew install ${missing[*]} (or your package manager)."
+ return 0
+}
+
+# Core picker. Prints a ready-to-run resume command to stdout (does not run it),
+# so it can drive both the ZLE widget and direct invocation.
+_aiss_select() {
+ emulate -L zsh
+ setopt pipefail 2>/dev/null
+ _aiss_missing_deps && return 1
+ local sel
+ sel=$(
+ zsh "$AISS_HELPER" scan | fzf \
+ --ansi \
+ --delimiter=$'\t' --with-nth=1 --nth=1 \
+ --no-sort \
+ --height=90% --layout=reverse --border \
+ --prompt='ai-sessions ❯ ' \
+ --query="${1:-}" \
+ --header='Enter: resume Ctrl-O: open Ctrl-/: preview Ctrl-R: rescan' \
+ --preview="zsh ${(q)AISS_HELPER} preview {2} {5}" \
+ --preview-window='right:55%:wrap:border-left' \
+ --bind='ctrl-/:toggle-preview' \
+ --bind="ctrl-r:reload(zsh ${(q)AISS_HELPER} scan)" \
+ --bind='ctrl-o:execute(${EDITOR:-vi} {5} < /dev/tty > /dev/tty)'
+ ) || return 1
+ [[ -z $sel ]] && return 1
+
+ local -a F
+ F=("${(@s: :)sel}") # split selected row on tabs
+ local provider=$F[2] id=$F[3] cwd=$F[4] file=$F[5]
+ [[ -z $id ]] && return 1
+ # Resume must run in the session's ORIGINAL directory: claude --resume locates
+ # the project by the current working directory. If that dir was deleted, recreate
+ # it (empty) so the project lookup still matches; never silently fall back to $PWD.
+ local cdpart
+ if [[ -z $cwd ]]; then
+ cdpart="cd ${(q)PWD}"
+ elif [[ -d $cwd ]]; then
+ cdpart="cd ${(q)cwd}"
+ else
+ cdpart="mkdir -p ${(q)cwd} && cd ${(q)cwd}"
+ fi
+
+ case $provider in
+ claude) print -r -- "( $cdpart && claude --resume ${(q)id} )" ;;
+ codex)
+ # Pin the model the session was recorded with, so `codex resume` doesn't
+ # warn about / silently switch to a different default model.
+ local model=""
+ [[ -r $file ]] && model=$(jq -rs 'first(.[] | .payload.model // empty) // empty' -- "$file" 2>/dev/null)
+ print -r -- "( $cdpart && codex resume ${(q)id}${model:+ -m ${(q)model}} )"
+ ;;
+ copilot) print -r -- "( $cdpart && copilot --resume=${(q)id} )" ;;
+ gemini) print -r -- "( $cdpart && gemini ) # gemini-cli has no resume-by-id; opens in dir" ;;
+ *) return 1 ;;
+ esac
+}
+
+# Direct command: ai-sessions [initial-query]
+ai-sessions() {
+ local cmd
+ cmd=$(_aiss_select "$@") || return 0
+ print -r -- "$cmd"
+ eval "$cmd"
+}
+
+# ZLE widget: pick, then drop the resume command on the command line and run it.
+_aiss_widget() {
+ local cmd
+ cmd=$(_aiss_select) || { zle reset-prompt; return 0; }
+ BUFFER=$cmd
+ zle reset-prompt
+ zle accept-line
+}
+zle -N _aiss_widget
+
+# Bind only in interactive shells (the fzf preview child shell sources nothing,
+# but guard anyway so non-interactive sourcing stays silent).
+if [[ -o interactive ]]; then
+ bindkey "${AISS_KEYBIND:-^X^W}" _aiss_widget
+fi
diff --git a/plugins/ai-session-search/aiss-helper.zsh b/plugins/ai-session-search/aiss-helper.zsh
new file mode 100644
index 000000000..3fb965699
--- /dev/null
+++ b/plugins/ai-session-search/aiss-helper.zsh
@@ -0,0 +1,299 @@
+#!/usr/bin/env zsh
+# aiss-helper.zsh — scanning + preview rendering for ai-session-search.
+# Invoked as a standalone script (not sourced):
+# zsh aiss-helper.zsh scan
+# zsh aiss-helper.zsh preview
+#
+# Kept separate from the plugin so the fzf child shell can call back into it
+# without needing the plugin's zsh functions in scope.
+
+emulate -L zsh
+setopt null_glob extended_glob pipefail 2>/dev/null
+zmodload zsh/datetime 2>/dev/null
+zmodload -F zsh/stat b:zstat 2>/dev/null
+
+CLAUDE_DIR=${AISS_CLAUDE_DIR:-$HOME/.claude/projects}
+CODEX_DIR=${AISS_CODEX_DIR:-$HOME/.codex/sessions}
+GEMINI_DIR=${AISS_GEMINI_DIR:-$HOME/.gemini/tmp}
+COPILOT_DIR=${AISS_COPILOT_DIR:-$HOME/.copilot/session-state}
+
+PREVIEW_MAX=${AISS_PREVIEW_LEN:-100} # chars of preview shown in the list
+
+# ---- jq filters (single-pass, slurped) ------------------------------------
+#
+# NOTE: assign via $(cat <<'JQ' ... JQ) — NOT `read -r -d '' VAR <<'JQ'`.
+# The `read` builtin consuming a heredoc HANGS when this script runs as an fzf
+# `--preview` command (fzf runs previews in a background process group, where
+# the builtin blocks). Forking `cat` sidesteps it. See README "Gotchas".
+
+# Claude: emit cwd \t first-real-user-prompt
+JQ_CLAUDE=$(cat <<'JQ'
+. as $a
+| (first($a[] | .cwd? // empty) // "") as $cwd
+| (first(
+ $a[]
+ | select(.type=="user")
+ | (.message.content) as $c
+ | (if ($c|type)=="string" then $c
+ elif ($c|type)=="array" then (first($c[] | select(.type=="text") | .text) // empty)
+ else empty end)
+ | select(type=="string" and . != "")
+ | select(test("^<")|not) # skip , etc.
+ | select(test("^Caveat:")|not)
+ ) // "") as $p
+| [ $cwd, ($p | gsub("[\\n\\t\\r]+"; " ")) ] | @tsv
+JQ
+)
+
+# Codex: emit id \t cwd \t first-real-user-prompt
+JQ_CODEX=$(cat <<'JQ'
+. as $a
+| (first($a[] | select(.type=="session_meta") | .payload)) as $m
+| (first(
+ $a[]
+ | select((.payload.type? // "")=="message" and (.payload.role? // "")=="user")
+ | .payload.content[]?
+ | (.text // empty)
+ | select(type=="string" and . != "")
+ | select(test("^(<(environment_context|user_instructions|permissions|INSTRUCTIONS)|# AGENTS\\.md)")|not)
+ ) // "") as $p
+| [ ($m.id // ""), ($m.cwd // ""), ($p | gsub("[\\n\\t\\r]+"; " ")) ] | @tsv
+JQ
+)
+
+# Copilot CLI: emit cwd \t first-real-user-prompt (events.jsonl event stream)
+JQ_COPILOT=$(cat <<'JQ'
+. as $a
+| (first($a[] | select(.type=="session.start") | .data.context.cwd) // "") as $cwd
+| (first(
+ $a[] | select(.type=="user.message") | .data.content
+ | select(type=="string" and . != "")
+ | select(test("^<")|not)
+ ) // "") as $p
+| [ $cwd, ($p | gsub("[\\n\\t\\r]+"; " ")) ] | @tsv
+JQ
+)
+
+# ---- preview renderers ----------------------------------------------------
+
+JQ_CLAUDE_PREVIEW=$(cat <<'JQ'
+.[]
+| select(.type=="user" or .type=="assistant")
+| (.type) as $role
+| (.message.content) as $c
+| (if ($c|type)=="string" then $c
+ elif ($c|type)=="array" then
+ ([ $c[]
+ | (.text
+ // (if .type=="tool_use" then "🔧 " + (.name // "tool")
+ elif .type=="tool_result" then "↩ [tool result]"
+ else empty end)) ] | join("\n"))
+ else "" end) as $t
+| select($t != "")
+| (if $role=="user" then "\u001b[1;36m▶ USER\u001b[0m" else "\u001b[1;32m◀ ASSISTANT\u001b[0m" end)
+ + "\n" + $t + "\n"
+JQ
+)
+
+JQ_CODEX_PREVIEW=$(cat <<'JQ'
+.[]
+| select((.payload.type? // "")=="message")
+| (.payload.role) as $role
+| select($role=="user" or $role=="assistant")
+| ([ .payload.content[]? | (.text // empty) ] | join("\n")) as $t
+| select($t != "")
+| select(($role=="user" and ($t | test("^(<(environment_context|user_instructions|permissions|INSTRUCTIONS)|# AGENTS\\.md)"))) | not)
+| (if $role=="user" then "\u001b[1;36m▶ USER\u001b[0m" else "\u001b[1;32m◀ ASSISTANT\u001b[0m" end)
+ + "\n" + $t + "\n"
+JQ
+)
+
+JQ_COPILOT_PREVIEW=$(cat <<'JQ'
+.[]
+| select(.type=="user.message" or .type=="assistant.message")
+| (.type) as $ty
+| (if $ty=="user.message" then (.data.content // "")
+ else ((.data.content // "")
+ + ([ (.data.toolRequests // [])[] | "\n🔧 " + (.name // "tool") ] | join(""))) end) as $t
+| select($t != "")
+| (if $ty=="user.message" then "\u001b[1;36m▶ USER\u001b[0m" else "\u001b[1;32m◀ ASSISTANT\u001b[0m" end)
+ + "\n" + $t + "\n"
+JQ
+)
+
+# Session metadata (for the preview header), emitted as one line whose fields
+# are joined with U+001F (unit separator) so empty fields don't collapse.
+JQ_CLAUDE_META=$(cat <<'JQ'
+. as $a
+| [ $a[] | .timestamp // empty ] as $ts
+| [ (($ts[0] // ""), ($ts[-1] // "")) | (sub("\\.[0-9]+Z$";"Z") | fromdateiso8601?) // 0 ] as $ep
+| [ (first($a[] | select(.type=="assistant") | .message.model // empty | select(test("^<")|not)) // "?"),
+ (first($a[] | .version // empty) // ""),
+ ((first($a[] | .gitBranch // empty) // "") | if . == "HEAD" then "" else . end),
+ (first($a[] | .cwd // empty) // ""),
+ ($ep[0]|tostring), ($ep[1]|tostring),
+ ([$a[] | select(.type=="user")] | length | tostring),
+ ([$a[] | select(.type=="assistant")] | length | tostring)
+ ] | join("\u001f")
+JQ
+)
+
+JQ_CODEX_META=$(cat <<'JQ'
+. as $a
+| [ $a[] | .timestamp // empty ] as $ts
+| [ (($ts[0] // ""), ($ts[-1] // "")) | (sub("\\.[0-9]+Z$";"Z") | fromdateiso8601?) // 0 ] as $ep
+| (first($a[] | select(.type=="session_meta") | .payload) // {}) as $m
+| [ (first($a[] | .payload.model // empty) // "?"),
+ ($m.model_provider // ""),
+ ($m.cli_version // ""),
+ ($m.cwd // ""),
+ ($ep[0]|tostring), ($ep[1]|tostring),
+ ([$a[] | select((.payload.role? // "")=="user")] | length | tostring),
+ ([$a[] | select((.payload.role? // "")=="assistant")] | length | tostring)
+ ] | join("\u001f")
+JQ
+)
+
+JQ_COPILOT_META=$(cat <<'JQ'
+. as $a
+| [ $a[] | .timestamp // empty ] as $ts
+| [ (($ts[0] // ""), ($ts[-1] // "")) | (sub("\\.[0-9]+Z$";"Z") | fromdateiso8601?) // 0 ] as $ep
+| (first($a[] | select(.type=="session.start") | .data)) as $s
+| [ ((last($a[] | select(.type=="assistant.message") | .data.model)) // (last($a[] | select(.type=="session.model_change") | .data.newModel)) // "?"),
+ ($s.copilotVersion // ""),
+ "",
+ ($s.context.cwd // ""),
+ ($ep[0]|tostring), ($ep[1]|tostring),
+ ([$a[] | select(.type=="user.message")] | length | tostring),
+ ([$a[] | select(.type=="assistant.message")] | length | tostring)
+ ] | join("\u001f")
+JQ
+)
+
+# ---------------------------------------------------------------------------
+
+# Render a boxed metadata header for the preview pane.
+# _aiss_header
+_aiss_header() {
+ local provider=$1 cwd=$2 branch=$3 model=$4 verline=$5 t0=$6 t1=$7 nu=$8 na=$9
+ local cwd_disp=${cwd/#$HOME/\~} d0 d1 dur day0 day1
+ if [[ $t0 == <-> ]] && (( t0 > 0 )); then
+ strftime -s d0 '%Y-%m-%d %H:%M' $t0; strftime -s day0 '%Y-%m-%d' $t0
+ else d0='?'; fi
+ if [[ $t1 == <-> ]] && (( t1 > 0 )); then
+ strftime -s day1 '%Y-%m-%d' $t1
+ # show the end date too when the session spans more than one day
+ if [[ $day1 == $day0 ]]; then strftime -s d1 '%H:%M' $t1
+ else strftime -s d1 '%m-%d %H:%M' $t1; fi
+ else d1='?'; fi
+ if [[ $t0 == <-> && $t1 == <-> ]] && (( t0 > 0 && t1 >= t0 )); then
+ local s=$(( t1 - t0 ))
+ if (( s >= 86400 )); then dur=$(printf '%dd%dh' $((s/86400)) $(((s%86400)/3600)))
+ elif (( s >= 3600 )); then dur=$(printf '%dh%dm' $((s/3600)) $(((s%3600)/60)))
+ elif (( s >= 60 )); then dur=$(printf '%dm' $((s/60)))
+ else dur=$(printf '%ds' $s); fi
+ else dur='?'; fi
+ local C=$'\e[36m' B=$'\e[1m' D=$'\e[2m' R=$'\e[0m'
+ print -r -- "${C}╭─ ${B}${provider}${R}${C} · ${model}${R}"
+ print -r -- "${C}│${R} 📁 ${cwd_disp}${branch:+ ${D}⎇ ${branch}${R}}"
+ print -r -- "${C}│${R} 🕐 ${d0} → ${d1} ${D}(${dur})${R}"
+ print -r -- "${C}│${R} 💬 ${nu} user · ${na} assistant${verline:+ ${D}${verline}${R}}"
+ print -r -- "${C}╰──────────────────────────────────────────${R}"
+ print -r -- ""
+}
+
+_emit() {
+ # _emit
+ local epoch=$1 provider=$2 id=$3 cwd=$4 preview=$5 file=$6
+ [[ -z $preview ]] && preview="(no prompt)"
+ local cwd_disp=${cwd/#$HOME/\~}
+ # epoch is kept only as a sort key (stripped before fzf sees it); the visible
+ # row is: provider cwd preview
+ # field layout: epoch \t display \t provider \t id \t cwd \t file
+ printf '%s\t%-6s %-34.34s %.*s\t%s\t%s\t%s\t%s\n' \
+ "$epoch" "$provider" "$cwd_disp" "$PREVIEW_MAX" "$preview" \
+ "$provider" "$id" "$cwd" "$file"
+}
+
+scan() {
+ local f epoch out cwd id preview
+ local -a parts
+
+ # --- Claude ---
+ for f in $CLAUDE_DIR/*/*.jsonl(.N); do
+ zstat -A epoch +mtime -- $f 2>/dev/null || continue
+ out=$(jq -rs "$JQ_CLAUDE" -- "$f" 2>/dev/null) || continue
+ parts=("${(@s: :)out}") # split on literal tab
+ cwd=$parts[1]; preview=$parts[2]
+ [[ -z $cwd ]] && cwd=$HOME
+ # skip sessions whose original project dir no longer exists (set
+ # AISS_SHOW_MISSING=1 to keep them)
+ [[ -z $AISS_SHOW_MISSING && ! -d $cwd ]] && continue
+ id=${f:t:r} # filename minus .jsonl
+ _emit "$epoch" claude "$id" "$cwd" "$preview" "$f"
+ done
+
+ # --- Codex ---
+ for f in $CODEX_DIR/**/rollout-*.jsonl(.N); do
+ zstat -A epoch +mtime -- $f 2>/dev/null || continue
+ out=$(jq -rs "$JQ_CODEX" -- "$f" 2>/dev/null) || continue
+ parts=("${(@s: :)out}")
+ id=$parts[1]; cwd=$parts[2]; preview=$parts[3]
+ [[ -z $id ]] && id=${f:t:r#rollout-*-} # fallback: trailing uuid in name
+ [[ -z $AISS_SHOW_MISSING && -n $cwd && ! -d $cwd ]] && continue
+ _emit "$epoch" codex "$id" "$cwd" "$preview" "$f"
+ done
+
+ # --- Copilot CLI (~/.copilot/session-state//events.jsonl) ---
+ for f in $COPILOT_DIR/*/events.jsonl(.N); do
+ zstat -A epoch +mtime -- $f 2>/dev/null || continue
+ out=$(jq -rs "$JQ_COPILOT" -- "$f" 2>/dev/null) || continue
+ parts=("${(@s: :)out}")
+ cwd=$parts[1]; preview=$parts[2]
+ [[ -z $AISS_SHOW_MISSING && -n $cwd && ! -d $cwd ]] && continue
+ id=${f:h:t} # parent dir name = session UUID
+ _emit "$epoch" copilot "$id" "$cwd" "$preview" "$f"
+ done
+
+ # --- Gemini (experimental: gemini-cli stores ~/.gemini/tmp//logs.json) ---
+ for f in $GEMINI_DIR/*/logs.json(.N); do
+ zstat -A epoch +mtime -- $f 2>/dev/null || continue
+ preview=$(jq -r 'try (map(select(.type=="user")) | .[0].message) // "" catch ""' -- "$f" 2>/dev/null)
+ cwd=${f:h:t}
+ _emit "$epoch" gemini "${f:h:t}" "$cwd" "${preview//[$'\n\t']/ }" "$f"
+ done
+}
+
+preview() {
+ local provider=$1 file=$2
+ [[ -r $file ]] || { print -r -- "(file not found: $file)"; return 0; }
+ local meta model mp ver branch cwd t0 t1 nu na
+ case $provider in
+ claude)
+ meta=$(jq -rs "$JQ_CLAUDE_META" -- "$file" 2>/dev/null)
+ IFS=$'\x1f' read -r model ver branch cwd t0 t1 nu na <<<"$meta"
+ _aiss_header claude "$cwd" "$branch" "$model" "${ver:+Claude Code $ver}" "$t0" "$t1" "$nu" "$na"
+ jq -rs "$JQ_CLAUDE_PREVIEW" -- "$file" 2>/dev/null
+ ;;
+ codex)
+ meta=$(jq -rs "$JQ_CODEX_META" -- "$file" 2>/dev/null)
+ IFS=$'\x1f' read -r model mp ver cwd t0 t1 nu na <<<"$meta"
+ _aiss_header codex "$cwd" "" "$model" "codex ${ver}${mp:+ · $mp}" "$t0" "$t1" "$nu" "$na"
+ jq -rs "$JQ_CODEX_PREVIEW" -- "$file" 2>/dev/null
+ ;;
+ copilot)
+ meta=$(jq -rs "$JQ_COPILOT_META" -- "$file" 2>/dev/null)
+ IFS=$'\x1f' read -r model ver branch cwd t0 t1 nu na <<<"$meta"
+ _aiss_header copilot "$cwd" "$branch" "$model" "${ver:+Copilot CLI $ver}" "$t0" "$t1" "$nu" "$na"
+ jq -rs "$JQ_COPILOT_PREVIEW" -- "$file" 2>/dev/null
+ ;;
+ gemini) jq -r '.[]? | "\u001b[1;36m" + (.type // "?") + "\u001b[0m\n" + (.message // (.|tostring)) + "\n"' -- "$file" 2>/dev/null ;;
+ *) cat -- "$file" ;;
+ esac
+}
+
+case $1 in
+ scan) scan | sort -t$'\t' -k1,1rn | cut -f2- ;;
+ preview) shift; preview "$@" ;;
+ *) print -u2 "usage: aiss-helper.zsh {scan|preview }"; return 2 ;;
+esac