From 54ea46f7d74b696475a045e909122c794e50a3d9 Mon Sep 17 00:00:00 2001 From: "xinming.xu" Date: Wed, 24 Jun 2026 17:41:57 +0800 Subject: [PATCH] feat(ai-session-search): add plugin Add a plugin that provides an fzf-style fuzzy picker over the session histories of Claude Code, Codex, Copilot CLI and Gemini CLI. Bound to Ctrl-X Ctrl-W (and available as the `ai-sessions` command), it lets you filter by tool and resume the selected session in its original directory. Co-Authored-By: Claude Opus 4.8 --- plugins/ai-session-search/README.md | 78 +++++ .../ai-session-search.plugin.zsh | 117 +++++++ plugins/ai-session-search/aiss-helper.zsh | 299 ++++++++++++++++++ 3 files changed, 494 insertions(+) create mode 100644 plugins/ai-session-search/README.md create mode 100644 plugins/ai-session-search/ai-session-search.plugin.zsh create mode 100644 plugins/ai-session-search/aiss-helper.zsh 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